Python
Cum funcționează MRO-ul din Python și ce este linearizarea C3
Pe scurt
MRO-ul este o singură listă plată și ordonată de clase, în care caută Python când rezolvă un atribut, calculată o dată la crearea clasei și stocată în __mro__. Linearizarea C3 este algoritmul care construiește acea listă și garantează trei lucruri: fiecare clasă apare înaintea propriilor ei baze, bazele își păstrează ordinea de la stânga la dreapta în care le-a declarat clasa, iar rezultatul rămâne consistent cu MRO-ul fiecărui părinte. O clasă care moștenește o bază comună pe două căi primește acea bază exact o dată, poziționată după amândouă clasele care duc la ea. Când nicio ordonare nu poate satisface toate cele trei garanții, instrucțiunea class ridică imediat TypeError, în loc să aleagă tăcut una.
Cum funcționează
Căutarea unui atribut pe o instanță parcurge type(obj).__mro__ de la început la sfârșit și se oprește la prima clasă al cărei spațiu de nume conține numele. Aceasta este toată semnificația lui „moștenit" la rulare — nu există nicio căutare în arborele de moștenire per căutare, ci doar o parcurgere a unei liste precalculate. Lista este construită o singură dată, în timp ce se execută instrucțiunea class, deci costul unei ierarhii complicate este plătit la momentul importului, nu la fiecare apel.
C3 construiește lista prin îmbinare. Linearizarea unei clase este clasa însăși, urmată de o îmbinare a linearizărilor părinților ei, plus lista părinților în ordinea declarării. Îmbinarea ia în mod repetat prima clasă care apare în capul vreunei liste și nicăieri în coada altei liste — coada fiind tot ce urmează după capul propriu al unei liste. Acea condiție „nu se află în nicio coadă" este ce împiedică plasarea unei clase înaintea a ceva ce trebuie să o preceadă și este tot mecanismul, într-o singură propoziție.
Cele trei proprietăți decurg din acea regulă. O clasă își precedă bazele pentru că este pusă în față înainte să înceapă îmbinarea. Ordinea de declarare supraviețuiește pentru că lista părinților este una dintre listele îmbinate. Iar monotonia — proprietatea care îi dă lui C3 reputația — înseamnă că ordinea a oricare două clase din MRO-ul unei subclase nu contrazice niciodată ordinea lor din MRO-ul unui părinte, deci adăugarea unei subclase nu poate niciodată să reamestece comportamentul moștenit.
Rombul este cazul care motivează totul. Cu două mixinuri peste o singură bază comună, o parcurgere naivă în adâncime ar vizita baza comună imediat după primul mixin și l-ar rata complet pe al doilea. C3 plasează baza comună după amândouă mixinurile, motiv pentru care apare exact o dată și ultima dintre clasele care duc la ea.
Acea ordonare este și cea pe care o urmează super(). super() dintr-o metodă nu înseamnă „părintele meu" — înseamnă „clasa următoare după a mea, din MRO-ul obiectului asupra căruia se operează". De vreme ce tipul obiectului poate fi o subclasă despre care clasa metodei nu știe nimic, același apel super() poate dispeceriza către o soră care nu se află deloc printre bazele acelei clase.
Cum arată la rulare
Creează un fișier:
touch report_hierarchy.py
Rulează-l cu:
python3 report_hierarchy.py
Rombul și unde aterizează baza comună
Cel mai mic caz care arată C3 făcând ceva ce o parcurgere în adâncime nu ar face: două mixinuri peste o singură bază, combinate.
class ReportJob:
def render(self):
return "ReportJob.render"
class Timestamped(ReportJob):
def render(self):
return "Timestamped.render"
class Audited(ReportJob):
def render(self):
return "Audited.render"
class NightlyReport(Timestamped, Audited):
pass
order = [cls.__name__ for cls in NightlyReport.__mro__]
print("MRO:", order)
print("render() resolves to:", NightlyReport().render())
print("ReportJob appears exactly once:", order.count("ReportJob") == 1)
print("both mixins precede the shared base:", order.index("Audited") < order.index("ReportJob"))
MRO: ['NightlyReport', 'Timestamped', 'Audited', 'ReportJob', 'object']
render() resolves to: Timestamped.render
ReportJob appears exactly once: True
both mixins precede the shared base: True
- Lista este plată, iar fiecare clasă apare o dată, deși la
ReportJobse poate ajunge pe două rute. Timestampedvine înaintea luiAuditedpentru că aceasta este ordinea de declarare din instrucțiuneaclass— precedența locală este păstrată.ReportJobstă după amândouă mixinurile. O parcurgere în adâncime ar fi mersNightlyReport, Timestamped, ReportJobși ar fi ajuns la bază înainte să îl ia măcar în considerare peAudited, deci o metodă definită doar peAuditedar fi fost umbrită de baza din care moștenește.render()se rezolvă la prima potrivire din acea listă, motiv pentru careTimestamped.rendercâștigă, fără nicio ambiguitate de rezolvat la momentul apelului.
super() urmează MRO-ul, nu părintele
O dimensiune în plus: metode cooperative. Fiecare clasă apelează super(), iar lanțul rezultat nu este cel pe care l-ar sugera citirea vreunei clase individuale. Înlocuiește fișierul cu:
class ReportJob:
def render(self):
return "ReportJob"
class Timestamped(ReportJob):
def render(self):
return "Timestamped -> " + super().render()
class Audited(ReportJob):
def render(self):
return "Audited -> " + super().render()
class NightlyReport(Timestamped, Audited):
pass
print("MRO:", [cls.__name__ for cls in NightlyReport.__mro__])
print("full chain:", NightlyReport().render())
print("Timestamped alone:", Timestamped().render())
print("Audited is not a base of Timestamped:", Audited not in Timestamped.__mro__)
MRO: ['NightlyReport', 'Timestamped', 'Audited', 'ReportJob', 'object']
full chain: Timestamped -> Audited -> ReportJob
Timestamped alone: Timestamped -> ReportJob
Audited is not a base of Timestamped: True
super().render() din interiorul lui Timestamped a ajuns la Audited, o clasă care nu este una dintre bazele lui și nu apare în propriul lui MRO — ultima linie o confirmă. Nimic legat de Timestamped nu s-a schimbat; apelul s-a rezolvat pur și simplu față de MRO-ul instanței, care, pentru un NightlyReport, îl pune pe Audited următorul. Perechea din mijloc este dovada: cod identic din Timestamped produce un lanț cu doi pași pe un Timestamped simplu și un lanț cu trei pași pe un NightlyReport. Asta face mixinurile compozabile și este totodată motivul pentru care o clasă care își omite apelul super() trunchiază tăcut lanțul pentru fiecare subclasă care o include.
Când renunță C3
Ultima treaptă este modul de eșec. Două clase care ordonează diferit aceeași pereche de baze nu pot fi combinate, pentru că nicio listă unică nu le satisface pe amândouă. Înlocuiește fișierul cu:
class Extractor:
pass
class Loader:
pass
class ExtractFirst(Extractor, Loader):
pass
class LoadFirst(Loader, Extractor):
pass
print("ExtractFirst MRO:", [c.__name__ for c in ExtractFirst.__mro__])
print("LoadFirst MRO: ", [c.__name__ for c in LoadFirst.__mro__])
try:
class Pipeline(ExtractFirst, LoadFirst):
pass
except TypeError as exc:
print("combining them raises:", type(exc).__name__)
print("reason:", str(exc).split(" for bases")[0])
ExtractFirst MRO: ['ExtractFirst', 'Extractor', 'Loader', 'object']
LoadFirst MRO: ['LoadFirst', 'Loader', 'Extractor', 'object']
combining them raises: TypeError
reason: Cannot create a consistent method resolution order (MRO)
Fiecare părinte este perfect valid de unul singur, iar ei nu sunt de acord: unul cere Extractor înaintea lui Loader, celălalt cere inversul. Monotonia spune că o subclasă nu are voie să contrazică niciunul dintre părinți, deci îmbinarea ajunge într-un punct în care fiecare candidat rămas apare în coada altei liste și se oprește. Eroarea sosește la instrucțiunea class, nu la vreo căutare de atribut ulterioară, ceea ce este partea utilă — o ierarhie inconsistentă este respinsă la momentul definirii, în loc să se rezolve arbitrar și să producă o eroare mult mai târziu.
Cazuri limită și capcane
super()fără argumente are nevoie de un lanț real ca să fie util. Fiecare clasă dintr-o ierarhie cooperativă trebuie să îl apeleze, iar una care nu o face încheie tăcut lanțul pentru fiecare subclasă construită pe ea. Aceasta este cea mai frecventă cauză a unui mixin care „nu face nimic" în unele combinații și funcționează în altele.- Un
__init__cooperativ are nevoie de semnături compatibile. De vreme ce o clasă nu poate ști ce soră urmează, metodele dintr-un lanț cooperativ acceptă și redirecționează prin convenție**kwargs, iar lanțul trebuie să se oprească înainte să transmită argumente suplimentare cătreobject.__init__, care nu acceptă niciunul. - MRO-ul este fixat la crearea clasei. Este calculat în timp ce rulează instrucțiunea
class, deci__mro__este un instantaneu; reatribuirea lui__bases__după aceea nu este un mod suportat de a remodela ordinea de căutare. - O clasă nu poate lista aceeași bază de două ori.
class Report(Audited, Audited)ridicăTypeError: duplicate base class, de vreme ce ordinea de declarare pe care o implică nu are niciun sens. objecteste întotdeauna ultimul. Fiecare MRO se termină acolo, ceea ce face lanțurilesuper()să se termine și este ce ar trebui să se aștepte să predea o metodă cooperativă.
Rezumat
MRO-ul este o listă plată precalculată, iar C3 este îmbinarea care o produce sub trei constrângeri: o clasă înaintea bazelor ei, ordinea declarată păstrată și consistență cu MRO-ul fiecărui părinte. Moștenirea în romb primește o singură copie a bazei comune, plasată după fiecare clasă care duce la ea, iar super() parcurge acea listă, în loc să urmeze o legătură statică către părinte — ceea ce face mixinurile compozabile și ce le strică atunci când lipsește un apel super().
- Căutarea atributelor parcurge
__mro__de la început la sfârșit și ia prima potrivire; lista este construită o dată, la crearea clasei. - C3 îmbină linearizările părinților, luând doar o clasă care nu apare în coada niciunei alte liste.
- O clasă își precedă întotdeauna bazele, iar bazele își păstrează ordinea de la stânga la dreapta declarată de instrucțiunea
class. - O bază comună apare exact o dată, după fiecare clasă care moștenește din ea, ceea ce ordonarea în adâncime greșește.
super()se rezolvă față de MRO-ul tipului instanței, deci poate dispeceriza către o clasă soră care nu se află printre bazele apelantului.- O ordonare care ar contrazice MRO-ul unui părinte ridică
TypeErrorla instrucțiuneaclass, în loc să se rezolve arbitrar.