Mateusz Mazurek – programista z pasją

Python, architektura, ciekawostki ze świata IT

Programowanie Programowanie webowe Utrzymanie oprogramowania

Podgląd i zmiana wartości zmiennych in runtime – Python

Cześć,
jak pisałem we wcześniejszym wpisie, różne zdarzenia życiowe spowodowały że nagle mam znacznie więcej czasu, więc dziś poopowiadam Wam o ciekawym libie Python’owym – manhole. Z angielskiego manhole znaczy.. Właz. Kanalizacyjny. Taki jak mijacie na ulicach. A co on robi w Pythonie? Ano zgodnie z tytułem wpisu – pozwala wejść do programu w sposób interaktywny trochę tak „z boku”.

Ale powoli, napiszmy kawałek kodu który będzie agregował jakieś dane w pamięci. Niech ten program w odpowiedzi na wysyłanie do niego komunikatów wykonuje operacje które znajdują się w tym komunikacie. Uprośćmy to maksymalnie – komunikaty będą kazały wykonywać jedną z podstawowych działań matematycznych z aktualną wartością w pamięci a wartością wysłaną w owym komunikacie. Brzmi zagmatwanie? Nieee, jest turbo proste.

Stwórzmy 3 pliki w takiej hierarchii:

data_store
  __init__.py
subscriber
  __init__.py
main.py

I plik w folderze data_store niech ma taką zawartość:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import inspect
myself = lambda: inspect.stack()[1][3]

class DataStore():
    def __init__(self):
    self.current_value = 0
    self.history = []

def __add__(self, other):
    self.current_value = self.current_value + other
    self.history.append([
        other,
        str(myself())
    ])

def __mul__(self, other):
    self.current_value = self.current_value + other
    self.history.append([
        other,
        str(myself())
    ])

def __sub__(self, other):
    self.current_value = self.current_value - other
    self.history.append([
        other,
        str(myself())
    ])

def __div__(self, other):
    self.current_value = self.current_value / other
    self.history.append([
        other,
        str(myself())
    ])

def __str__(self):
    return str(self.current_value)

data_store = DataStore()

Nic skomplikowanego – klasa definiuje zachowania operatorów dodawania, odejmowania, mnożenia i dzielenia jako operacje na jednej z zmiennych tej klasy i zapisuje te zmiany jako historię.

Teraz plik w folderze subscriber:

1
2
3
4
5
6
7
8
9
import redis

def main(worker):
    redis_conn = redis.Redis()
    subscriber = redis_conn.pubsub()
    subscriber.subscribe('test')

    for item in subscriber.listen():
        worker(item['data'])

Co tu robimy? Prawie nic. Na każdą wiadomość która przyjdzie na kanał redisowy o nazwe „test” reagujemy wykonując przekazaną w parametrze funkcję. Banalne, nie?

I na koniec plik main.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from subscriber import main
from datastore import data_store
import operator
from json import loads

def worker(data):
    try:
        data = loads(data)
        print data
        op_func = getattr(operator, data['operation'])
        op_func(data_store, data['value'])
    except:
        pass

if name == 'main':
    main(worker)

Jako funkcję wejściową wykonujemy funkcję ze subscriber’a i przekazując mu jako argument naszą funkcję. Jaki będzie efekt? Bardzo prosty:

Jeśli na kanał wrzucimy 3 razy coś takiego:

redis-cli publish test '{"operation": "add", "value":6}'

To funkcja „worker” pobierze metodę dodającą i wykona na aktualnej wartości przechowywanej w data storze tę metodę. Co przy pierwszym wysłaniu spowoduje dodanie do 0 liczby 6. Drugie – do liczby 6 kolejną 6. I trzecie – do liczby 12(sumy) – kolejną 6. Co powoduje przechowanie w pamięci liczby 18 i 3 elementowej listy z historią tych operacji.

I teraz, na białym koniu, cały na biało – wjeżdża manhole.

Bo co jeśli w kodzie jest np. błąd i wartość przechowywana w pamięci jest zepsuta? Oczywiście można logować każdą zmianę i w myśl event source’ingu – odtworzyć stan i tak go skorygować dodatkowymi komunikatami by był poprawny, ale raz że to dość pracochłonne zajęcie a dwa wymusza wyczyszczenie pamięci programu co może wiązać się różnymi konsekwencjami. Np. straceniem tych danych które są poprawne lub przerwę w działaniu usługi.

Zmodyfikujmy więc nasz kod troszkę:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from subscriber import main
from datastore import data_store
import operator
from json import loads
import manhole

def worker(data):
    if data == 'enable_manhole':
        manhole.install(locals={'data_store':data_store})
    else:
        try:
            data = loads(data)
            print data
            op_func = getattr(operator, data['operation'])
            op_func(data_store, data['value'])
        except:
            pass

if __name__ == '__main__':
    main(worker)

Dodaliśmy tu obsługę „zainstalowania” naszego włazu programistycznego. Wiec sprawdźmy to! Wyślijmy:

redis-cli publish test 'enable_manhole'

co spowoduje wypisanie na standardowe wyjście naszego programu:

Manhole[17744:1524079049.5979]: Patched <built-in function fork> and <built-in function fork>.
Manhole[17744:1524079049.5988]: Manhole UDS path: /tmp/manhole-17744
Manhole[17744:1524079049.5988]: Waiting for new connection (in pid:17744) ...

Co znaczy że manhole się zainstalował w osobnym wątku i udostępnia dostęp do programu (a dokładniej do zmiennej data_store co jest zdefiniowane parametrem locals) jako socket unixowy pod ścieżką /tmp/manhole-17744.

Połączmy się!

sudo nc -U /tmp/manhole-17744

Co spowoduje info o przyjęciu połączenia:

Manhole[17744:1524079333.6725]: Started ManholeConnectionThread thread. Checking credentials …
Manhole[17744:1524079333.6726]: Accepted connection on fd:5 from PID:18093 UID:0 GID:0

a my mamy dostęp do programu w formie interaktywnej konsoli Pythona:

możemy też edytować zmienne:

Wydaje mi się że łatwo zauważyć potencjał tej biblioteki.

Dzięki za wizytę,
Mateusz Mazurek

A może wolisz nowości na mail?

Subskrybuj
Powiadom o
guest

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.

1 Komentarz
Inline Feedbacks
View all comments
Maciej Michalec

Ciekawy artykuł. Zdaje się, że koniecznie muszę nadrobić swoje zaległości jeśli chodzi o redisa, bo kiedy w przeszłości miałem okazję używać tej bazy, to nigdy nie bawiłem się w niej kanałami, a wyglądają one na zdecydowanie przydatną funkcjonalność. ;)