Atributele clasei Python: un ghid prea amănunțit
Publicat: 2022-03-11Am avut recent un interviu de programare, un ecran de telefon în care am folosit un editor de text colaborativ.
Mi s-a cerut să implementez un anumit API și am ales să fac acest lucru în Python. Abstragând declarația problemei, să presupunem că aveam nevoie de o clasă ale cărei instanțe au stocat unele data
și alte other_data
.
Am inspirat adânc și am început să scriu. După câteva rânduri, am avut așa ceva:
class Service(object): data = [] def __init__(self, other_data): self.other_data = other_data ...
Intervievatorul meu m-a oprit:
- Intervievator: „Acea linie:
data = []
. Nu cred că este valid Python?” - Eu: „Sunt destul de sigur că este. Este doar setarea unei valori implicite pentru atributul instanței.”
- Intervievator: „Când se execută acel cod?”
- Eu: „Nu sunt chiar sigur. O voi repara doar pentru a evita confuzia.”
Pentru referință și pentru a vă face o idee despre ce căutam, iată cum am modificat codul:
class Service(object): def __init__(self, other_data): self.data = [] self.other_data = other_data ...
După cum se dovedește, am greșit amândoi. Răspunsul real constă în înțelegerea distincției dintre atributele clasei Python și atributele instanței Python.
Notă: dacă aveți un expert în atributele clasei, puteți sări mai departe la cazurile de utilizare.
Atributele clasei Python
Intervievatorul meu a greșit prin faptul că codul de mai sus este valid din punct de vedere sintactic.
Și eu m-am înșelat prin faptul că nu setează o „valoare implicită” pentru atributul instanței. În schimb, definește data
ca atribut de clasă cu valoarea []
.
Din experiența mea, atributele clasei Python sunt un subiect despre care mulți oameni știu ceva , dar puțini îl înțeleg complet.
Variabila de clasă Python vs. Variabila de instanță: Care este diferența?
Un atribut de clasă Python este un atribut al clasei (circular, știu), mai degrabă decât un atribut al unei instanțe a unei clase.
Să folosim un exemplu de clasă Python pentru a ilustra diferența. Aici, class_var
este un atribut de clasă, iar i_var
este un atribut de instanță:
class MyClass(object): class_var = 1 def __init__(self, i_var): self.i_var = i_var
Rețineți că toate instanțele clasei au acces la class_var
și că poate fi accesată și ca o proprietate a clasei în sine :
foo = MyClass(2) bar = MyClass(3) foo.class_var, foo.i_var ## 1, 2 bar.class_var, bar.i_var ## 1, 3 MyClass.class_var ## <— This is key ## 1
Pentru programatorii Java sau C++, atributul de clasă este similar, dar nu identic, cu membrul static. Vom vedea cum diferă mai târziu.
Spații de nume clasă vs
Pentru a înțelege ce se întâmplă aici, să vorbim pe scurt despre spațiile de nume Python .
Un spațiu de nume este o mapare de la nume la obiecte, cu proprietatea că nu există nicio relație între nume din spații de nume diferite. Ele sunt de obicei implementate ca dicționare Python, deși acest lucru este abstractizat.
În funcție de context, poate fi necesar să accesați un spațiu de nume folosind sintaxa punct (de exemplu, object.name_from_objects_namespace
) sau ca o variabilă locală (de exemplu, object_from_namespace
). Ca exemplu concret:
class MyClass(object): ## No need for dot syntax class_var = 1 def __init__(self, i_var): self.i_var = i_var ## Need dot syntax as we've left scope of class namespace MyClass.class_var ## 1
Clasele Python și instanțele de clase au fiecare propriile spații de nume distincte reprezentate de atributele predefinite MyClass.__dict__
și, respectiv, instance_of_MyClass.__dict__
.
Când încercați să accesați un atribut dintr-o instanță a unei clase, mai întâi se uită la spațiul de nume al instanței sale. Dacă găsește atributul, returnează valoarea asociată. Dacă nu, atunci caută în spațiul de nume ale clasei și returnează atributul (dacă este prezent, aruncând o eroare în caz contrar). De exemplu:
foo = MyClass(2) ## Finds i_var in foo's instance namespace foo.i_var ## 2 ## Doesn't find class_var in instance namespace… ## So look's in class namespace (MyClass.__dict__) foo.class_var ## 1
Spațiul de nume de instanță preia supremația asupra spațiului de nume de clasă: dacă există un atribut cu același nume în ambele, spațiul de nume de instanță va fi verificat mai întâi și valoarea acestuia va fi returnată. Iată o versiune simplificată a codului (sursă) pentru căutarea atributelor:
def instlookup(inst, name): ## simplified algorithm... if inst.__dict__.has_key(name): return inst.__dict__[name] else: return inst.__class__.__dict__[name]
Și, sub formă vizuală:
Cum gestionează atributele clasei atribuirea
Având în vedere acest lucru, putem înțelege modul în care atributele clasei Python gestionează atribuirea:
Dacă un atribut de clasă este setat prin accesarea clasei, acesta va suprascrie valoarea pentru toate instanțele. De exemplu:
foo = MyClass(2) foo.class_var ## 1 MyClass.class_var = 2 foo.class_var ## 2
La nivelul spațiului de nume...
MyClass.__dict__['class_var'] = 2
. (Notă: acesta nu este codul exact (care ar fisetattr(MyClass, 'class_var', 2)
) deoarece__dict__
returnează un dictproxy, un wrapper imuabil care împiedică atribuirea directă, dar ajută de dragul demonstrației). Apoi, când accesămfoo.class_var
,class_var
are o nouă valoare în spațiul de nume ale clasei și astfel este returnat 2.Dacă o variabilă de clasă Paython este setată prin accesarea unei instanțe, aceasta va suprascrie valoarea numai pentru acea instanță . Aceasta anulează în esență variabila de clasă și o transformă într-o variabilă de instanță disponibilă, intuitiv, numai pentru acea instanță . De exemplu:
foo = MyClass(2) foo.class_var ## 1 foo.class_var = 2 foo.class_var ## 2 MyClass.class_var ## 1
La nivel de spațiu de nume... adăugăm atributul
class_var
lafoo.__dict__
, așa că atunci când căutămfoo.class_var
, returnăm 2. Între timp, alte instanțe aleMyClass
nu vor aveaclass_var
în spațiile de nume ale instanțelor, așa că ei continuă să găseascăclass_var
înMyClass.__dict__
și astfel returnați 1.
Mutabilitate
Întrebare test: Ce se întâmplă dacă atributul clasei dvs. are un tip mutabil ? Puteți manipula (mutilați?) atributul de clasă accesând-o printr-o anumită instanță și, la rândul său, ajungeți să manipulați obiectul referit pe care îl accesează toate instanțele (după cum a subliniat Timothy Wiseman).
Acest lucru este cel mai bine demonstrat prin exemplu. Să ne întoarcem la Service
pe care l-am definit mai devreme și să vedem cum utilizarea mea a unei variabile de clasă ar fi putut duce la probleme pe viitor.
class Service(object): data = [] def __init__(self, other_data): self.other_data = other_data ...
Scopul meu a fost să am lista goală ( []
) ca valoare implicită pentru data
și pentru fiecare instanță de Service
să aibă propriile sale date care să fie modificate în timp, în funcție de instanță. Dar în acest caz, obținem următorul comportament (amintim că Service
ia un argument other_data
, care este arbitrar în acest exemplu):
s1 = Service(['a', 'b']) s2 = Service(['c', 'd']) s1.data.append(1) s1.data ## [1] s2.data ## [1] s2.data.append(2) s1.data ## [1, 2] s2.data ## [1, 2]
Acest lucru nu este bun - modificarea variabilei de clasă printr-o singură instanță o modifică pentru toate celelalte!
La nivel de spațiu de nume... toate instanțele Service
accesează și modifică aceeași listă în Service.__dict__
fără a-și crea propriile atribute de data
în spațiile de nume ale instanțelor.
Am putea ocoli acest lucru folosind misiunea; adică, în loc să exploatăm mutabilitatea listei, am putea atribui obiectelor noastre Service
să aibă propriile liste, după cum urmează:
s1 = Service(['a', 'b']) s2 = Service(['c', 'd']) s1.data = [1] s2.data = [2] s1.data ## [1] s2.data ## [2]
În acest caz, adăugăm s1.__dict__['data'] = [1]
, astfel încât Service.__dict__['data']
rămâne neschimbat.
Din păcate, acest lucru necesită ca utilizatorii Service
să aibă cunoștințe intime despre variabilele sale și, cu siguranță, sunt predispuși la greșeli. Într-un fel, ne-am aborda mai degrabă simptomele decât cauza. Am prefera ceva care să fie corect prin construcție.
Soluția mea personală: dacă utilizați doar o variabilă de clasă pentru a atribui o valoare implicită unei eventuale variabile de instanță Python, nu utilizați valori modificabile . În acest caz, fiecare instanță de Service
urma să înlocuiască Service.data
cu propriul atribut de instanță în cele din urmă, așa că utilizarea unei liste goale ca implicită a dus la o eroare mică care a fost ușor trecută cu vederea. În loc de cele de mai sus, am fi putut:
- Respectați în întregime atributele de instanță, așa cum sa demonstrat în introducere.
S-a evitat utilizarea listei goale (o valoare modificabilă) ca „implicit”:
class Service(object): data = None def __init__(self, other_data): self.other_data = other_data ...
Desigur, ar trebui să ne ocupăm de cazul
None
în mod corespunzător, dar acesta este un preț mic de plătit.
Deci, când ar trebui să utilizați atributele clasei Python?
Atributele clasei sunt complicate, dar să ne uităm la câteva cazuri în care ar fi utile:
Stocarea constantelor . Deoarece atributele clasei pot fi accesate ca atribute ale clasei în sine, este adesea plăcut să le folosiți pentru a stoca constante la nivelul întregii clase, specifice clasei. De exemplu:
class Circle(object): pi = 3.14159 def __init__(self, radius): self.radius = radius def area(self): return Circle.pi * self.radius * self.radius Circle.pi ## 3.14159 c = Circle(10) c.pi ## 3.14159 c.area() ## 314.159
Definirea valorilor implicite . Ca exemplu banal, am putea crea o listă delimitată (adică, o listă care poate conține doar un anumit număr de elemente sau mai puțin) și să alegem să avem o limită implicită de 10 elemente:
class MyClass(object): limit = 10 def __init__(self): self.data = [] def item(self, i): return self.data[i] def add(self, e): if len(self.data) >= self.limit: raise Exception("Too many elements") self.data.append(e) MyClass.limit ## 10
Apoi, am putea crea instanțe cu propriile limite specifice, de asemenea, prin atribuirea atributului
limit
al instanței.foo = MyClass() foo.limit = 50 ## foo can now hold 50 elements—other instances can hold 10
Acest lucru are sens doar dacă doriți ca instanța dvs. tipică a
MyClass
să conțină doar 10 elemente sau mai puține - dacă le dați tuturor instanțelor limite diferite, atuncilimit
ar trebui să fie o variabilă de instanță. (Nu uitați, totuși: aveți grijă când utilizați valori modificabile ca valori implicite.)Urmărirea tuturor datelor în toate instanțele unei clase date . Acest lucru este oarecum specific, dar am putut vedea un scenariu în care ați putea dori să accesați o bucată de date referitoare la fiecare instanță existentă a unei clase date.
Pentru a face scenariul mai concret, să presupunem că avem o clasă
Person
și fiecare persoană are unname
. Vrem să ținem evidența tuturor denumirilor care au fost folosite. O abordare ar putea fi să iterați lista de obiecte a colectorului de gunoi, dar este mai simplu să utilizați variabilele de clasă.Rețineți că, în acest caz,
names
vor fi accesate doar ca o variabilă de clasă, astfel încât valoarea implicită mutabilă este acceptabilă.class Person(object): all_names = [] def __init__(self, name): self.name = name Person.all_names.append(name) joe = Person('Joe') bob = Person('Bob') print Person.all_names ## ['Joe', 'Bob']
Am putea chiar să folosim acest model de design pentru a urmări toate instanțele existente ale unei clase date, mai degrabă decât doar câteva date asociate.
class Person(object): all_people = [] def __init__(self, name): self.name = name Person.all_people.append(self) joe = Person('Joe') bob = Person('Bob') print Person.all_people ## [<__main__.Person object at 0x10e428c50>, <__main__.Person object at 0x10e428c90>]
Performanță (un fel de... vezi mai jos).
Sub capotă
Notă: dacă vă faceți griji cu privire la performanța la acest nivel, s-ar putea să nu doriți să utilizați Python în primul rând, deoarece diferențele vor fi de ordinul zecimii de milisecundă - dar este totuși distractiv să vă uitați puțin, și ajută de dragul ilustrației.
Amintiți-vă că spațiul de nume al unei clase este creat și completat în momentul definirii clasei. Aceasta înseamnă că facem o singură atribuire — întotdeauna — pentru o anumită variabilă de clasă, în timp ce variabilele de instanță trebuie alocate de fiecare dată când este creată o nouă instanță. Să luăm un exemplu.
def called_class(): print "Class assignment" return 2 class Bar(object): y = called_class() def __init__(self, x): self.x = x ## "Class assignment" def called_instance(): print "Instance assignment" return 2 class Foo(object): def __init__(self, x): self.y = called_instance() self.x = x Bar(1) Bar(2) Foo(1) ## "Instance assignment" Foo(2) ## "Instance assignment"
Atribuim lui Bar.y
o singură dată, dar instance_of_Foo.y
la fiecare apel către __init__
.
Ca dovadă suplimentară, să folosim dezasamblatorul Python:
import dis class Bar(object): y = 2 def __init__(self, x): self.x = x class Foo(object): def __init__(self, x): self.y = 2 self.x = x dis.dis(Bar) ## Disassembly of __init__: ## 7 0 LOAD_FAST 1 (x) ## 3 LOAD_FAST 0 (self) ## 6 STORE_ATTR 0 (x) ## 9 LOAD_CONST 0 (None) ## 12 RETURN_VALUE dis.dis(Foo) ## Disassembly of __init__: ## 11 0 LOAD_CONST 1 (2) ## 3 LOAD_FAST 0 (self) ## 6 STORE_ATTR 0 (y) ## 12 9 LOAD_FAST 1 (x) ## 12 LOAD_FAST 0 (self) ## 15 STORE_ATTR 1 (x) ## 18 LOAD_CONST 0 (None) ## 21 RETURN_VALUE
Când ne uităm la codul octet, este din nou evident că Foo.__init__
trebuie să facă două sarcini, în timp ce Bar.__init__
face doar una.
În practică, cum arată cu adevărat acest câștig? Voi fi primul care admite că testele de sincronizare depind foarte mult de factori adesea necontrolați și diferențele dintre ei sunt adesea greu de explicat cu acuratețe.
Cu toate acestea, cred că aceste mici fragmente (rulate cu modulul Python timeit) ajută la ilustrarea diferențelor dintre variabilele de clasă și de instanță, așa că le-am inclus oricum.
Notă: sunt pe un MacBook Pro cu OS X 10.8.5 și Python 2.7.2.
Inițializare
10000000 calls to `Bar(2)`: 4.940s 10000000 calls to `Foo(2)`: 6.043s
Inițializările lui Bar
sunt mai rapide cu peste o secundă, așa că diferența de aici pare să fie semnificativă statistic.
Deci de ce este acesta cazul? O explicație speculativă : facem două sarcini în Foo.__init__
, dar doar una în Bar.__init__
.
Misiune
10000000 calls to `Bar(2).y = 15`: 6.232s 10000000 calls to `Foo(2).y = 15`: 6.855s 10000000 `Bar` assignments: 6.232s - 4.940s = 1.292s 10000000 `Foo` assignments: 6.855s - 6.043s = 0.812s
Notă: nu există nicio modalitate de a rula din nou codul de configurare la fiecare încercare cu timeit, așa că trebuie să reinițializăm variabila noastră în perioada de încercare. Al doilea rând de timpi reprezintă timpii de mai sus cu timpii de inițializare calculați anterior deduși.
Din cele de mai sus, se pare că lui Foo
îi ia aproximativ 60% atâta timp cât Bar
pentru a gestiona sarcinile.
De ce este acesta cazul? O explicație speculativă : când atribuim lui Bar(2).y
, căutăm mai întâi în spațiul de nume al instanței ( Bar(2).__dict__[y]
), nu reușim să găsim y
, apoi căutăm în spațiul de nume ale clasei ( Bar.__dict__[y]
), apoi efectuând atribuirea corespunzătoare. Când atribuim lui Foo(2).y
, facem jumătate din câte căutări, decât atribuim imediat spațiului de nume al instanței ( Foo(2).__dict__[y]
).
Pe scurt, deși aceste câștiguri de performanță nu vor conta în realitate, aceste teste sunt interesante la nivel conceptual. În orice caz, sper că aceste diferențe ajută la ilustrarea distincțiilor mecanice dintre variabilele de clasă și de instanță.
În concluzie
Atributele clasei par a fi subutilizate în Python; o mulțime de programatori au impresii diferite despre modul în care funcționează și de ce ar putea fi de ajutor.
Aprecierea mea: variabilele clasei Python își au locul lor în școala de cod bun. Când sunt folosite cu grijă, pot simplifica lucrurile și pot îmbunătăți lizibilitatea. Dar atunci când sunt aruncați neglijent într-o anumită clasă, cu siguranță te vor împiedica.
Anexă : Variabile de instanță privată
Un lucru am vrut să includ, dar nu am avut un punct de intrare natural...
Python nu are variabile private , ca să spunem așa, dar o altă relație interesantă între denumirea clasei și a instanțelor vine cu denaturarea numelui.
În ghidul de stil Python, se spune că variabilele pseudo-private ar trebui să fie prefixate cu o liniuță dublă: „__”. Acesta nu este doar un semn pentru alții că variabila dvs. este menită să fie tratată în mod privat, ci și o modalitate de a preveni accesul la ea, într-un fel. Iată ce vreau să spun:
class Bar(object): def __init__(self): self.__zap = 1 a = Bar() a.__zap ## Traceback (most recent call last): ## File "<stdin>", line 1, in <module> ## AttributeError: 'Bar' object has no attribute '__baz' ## Hmm. So what's in the namespace? a.__dict__ {'_Bar__zap': 1} a._Bar__zap ## 1
Uită-te la asta: atributul instanței __zap
este prefixat automat cu numele clasei pentru a produce _Bar__zap
.
Deși încă se poate seta și se poate obține folosind a._Bar__zap
, această modificare a numelui este un mijloc de a crea o variabilă „privată”, deoarece vă împiedică pe dvs. și pe alții să o accesați accidental sau din ignoranță.
Editare: după cum a subliniat cu amabilitate Pedro Werneck, acest comportament este în mare măsură menit să ajute cu subclasarea. În ghidul de stil PEP 8, ei îl văd ca având două scopuri: (1) împiedicarea accesului subclaselor la anumite atribute și (2) prevenirea ciocnirilor spațiilor de nume în aceste subclase. Deși utilă, manipularea variabilelor nu ar trebui văzută ca o invitație de a scrie cod cu o distincție public-privat asumată, cum este prezentă în Java.