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:
P1.instance_var1 = "New instance value"
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:
{'cls_var1': 'New value', 'instance_var1': 'New instance
variable', 'instance_var2': 3.14156, 'instance_var3': 0.9764029825441902}
{'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'