Python
Ce sunt metaclasele în Python și care este un caz real de utilizare pentru una
Răspuns scurt
O metaclasă este clasa unei clase. Pentru că însele clasele sunt obiecte, ceva trebuie să le construiască, iar implicit acel ceva este type — o instrucțiune class își adună corpul într-un spațiu de nume și apelează type(name, bases, namespace) ca să producă obiectul clasă. O metaclasă este orice obiect apelabil pus în locul lui type la acel pas, deci codul ei rulează o dată per definiție de clasă, nu per instanță. Asta o face instrumentul potrivit pentru muncă ce se întâmplă la momentul definirii: înregistrarea automată a fiecărei subclase într-un registru de pluginuri sau respingerea unei clase care nu declară ce cere un framework, astfel încât eroarea să sosească la import, nu la prima folosire.
Cum funcționează
Pornește de la faptul din care decurge tot restul: o clasă este un obiect, iar fiecare obiect are un tip. type(gauge) este Gauge, iar type(Gauge) este type. Același type care raportează clasa unui obiect este și constructorul care construiește clase, iar apelarea lui cu trei argumente — un nume, un tuplu de baze și un dicționar de spațiu de nume — produce o clasă identică cu una pe care ar fi făcut-o o instrucțiune class.
Instrucțiunea class este în mare parte zahăr sintactic pentru acel apel. Python determină ce metaclasă se aplică, îi cere o mapare de spațiu de nume prin __prepare__, execută corpul clasei în interiorul acelui spațiu de nume și, în final, apelează metaclasa cu numele, bazele și spațiul de nume populat. Rezultatul este legat de numele clasei. Orice cuvânt-cheie dat în antetul clasei, ca în class Report(Base, tag="v2"), este transmis la fel mai departe metaclasei.
O metaclasă interceptează acel apel prin derivarea din type și suprascrierea lui __new__, __init__ sau a amândurora. __new__ rulează înainte să existe obiectul clasă și primește spațiul de nume brut, deci poate inspecta sau rescrie ce a definit corpul. __init__ rulează după ce clasa a fost construită și primește clasa terminată, ceea ce este locul natural pentru a o înregistra undeva. Niciuna nu are vreo legătură cu instanțele — o instanță este creată mai târziu, prin apelarea clasei, ceea ce este cu totul alt mecanism.
Proprietatea care face registrele să funcționeze este moștenirea metaclasei înseși. O clasă creată de o metaclasă transmite automat acea metaclasă subclaselor ei, deci o clasă de bază declarată cu metaclass=ExporterMeta face ca fiecare descendent să ruleze același cod de la momentul definirii, fără să opteze pentru asta. Exact asta vrei de la un sistem de pluginuri și exact asta nu poate face un decorator, de vreme ce un decorator trebuie aplicat manual fiecărei clase.
Aici își are locul o precizare onestă. Cea mai mare parte din ce cerea odinioară metaclase este acum acoperită de două cârlige mai simple: __init_subclass__, o metodă de pe părinte pe care Python o apelează ori de câte ori este creată o subclasă, și __set_name__, care îi spune unui descriptor numele lui de atribut. Amândouă își fac treaba la momentul definirii, fără să introducă un al doilea tip în ierarhie. Recurge la o metaclasă când ai nevoie să controlezi spațiul de nume înainte să ruleze corpul sau să schimbi însuși obiectul clasă în feluri pe care acele cârlige nu le pot face.
Cum arată la rulare
Creează un fișier:
touch export_registry.py
Rulează-l cu:
python3 export_registry.py
O clasă este un obiect construit de type
Cel mai mic caz: arăți că clasa unei clase este type, apoi construiești o clasă fără nicio instrucțiune class.
class Gauge:
unit = "psi"
def read(self):
return 31.5
def read(self):
return 31.5
DynamicGauge = type("DynamicGauge", (), {"unit": "psi", "read": read})
print("type of an instance:", type(Gauge()).__name__)
print("type of the class: ", type(Gauge).__name__)
print("type of type itself:", type(type).__name__)
print("built by calling type():", DynamicGauge.__name__, DynamicGauge.unit, DynamicGauge().read())
print("both are the same kind of object:", type(DynamicGauge) is type(Gauge))
type of an instance: Gauge
type of the class: type
type of type itself: type
built by calling type(): DynamicGauge psi 31.5
both are the same kind of object: True
type(Gauge())dăGauge, iartype(Gauge)dătype— încă o treaptă pe aceeași scară, ceea ce este toată ideea pe care se sprijină o metaclasă.- Apelul
type(...)cu trei argumente produce o clasă funcțională, cu un atribut și o metodă, dovedind că instrucțiuneaclasseste o scriere comodă a unui apel de funcție, nu o sintaxă specială pe care interpretorul o tratează altfel. type(type)estetype: scara se termină în ea însăși, motiv pentru care nu există o regresie infinită de metaclase deasupra ei.- Ultima linie confirmă că cele două clase sunt același fel de obiect, deci orice este adevărat despre una este adevărat și despre cealaltă.
Cazul real de utilizare: un registru care se umple singur
O dimensiune în plus: înlocuirea lui type cu o metaclasă. Pentru că metaclasa este moștenită, fiecare subclasă rulează codul de înregistrare fără decorator și fără niciun apel manual. Înlocuiește fișierul cu:
class ExporterMeta(type):
registry = {}
def __init__(cls, name, bases, namespace, **kwargs):
super().__init__(name, bases, namespace, **kwargs)
format_name = namespace.get("format_name")
if format_name is not None:
ExporterMeta.registry[format_name] = cls
class Exporter(metaclass=ExporterMeta):
pass
class CsvExporter(Exporter):
format_name = "csv"
def render(self, rows):
return "csv:" + ";".join(rows)
class ParquetExporter(Exporter):
format_name = "parquet"
def render(self, rows):
return "parquet:" + "|".join(rows)
print("registered formats:", sorted(ExporterMeta.registry))
print("the abstract base stayed out:", Exporter not in ExporterMeta.registry.values())
chosen = ExporterMeta.registry["parquet"]
print("lookup by name gives:", chosen.__name__)
print("rendered:", chosen().render(["ORD-4471", "ORD-4472"]))
print("subclasses inherited the metaclass:", type(CsvExporter).__name__)
registered formats: ['csv', 'parquet']
the abstract base stayed out: True
lookup by name gives: ParquetExporter
rendered: parquet:ORD-4471|ORD-4472
subclasses inherited the metaclass: ExporterMeta
Niciun exportator nu pomenește registrul, iar nimic nu apelează vreo funcție de înregistrare — definirea clasei este înregistrarea, pentru că ExporterMeta.__init__ a rulat ca parte din fiecare instrucțiune class. Clasa de bază este exclusă gratuit: nu are niciun format_name în spațiul ei de nume, deci protecția o sare, motiv pentru care verificarea spațiului de nume, nu a clasei terminate, contează aici. Ultima linie este mecanismul care leagă totul — CsvExporter nu a numit niciodată vreo metaclasă, a moștenit una de la Exporter, iar acea moștenire face ca un sistem de pluginuri să funcționeze fără nicio cooperare din partea autorilor de pluginuri.
Validarea la momentul definirii și cârligul modern, mai simplu
Ultima treaptă acoperă cealaltă utilizare clasică și apoi instrumentul care a înlocuit-o în mare parte. Amândouă resping o clasă care omite un atribut necesar și amândouă pică în timp ce modulul este importat. Înlocuiește fișierul cu:
class RequiresFormatName(type):
def __new__(mcls, name, bases, namespace, **kwargs):
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
if bases and "format_name" not in namespace:
raise TypeError(f"{name} must define format_name")
return cls
class Exporter(metaclass=RequiresFormatName):
pass
class JsonExporter(Exporter):
format_name = "json"
print("valid subclass created:", JsonExporter.__name__)
try:
class BrokenExporter(Exporter):
pass
except TypeError as exc:
print("metaclass rejected it:", exc)
class ModernExporter:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if not hasattr(cls, "format_name"):
raise TypeError(f"{cls.__name__} must define format_name")
class AvroExporter(ModernExporter):
format_name = "avro"
print("same rule without a metaclass:", AvroExporter.format_name)
try:
class BrokenModern(ModernExporter):
pass
except TypeError as exc:
print("__init_subclass__ rejected it:", exc)
valid subclass created: JsonExporter
metaclass rejected it: BrokenExporter must define format_name
same rule without a metaclass: avro
__init_subclass__ rejected it: BrokenModern must define format_name
Ambele jumătăți ridică excepția la instrucțiunea class, deci un atribut lipsă înseamnă un eșec la import, nu un AttributeError peste luni, pe o cale de cod rară — acea sincronizare este valoarea reală a verificărilor de la momentul definirii. Cele două diferă prin mașinărie, nu prin efect: versiunea cu metaclasă introduce un al doilea tip în ierarhie și poate inspecta spațiul de nume brut, în timp ce __init_subclass__ este o metodă obișnuită de pe părinte, care vede clasa terminată. Pentru o regulă atât de simplă precum „declară acest atribut", a doua este cea de scris, iar faptul că știi asta face parte din răspuns la fel de mult ca faptul că știi cum funcționează metaclasele.
Cazuri limită și capcane
- Metaclasele intră în conflict. O clasă ale cărei baze au fost construite de două metaclase fără legătură ridică
TypeError: metaclass conflict, pentru că metaclasa clasei derivate trebuie să fie o subclasă a metaclasei fiecărei baze. Acesta este motivul practic pentru care metaclasele sunt de evitat în cod de bibliotecă pe care alții îl vor amesteca cu al lor. __init_subclass__este implicit o metodă de clasă. Python o învelește automat, deci scrierea lui@classmethodpe ea este redundantă, iar primul ei parametru este noua subclasă, nu părintele pe care este definită.- Corpul clasei rulează înainte ca metaclasa să vadă o clasă.
__prepare__furnizează maparea în care este executat corpul și rulează prima, adică este cârligul de suprascris când contează ordinea sau forma spațiului de nume. ABCMetaeste o metaclasă reală, pe care deja o folosești.abc.ABCeste doar o clasă cumetaclass=ABCMeta, iar asta face ca@abstractmethodsă blocheze instanțierea — demonstrația proprie a bibliotecii standard pentru acest tipar.- Metaclasele nu fac nimic pentru instanțe. Ele rulează la crearea clasei, deci un comportament care ar trebui să se aplice per obiect își are locul în
__init__, într-un descriptor sau într-o clasă de bază obișnuită. Recurgerea la o metaclasă ca să modifici comportamentul instanțelor este folosirea greșită clasică.
Rezumat
O metaclasă este clasa unei clase, iar o instrucțiune class este un apel către ea. Suprascrierea lui __new__ sau __init__ pe o subclasă a lui type permite unui cod să ruleze o dată per definiție de clasă, ceea ce face posibile înregistrarea automată și validarea la momentul definirii — iar pentru că metaclasa este moștenită, subclasele primesc acel comportament fără să opteze pentru el. Pentru majoritatea regulilor care cereau odinioară una, __init_subclass__ exprimă acum același lucru cu mai puțină mașinărie.
type(obj)dă clasa lui, iartype(cls)dătype; apelarea luitype(name, bases, namespace)construiește o clasă exact cum o face o instrucțiuneclass.- O metaclasă derivă din
typeși suprascrie__new__(vede spațiul de nume brut) sau__init__(vede clasa terminată). - Metaclasele sunt moștenite, deci fiecare subclasă rulează același cod de la momentul definirii, fără să îl numească.
- Înregistrarea și validarea sunt cele două utilizări care se rentabilizează cu adevărat, amândouă pentru că transformă o surpriză de la rulare într-un eșec de la import.
- Bazele construite de metaclase fără legătură nu pot fi combinate, ceea ce limitează cât de sigur se amestecă metaclasele între biblioteci.
- Preferă
__init_subclass__pentru reguli per subclasă și__set_name__pentru numirea descriptorilor; păstrează metaclasele pentru controlul spațiului de nume sau al obiectului clasă însuși.