Ludzkie życie jest takie jakim uczyniły je ludzkie myśli.

Marek Aureliusz

W niedalekiej przeszłości zainteresował mnie temat tworzenia i inicjalizacji wielu instancji klas na podstawie danych otrzymanych formacie JSON i voilà. Pierwsze co przychodzi na myśl to mozolne dopasowywanie odpowiednich danych, do odpowiednich atrybutów obiektu. Tu górę wzięło lenistwo podpowiadając mi, że musi być szybszy i mniej pracochłonny sposób. Zagłębiając się w temat w jaki Python przechowuje dane klas i ich instancji natrafiłem na oświecającą wiedzę. Kolejny raz zaskoczyłem się jak wygodny w użyciu może być Python, a przynajmniej jak wygodny jest jest wygodny w mych zastosowaniach.

Nim zaczniemy zabawę na dobre potrzebny jest nam moduł z deklaracją prostej klasy na której instancjach przeprowadzimy eksperymenty pomagające zrozumieć co dzieje się pod maską.

#!/usr/bin/env python3
# -*- coding: 'utf-8' -*-

from datetime import datetime
import random

random.seed(datetime.now()) # Initialize seed with time

class Prototype(object):
    cls_var1 = "Class variable"
    cls_var2 = random.random()
    cls_var3 = ["Share", "It"]

    def __init__(self, var):
        self.instance_var1 = "Instance variable"
        self.instance_var2 = 3.14156
        self.instance_var3 = var

Na początku tworzymy dwie instancje naszej klasy Prototype:

P1 = Prototype(random.random())
P2 = Prototype(299792458)

Możemy teraz sprawdzić co zwierają słowniki opisujące instancje jak i klasę z których zostały utworzone. Zawartość P1.__class__.__dict__ prezentuje się w następujący sposób:

{'cls_var2': 0.462571030469879, 'cls_var3': ['Share', 'It'], '__module__':
'__main__',  'cls_var1': 'Class variable', '__dict__': <attribute '__dict__' of
'Prototype' objects>,   '__weakref__': <attribute '__weakref__' of 'Prototype'
objects>, '__doc__': None,  '__init__': <function __init__ at 0x7f61add5b1b8>}

Oprócz metod zdefiniowanych w klasie słownik P1.__class__.__dict__ umożliwia bezpośredni dostęp do wszystkich zmiennych cls_var*. Rodzi się pytanie co ze zmiennymi instance_var*? Otóż te przechowywane są w P1.__dict__. Rzućmy okiem na zawartość wspomnianego słownika:

{'instance_var1': 'Instance variable', 'instance_var2': 3.14156,
'instance_var3': 0.5605434189263687}

Analogicznie wykonajmy inspekcje słowników naszej drugiej instancji. P2.__class__.__dict__ prezentuje się w następujący sposób:

{'cls_var2': 0.462571030469879, 'cls_var3': ['Share', 'It'], '__module__':
'__main__',  'cls_var1': 'Class variable', '__dict__': <attribute '__dict__' of
'Prototype' objects>,  '__weakref__': <attribute '__weakref__' of 'Prototype'
objects>, '__doc__': None, '__init__': <function __init__ at 0x7f61add5b1b8>}

Słownik P2.__dict__:

{'instance_var1': 'Instance variable', 'instance_var2': 3.14156,
'instance_var3': 299792458} 

Ważna obserwacja to dostrzeżenie, że między instancjami różnice widoczne są tylko w słownikach instancji, czyli P1.__dict__ i P2.__dict__, które przechowują wartości zmiennych tworzonych w czasie inicjalizacji instancji.

Bazując na wiedzy jaką posiedliśmy będziemy eksplorować wspomniane obszary, poszukując możliwości jakie nam oferują. Krok pierwszy to poznanie sposobów w jakie możemy odczytać i zapisać zmienne instancji. Tu wyróżnić możemy dwa podejścia:

  • Klasyczne użycie notacji z "kropką" by przekazać instancji, że chcemy się odwołać P1.instance_var1 = "New instance value"
  • Odwołanie się do elemetów słownika P1.__dict__["instance_var1"] = "New instance value"

Wynik działań obu podejść jest tożsamy. Poza możliwością edycji i dobierania się do zmiennych możemy wykorzystując którąkolwiek z metod tworzyć nowe argumenty dla już istniejących instancji P1.instance_var1 = "New instance variable"

Zupełnie inaczej wygląda korzystanie ze słownika P1.__class__.__dict__.

Traceback (most recent call last):
  File "lesson.py", line 59, in <module>
    P1.__class__.__dict__["cls_var1"] = "New value"
TypeError: 'dictproxy' object does not support item assignment 

W oczy powinien rzucić się nam typ dictproxy jakim jest słownik P1.__class__.__dict__. Za przypisaniem wspomnianego typu do słownika klasy stoi umożliwienie optymalizacji w interpreterze Pythona, oraz zapewnienie stabilności. dictproxy wymusza tryb tylko do odczytu, dlatego też możemy czytać dane natomiast próba zapisu wywołuje wyjątek.

W jaki sposób możemy, zmienić wartość zmiennej w słowniku klasy? Interpreter Pythona zachowa się inaczej jeśli do atrybutu cls_var1 odwołamy się używając notacji z kropką: P1.cls_var1 = "New value". Efekt działania nie do końca będzie spełniał nasze oczekiwania. Szybka inspekcja słowników wszystko nam wyjaśni:

  • P1.__dict__: {'cls_var1': 'New value', 'instance_var1': 'New instance variable', 'instance_var2': 3.14156, 'instance_var3': 0.9764029825441902}
  • P1.__class__.__dict__: {'cls_var2': 0.5508205843860968, 'cls_var3': ['Share', 'It'], '__module__': '__main__', 'cls_var1': 'Class variable', '__dict__': <attribute '__dict__' of 'Prototype' objects>, '__weakref__': <attribute '__weakref__' of 'Prototype' objects>, '__doc__': None, '__init__': <function __init__ at 0x7f00c7cdf230>}

Zmienna cls_var1 w słowniku klasy w dalszym ciągu pozostała niezmieniona, a jedynie przesłonięta przez dodanie zmiennej o tej samej nazwie do słownika instancji. Reasumując, referencje do obierków w słowniku klasy pozostają zawsze stałe i mogą zostać jedynie przesłonięte.

Kończąc temat wspomnę o atrybucie __slots__. W wielkim skrócie zadeklarowanie klasy w ten sposób:

class Prototype(object):
    __slots__ = ['x', 'y']

ograniczy zmienne instancji do zdeklarowanych x oraz y. Wyjście poza ten zakres wywoła wyjątek:

Traceback (most recent call last):
  File "lesson.py", line 65, in <module>
    P3.z = 1
AttributeError: 'Prototype' object has no attribute 'z'
Artykuł dodano 2020-02-19