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ść! Cieszę się, że mnie odwiedziłeś/aś. Zanim przejdziesz do artykułu chciałbym zwrocić Ci uwagę na to, że ten artykuł był pisany kilka lat temu (2018-04-18) miej więc proszę na uwadzę że rozwiązania i przemyślenia które tu znajdziesz nie muszą być aktualne. Niemniej jednak zachęcam do przeczytania.

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?

1 KOMENTARZ

  1. 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ść. ;)

ZOSTAW ODPOWIEDŹ

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

This site uses Akismet to reduce spam. Learn how your comment data is processed.