Python din interior

Python din interior
1. Introducere.
2. Obiecte. Începutul.
3. Obiecte. Coada.

În partea anterioară, am început să studiem sistemul de obiecte Python: am înțeles exact ce poate fi considerat un obiect și cum obiectele își îndeplinesc activitatea. Să continuăm să analizăm această întrebare.







Vă urez bun venit în a treia parte a seriei noastre de articole despre insulele lui Python (recomandăm insistent citirea celei de-a doua părți, dacă nu ați făcut-o deja, altfel nu veți înțelege nimic). În acest episod, vom vorbi despre un concept important pe care încă nu îl putem da seama despre atribute. Dacă ați scris vreodată ceva despre Python, atunci le-ați folosit. Atributele obiectului sunt alte obiecte asociate cu acesta, accesibile prin operator. (punct), de exemplu: >>> my_object.attribute_name. Descrieți pe scurt comportamentul Python atunci când accesați atributele. Acest comportament depinde de tipul de obiect disponibil prin atribut (deja realizat că acest lucru se aplică tuturor operațiunilor asociate cu obiectele?).

Într-un tip, puteți descrie metode speciale care modifică accesul la atributele instanțelor sale. Aceste metode sunt descrise aici (după cum deja știm, vor fi asociate cu sloturile necesare, cum ar fi funcția fixup_slot_dispatchers, unde este creat tipul ... ați citit postul anterior, nu?). Aceste metode pot face orice, orice; Dețineți de tip C, în care descrie sau Python, puteți scrie astfel de metode care păstrează și returnează atributele o locație incredibil, dacă doriți, puteți trimite și primi atribute la radio la ISS sau chiar să le stoca într-un relațională bază de date. Dar într-o mai mult sau mai puțin condiții normale, aceste metode de a scrie pur și simplu atribut sub formă de perechi cheie-valoare (valoare atribut / atribut al numelui) în orice obiect dicționar atunci când atributul este setat, și returnează un atribut din dicționar atunci când se solicită (sau o excepție este aruncată AttributeError: dacă dicționarul nu are o cheie corespunzătoare numelui atributului solicitat). Totul este atât de simplu și frumos, vă mulțumim pentru atenție, probabil că vom termina acest lucru.

Să stea. Prietenii mei, fecalele tocmai au început abordarea rapidă a unui generator eolian rotativ. Dispăreați, astfel încât toți să dispară. Îmi propun să studiez împreună ce se întâmplă în interpret și să ceară, cum facem de obicei, câteva întrebări enervante.

Citiți cu atenție codul sau mergeți direct la descrierea textului:

Să traducem acest lucru în limbajul uman: obiect (acesta este cel mai simplu tip construit, dacă uitați), după cum vedem, există un dicționar și tot ceea ce putem accesa prin atribute este identic cu ceea ce vedem în obiectul .__ dict__. Ar trebui să fim surprinși de faptul că instanțele de tip obiect (de exemplu, obiectul o) nu acceptă definiția atributelor suplimentare și nu au __dict__ deloc. dar suportă accesul la atributele existente (încercați o .__ class__.o .__ hash__ etc, aceste comenzi returnează ceva). După aceea, am creat o nouă clasă C. am moștenit-o din obiect. a adăugat atributul A și a văzut că este accesibil prin C.A și C.__ dict __ ['A']. cum era de așteptat. Apoi am creat o instanță de o2 din clasa C și am văzut că definiția atributului se modifică __dict__. și invers, schimbarea __dict__ afectează atributele. După aceea, am fost surprinși să aflăm că clasa __dict__ este doar pentru citire, chiar dacă definiția atributului (C.A2) funcționează bine. În cele din urmă, am văzut că obiectele __dict__ ale instanței și ale clasei au diferite tipuri - dictul familiar și dict_proxy misterios, respectiv. Și dacă toate acestea nu sunt suficiente, amintiți-vă puzzle-ul din partea anterioară: dacă moștenitorii unui obiect curat (de exemplu, o) nu au __dict__. iar C extinde obiectul. fără a adăuga nimic semnificativ, de unde brusc la instanțele clasei C (o2) apare __dict__?

În acest exemplu, ultima linie demonstrează modul de accesare a atributului type. În acest caz, pentru a găsi bara de atribute. va fi apelată funcția de acces la atributele clasei Foo (evidențiată prin tp_getattro). Aproximativ același lucru se întâmplă atunci când se definesc și se șterg atributele (pentru interpret, apropo, "ștergerea" este doar setarea valorii la NULL). Sper că până acum totul a fost clar, iar între timp am discutat despre recursul la atribute.







Înainte de a lua în considerare accesul la atributele instanțelor, permiteți-mi să spun despre un concept puțin cunoscut (dar foarte important!): Un descriptor. Descriptorii joacă un rol special în accesarea atributelor instanțelor și trebuie să clarific ce este. Un obiect este considerat un descriptor dacă unul sau două sloturi de tipul acestuia (tp_descr_get și / sau tp_descr_set) sunt umplute cu valori nonzero. Aceste sloturi sunt asociate cu metode speciale __get__. __set__ și __delete__ (de exemplu, dacă definiți o clasă cu o metodă __get__., care va contacta tp_descr_get slotul. și de a crea un obiect al acestei clase, obiectul este un descriptor). În final, obiectul este considerat un descriptor de date. dacă slotul tp_descr_set este umplut cu o valoare diferită de zero. După cum vom vedea, descriptorii joacă un rol important în accesarea atributelor și voi mai da câteva explicații și trimiteri la documentația necesară.

