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
Mateusz M.

Pokaż komentarze

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

Ostatnie wpisy

Python 1.0 vs. 3.13: Co się zmieniło?

Cześć. Dziś luźny artykuł, bo dziś pobawimy się jedną z pierwszy wersji Pythona. Skompilujemy go i zobaczymy co tam w… Read More

1 tydzień ago

Podsumowanie: styczeń i luty 2025

Nowy rok czas zacząć! Więc lećmy z podsumowaniem. Nowy artykuł Nie uwierzycie, ale pojawił się na blogu nowy artykuł! Piszę… Read More

3 tygodnie ago

Just-in-time compiler (JIT) w Pythonie

Cześć! W Pythonie 3.13 dodano JITa! JIT, czyli just-in-time compiler to optymalizacja na która Python naprawdę długo czekał. Na nasze… Read More

1 miesiąc ago

Podsumowanie roku 2024

Cześć! Zapraszam na podsumowanie roku 2024. Książki W sumie rok 2024 był pod względem ilości książek nieco podobny do roku… Read More

1 miesiąc ago

Podsumowanie: wrzesień, październik, listopad i grudzień 2024

Podtrzymując tradycję, prawie regularnych podsumowań, zapraszam na wpis! Nie mogło obyć się bez Karkonoszy We wrześniu odwiedziłem z kolegą Karkonosze,… Read More

2 miesiące ago

Podsumowanie: maj, czerwiec, lipiec i sierpień 2024

Oj daaawnoo mnie tu nie było. Ale wakacje to był czas dużej liczby intensywnych wyjazdów i tak naprawdę, dopiero jakoś… Read More

6 miesięcy ago