Deci, am dat seama ce descriptori sunt și au înțeles cum apare accesul la atributele de tip. Dar cele mai multe obiecte nu sunt tipuri; tipul lor nu este tip. dar ceva mai prozaic, de exemplu, int. dict sau o clasă personalizată. Toți se bazează pe funcțiile de accesare a atributelor universale, care sunt definite fie în tip, fie moștenite de la părintele de tip atunci când a fost creat (acest subiect, moștenirea sloturilor, discutam în "Head"). Algoritmul funcției de acces atribut universal (PyObject_GenericGetAttr) arată astfel:

  1. Căutați în tipul de dicționar al instanței și în dicționarele tuturor părinților de tip. Dacă este descris un descriptor de date. apela funcția tp_descr_get și returnează rezultatul. Dacă găsiți altceva, amintiți-vă acest lucru doar în cazul (de exemplu, sub numele X).
  2. Căutați în dicționarul de obiecte și returnați rezultatul dacă acesta este găsit.
  3. Dacă nu sa găsit nimic în dicționarul de obiecte, bifați X. dacă a fost instalat; dacă X este un descriptor, apelați funcția tp_descr_get și returnați rezultatul. Dacă X este un obiect obișnuit, întoarce-l.
  4. În cele din urmă, dacă nu s-a găsit nimic, aruncați excepția AttributeError.

Am observat că tocmai am câștigat o înțelegere deplină a moștenirii orientate pe obiecte în Python: căutarea atributelor începe cu tipul obiectului, iar apoi în toți părinții, înțelegem că accesând atributul A al obiectului O din clasa C1. care este moștenit de la C2. care la rândul său este moștenit de la C3. poate întoarce A și de la O. și C1. și C2 și C3. care este determinată de o anumită ordine de rezolvare a metodelor, care este bine descrisă aici. Acest mod de rezolvare a atributelor, împreună cu moștenirea sloturilor, este suficient pentru a explica majoritatea funcțiilor de moștenire din Python (deși diavolul, ca de obicei, este acoperit în detaliu).

Am creat o clasă nouă, un obiect și am definit atributul (o.foo = 'bar'), introdus gdb. a dereferențiat tipul obiectului (C) și a găsit-o tp_dictoffset (16) și apoi a verificat ce se află pe această compensare în structura C a obiectului. În mod surprinzător, am găsit acolo un dicționar de obiecte cu o cheie de tip foo. indicând valoarea barei. Firește, dacă verificați tipul tp_dictoffset, care nu are __dict__. de exemplu, obiect. atunci vom găsi zero acolo. Puștii de gâscă, da?

Faptul că dicționarele tipurilor și dicționarelor instanțelor sunt similare, dar implementarea lor este destul de diferită, poate fi jenată. Există încă câteva ghicitori. Să rezumăm și să determinăm ce am lipsit: definiți o clasă goală C moștenită de la obiect. crea un obiect o din această clasă, alocă o memorie adițională pentru pointerul dicționarului la offsetul tp_dictoffset (spațiul este selectat încă de la început, dar dicționarul este alocat numai la prima conversie (aici)). Apoi, executați în interpret o .__ dict__. Bytecode este compilat cu comanda LOAD_ATTR. care apelează funcția PyObject_GetAttr. care dereferențează tipul obiectului o și găsește slotul tp_getattro. care execută procesul de căutare atribut standard descris mai sus și implementat în PyObject_GenericGetAttr. În cele din urmă, după ce se întâmplă toate acestea, ce se întoarce dicționarul obiectului nostru? Știm unde este stocat dicționarul, dar puteți vedea că __dict__ nu îl are, deci apare problema puiului și a ouălor: ceea ce ne aduce înapoi în dicționar când ne întoarcem la __dict__. dacă nu există nici unul în dicționarul propriu-zis?

Ceva care are prioritate față de dicționarul de obiecte este un descriptor. A se vedea:

Aici este! Vidto că există ceva numit getset_descriptor (fișier ./Objects/typeobject.c), un anumit grup de funcții, realizând protocolul descriptor, și că ar trebui să fie tipul de obiect __dict__. Acest descriptor va intercepta toate încercările de accesare a obiectelor .__ dict__ de acest tip și va returna orice vrea, în cazul nostru va fi un pointer la dicționar prin compensarea tp_dictoffset în o. De asemenea, acest lucru explică de ce am văzut dict_proxy puțin mai devreme. Dacă în tp_dict există un pointer într-un dicționar simplu, de ce îl vedem înfășurat într-un obiect în care este imposibil să scrie ceva? Aceasta face ca descriptorul __dict__ al tipului nostru de tip, tip.

Să terminăm acest exemplu:

Acest descriptor este o funcție care împachetează un dicționar cu un obiect simplu care simulează comportamentul unui dicționar convențional, cu excepția faptului că este doar pentru citire. De ce este atât de important să împiedicați utilizatorul să interfereze cu tipul __dict__? Deoarece spațiul de nume al unui tip poate conține metode speciale, de exemplu __sub__. Atunci când creăm un tip cu metode speciale sau când îi definim cu un tip prin atribute, funcția update_one_slot este executată. care va conecta aceste metode la sloturi de tipul, de exemplu, așa cum sa întâmplat cu operația de scădere în postul anterior. Dacă am putea adăuga aceste metode direct în __dict__ de tip, acestea nu ar fi luat legătura cu sloturile, și le-am primit, am tip, care este similar cu ceea ce ai nevoie (de exemplu, a __sub__ în dicționar), dar care se comportă altfel .

Știți ce? Nu doar fete. Ne plac, de asemenea, tipii ăștia. Vino la noi. Împreună mai distractiv.







Articole similare

Trimiteți-le prietenilor: