Što su metaklase u Pythonu?

Što su metaklasse i zašto ih koristimo?

4879
19 сент. postavljen na e-zadovoljan 19 rujna 2008-09-19 09:10 '08 u 9:10 am 2008-09-19 09:10
@ 19 odgovora

Metaclass je klasa klase. Kako klasa definira ponašanje instance klase, metaklas definira ponašanje klase. Klasa je instanca metaklasa.

Dok u Pythonu možete koristiti proizvoljne pozive za metaklasse (kao što je prikazano na Jerubu ), korisniji pristup je zapravo da bude klasa sama. type je uobičajeni metaclass u Pythonu. Ako ste zainteresirani, da, type je sam po sebi klasa, a to je njegov vlastiti tip. Ne možete stvoriti nešto poput type isključivo u Pythonu, ali Python je malo obmanjiv. Da biste stvorili vlastiti metaclass u Pythonu, samo želite koristiti type potklasa.

Metaclass se najčešće koristi kao tvornica klasa. Baš kao što kreirate instancu klase pozivom klase, Python stvara novu klasu (kada izvršava 'class' operator) pozivajući metaclass. U kombinaciji s uobičajenim metodama __init__ i __new__ , možete stvoriti "dodatne stvari" prilikom kreiranja klase, kao što je registracija nove klase u registru ili čak potpuno zamjena klase s nečim drugim.

Kada se izvrši naredba class , Python prvo izvrši tijelo izraza class kao normalni blok koda. Rezultirajući imenski prostor (dict) sadrži atribute buduće klase. Metaklas se određuje pregledom osnovnih klasa budućeg razreda (naslijeđene __metaclass__ ), atributa __metaclass__ klase (ako postoji) ili globalne varijable __metaclass__ . Metaclass se tada poziva s imenom, osnovama i atributima klase kako bi stvorio njegovu instancu.

Međutim, metaklasse zapravo definiraju vrstu klase, a ne samo tvornicu za nju, tako da s njima možete učiniti mnogo više. Na primjer, u metaklasu možete definirati normalne metode. Ove metode metaklasa slične su metodama klasa, u smislu da ih se može pozvati u klasi bez instance, ali također nisu kao metode klasa, jer se ne mogu pozvati u instanci klase. type.__subclasses__() je primjer metode type metaclass. Također možete definirati uobičajene "magične" metode, kao što su __add__ , __iter__ i __getattr__ , kako biste implementirali ili promijenili ponašanje klase.

Ovo je skupni primjer bitova:

 def make_hook(f): """Decorator to turn 'foo' method into '__foo__'""" f.is_hook = 1 return f class MyType(type): def __new__(mcls, name, bases, attrs): if name.startswith('None'): return None # Go over attributes and see if they should be renamed. newattrs = {} for attrname, attrvalue in attrs.iteritems(): if getattr(attrvalue, 'is_hook', 0): newattrs['__%s__' % attrname] = attrvalue else: newattrs[attrname] = attrvalue return super(MyType, mcls).__new__(mcls, name, bases, newattrs) def __init__(self, name, bases, attrs): super(MyType, self).__init__(name, bases, attrs) # classregistry.register(self, self.interfaces) print "Would register class %s now." % self def __add__(self, other): class AutoClass(self, other): pass return AutoClass # Alternatively, to autogenerate the classname as well as the class: # return type(self.__name__ + other.__name__, (self, other), {}) def unregister(self): # classregistry.unregister(self) print "Would unregister class %s now." % self class MyObject: __metaclass__ = MyType class NoneSample(MyObject): pass # Will print "NoneType None" print type(NoneSample), repr(NoneSample) class Example(MyObject): def __init__(self, value): self.value = value @make_hook def add(self, other): return self.__class__(self.value + other.value) # Will unregister the class Example.unregister() inst = Example(10) # Will fail with an AttributeError #inst.unregister() print inst + inst class Sibling(MyObject): pass ExampleSibling = Example + Sibling # ExampleSibling is now a subclass of both Example and Sibling (with no # content of its own) although it will believe it called 'AutoClass' print ExampleSibling print ExampleSibling.__mro__ 
2286
19 сент. Odgovor Thomasu Woutersu 19. rujna 2008-09-19 10:01 '08 u 10:01 am 2008-09-19 10:01

Klase kao objekti

Prije razumijevanja metaklasa, morate svladati nastavu u Pythonu. Python ima vrlo neobičnu ideju o tome koje su klase posuđene iz jezika Smalltalk.

U većini jezika, klasa je jednostavno dio koda koji opisuje kako stvoriti objekt. To vrijedi i za Python:

 >>> class ObjectCreator(object): ... pass ... >>> my_object = ObjectCreator() >>> print(my_object) <__main__.ObjectCreator object at 0x8974f2c> 

Ali klase su veće nego u pitonu. Nastava je također objekt.

Da, predmeti.

Čim upotrijebite ključnu riječ class , Python ju izvršava i stvara OBJEKT. instrukcija

 >>> class ObjectCreator(object): ... pass ... 

stvara objekt u memoriji nazvan "ObjectCreator".

Sam objekt (klasa) je sposoban stvoriti objekte (instance), i zato je klasa .

Ali ipak, ovo je objekt i stoga:

  • Možete ga dodijeliti varijabli.
  • Možete ga kopirati
  • Možete mu dodati atribute.
  • Možete ga proslijediti kao parametar funkcije.

na primjer:

 >>> print(ObjectCreator) # you can print a class because it an object <class '__main__.ObjectCreator'> >>> def echo(o): ... print(o) ... >>> echo(ObjectCreator) # you can pass a class as a parameter <class '__main__.ObjectCreator'> >>> print(hasattr(ObjectCreator, 'new_attribute')) False >>> ObjectCreator.new_attribute = 'foo' # you can add attributes to a class >>> print(hasattr(ObjectCreator, 'new_attribute')) True >>> print(ObjectCreator.new_attribute) foo >>> ObjectCreatorMirror = ObjectCreator # you can assign a class to a variable >>> print(ObjectCreatorMirror.new_attribute) foo >>> print(ObjectCreatorMirror()) <__main__.ObjectCreator object at 0x8997b4c> 

Kreiranje klasa dinamički

Budući da su klase objekti, možete ih stvoriti u letu, baš kao i svaki drugi objekt.

Prvo, možete stvoriti klasu u funkciji koja koristi class :

 >>> def choose_class(name): ... if name == 'foo': ... class Foo(object): ... pass ... return Foo # return the class, not an instance ... else: ... class Bar(object): ... pass ... return Bar ... >>> MyClass = choose_class('foo') >>> print(MyClass) # the function returns a class, not an instance <class '__main__.Foo'> >>> print(MyClass()) # you can create an object from this class <__main__.Foo object at 0x89c6d4c> 

Ali to nije tako dinamično, jer i dalje morate sami pisati cijeli razred.

Budući da su klase objekti, oni moraju biti generirani nečim.

Kada koristite ključnu riječ class , Python kreira ovaj objekt automatski. Ali, kao i većina stvari u Pythonu, daje vam mogućnost da to učinite ručno.

Sjećate li se type funkcije? Dobra stara funkcija koja vam omogućuje da saznate vrstu objekta:

 >>> print(type(1)) <type 'int'> >>> print(type("1")) <type 'str'> >>> print(type(ObjectCreator)) <type 'type'> >>> print(type(ObjectCreator())) <class '__main__.ObjectCreator'> 

Pa, type ima sasvim drugu sposobnost, može stvoriti i klase u letu. type može uzeti opis klase kao parametre i vratiti klasu.

(Znam da je glupo da ista funkcija može imati dvije potpuno različite upotrebe u skladu s parametrima koje prosljeđujete. To je problem zbog kompatibilnosti unatrag u Pythonu)

type radi na sljedeći način:

 type(name of the class, tuple of the parent class (for inheritance, can be empty), dictionary containing attributes names and values) 

na primjer:

 >>> class MyShinyClass(object): ... pass 

može se izraditi ručno na ovaj način:

 >>> MyShinyClass = type('MyShinyClass', (), {}) # returns a class object >>> print(MyShinyClass) <class '__main__.MyShinyClass'> >>> print(MyShinyClass()) # create an instance with the class <__main__.MyShinyClass object at 0x8997cec> 

Primijetit ćete da koristimo "MyShinyClass" kao ime klase i kao varijablu za pohranjivanje reference na klasu. Mogu biti drugačiji, ali nema razloga da se stvari zakompliciraju.

type uzima rječnik za definiranje atributa klase. pa:

 >>> class Foo(object): ... bar = True 

Može prevesti na:

 >>> Foo = type('Foo', (), {'bar':True}) 

I koristi se kao regularna klasa:

 >>> print(Foo) <class '__main__.Foo'> >>> print(Foo.bar) True >>> f = Foo() >>> print(f) <__main__.Foo object at 0x8a9b84c> >>> print(f.bar) True 

I naravno, od nje možete naslijediti, tako da:

 >>> class FooChild(Foo): ... pass 

bio bi:

 >>> FooChild = type('FooChild', (Foo,), {}) >>> print(FooChild) <class '__main__.FooChild'> >>> print(FooChild.bar) # bar is inherited from Foo True 

Na kraju, želite dodati metode u svoj razred. Jednostavno definirajte funkciju s ispravnim potpisom i dodijelite je kao atribut.

 >>> def echo_bar(self): ... print(self.bar) ... >>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar}) >>> hasattr(Foo, 'echo_bar') False >>> hasattr(FooChild, 'echo_bar') True >>> my_foo = FooChild() >>> my_foo.echo_bar() True 

Možete dodati još više metoda nakon dinamičkog kreiranja klase, baš kao i dodavanje metoda u normalno stvoreni objekt klase.

 >>> def echo_bar_more(self): ... print('yet another method') ... >>> FooChild.echo_bar_more = echo_bar_more >>> hasattr(FooChild, 'echo_bar_more') True 

Vidite gdje idemo: u Pythonu, klase su objekti i možete dinamički stvarati klase u hodu.

To je ono što Python radi kada koristite ključnu riječ class , a to se radi pomoću metaclassa.

Što su metaklasse (napokon)

Metaklasi su "materijal" koji stvara klase.

Definirate klase za stvaranje objekata, zar ne?

Ali naučili smo da su klase Pythona objekti.

Pa, metaklasse su ono što stvara te objekte. To su klase klasa, možete ih predstaviti na sljedeći način:

 MyClass = MetaClass() my_object = MyClass() 

Vidjeli ste da vam ova type omogućuje da učinite nešto slično ovome:

 MyClass = type('MyClass', (), {}) 

To je zato što je type funkcije zapravo metaklas. type je metaklast koji Python koristi za stvaranje svih klasa iza kulisa.

Sada se pitate, što je, dovraga, napisano malim slovima, a ne Type ?

Pa, mislim da je to pitanje dosljednosti sa str , klasom koja stvara stringove, i int s klasom koja stvara cjelobrojne objekte. type je samo klasa koja stvara objekte klase.

To ćete vidjeti provjerom atributa __class__ .

Sve, i mislim sve, objekt je u Pythonu. To uključuje cijele brojeve, nizove, funkcije i klase. Svi oni su predmeti. I svi su stvoreni iz klase:

 >>> age = 35 >>> age.__class__ <type 'int'> >>> name = 'bob' >>> name.__class__ <type 'str'> >>> def foo(): pass >>> foo.__class__ <type 'function'> >>> class Bar(object): pass >>> b = Bar() >>> b.__class__ <class '__main__.Bar'> 

Što je __class__ bilo kojeg __class__ ?

 >>> age.__class__.__class__ <type 'type'> >>> name.__class__.__class__ <type 'type'> >>> foo.__class__.__class__ <type 'type'> >>> b.__class__.__class__ <type 'type'> 

Tako je metaklas samo materijal koji stvara objekte klase.

Možete ga nazvati "tvornica klase" ako želite.

type je ugrađeni metaklas koji koristi Python, ali, naravno, možete stvoriti vlastiti metaklas.

__metaclass__

U Pythonu 2 možete dodati atribut __metaclass__ prilikom pisanja klase (pogledajte sljedeći odjeljak o sintaksi Pythona 3):

 class Foo(object): __metaclass__ = something... [...] 

Ako to učinite, Python će koristiti metaclass za stvaranje Foo klase.

Pažljivo, to je teško.

Prvo pišete class Foo(object) , ali objekt klase Foo još nije stvoren u memoriji.

Python će tražiti __metaclass__ u definiciji klase. Ako ga pronađe, upotrijebit će ga za stvaranje klase objekata Foo . Ako ne, koristit će type za stvaranje klase.

border=0

Pročitajte ovo nekoliko puta.

Kada to učinite:

 class Foo(Bar): pass 

Python čini sljedeće:

Postoji li atribut __metaclass__ u Foo ?

Ako je tako, stvorite objekt klase u memoriji (rekao sam predmet klase, ostanite ovdje sa mnom) s imenom Foo , koristeći ono što je u __metaclass__ .

Ako Python ne može pronaći __metaclass__ , on će tražiti __metaclass__ na razini MODULE i pokušati učiniti isto (ali samo za klase koje ništa ne nasljeđuju, uglavnom klasa starog stila).

Zatim, ako uopće ne može pronaći __metaclass__ , koristit će svoju vlastitu metaklasičnu Bar (prvi roditelj) (koja može biti zadani type ) za stvaranje objekta klase.

Ovdje biste trebali biti oprezni, __metaclass__ atribut __metaclass__ nije naslijedio, i metaclass roditelja ( Bar.__class__ ) će biti. Ako je Bar koristio atribut __metaclass__ koji je stvorio Bar s type() (a ne s type.__new__() ), potklase neće naslijediti ovo ponašanje.

Sada je veliko pitanje što možete staviti u __metaclass__ ?

Odgovor: nešto što može stvoriti klasu.

A što može stvoriti klasu? ili nešto što ga podklasira ili koristi.

Metaklasi u Pythonu 3

Sintaksa za postavljanje metaclassa je promijenjena u Pythonu 3:

 class Foo(object, metaclass=something): ... 

odnosno Atribut __metaclass__ više ne koristi u korist ključnog argumenta na popisu osnovnih klasa.

Međutim, ponašanje metaklasa ostaje u osnovi isto .

Jedna stvar koja se dodaje metaklasama u Pythonu 3 je da također možete proslijediti atribute kao argumente ključne riječi metaklasi, kao što je ovaj:

 class Foo(object, metaclass=something, kwarg1=value1, kwarg2=value2): ... 

Pročitajte odjeljak ispod kako biste saznali kako Python to čini.

Custom metaclasses

Glavna svrha metaklasa je da automatski promijeni klasu kada je stvorena.

Obično to radite za API gdje želite stvoriti klase koje odgovaraju trenutnom kontekstu.

Zamislite glupi primjer kada odlučite da sve klase u vašem modulu imaju atribute napisane velikim slovima. Postoji nekoliko načina da to učinite, ali jedan od njih je postaviti __metaclass__ na razini modula.

Stoga će sve klase ovog modula biti stvorene pomoću ovog metaklasa, a mi samo trebamo odrediti metaklas tako da se svi atributi pretvore u velika slova.

Srećom, __metaclass__ zapravo može biti bilo koji pozivatelj, to ne mora biti formalna klasa (znam da nešto s imenom 'class' u imenu ne mora biti klasa, razumjeti ... ali to je korisno).

Dakle, započinjemo s jednostavnim primjerom pomoću funkcije.

 # the metaclass will automatically get passed the same argument # that you usually pass to 'type' def upper_attr(future_class_name, future_class_parents, future_class_attr): """ Return a class object, with the list of its attribute turned into uppercase. """ # pick up any attribute that doesn't start with '__' and uppercase it uppercase_attr = {} for name, val in future_class_attr.items(): if not name.startswith('__'): uppercase_attr[name.upper()] = val else: uppercase_attr[name] = val # let 'type' do the class creation return type(future_class_name, future_class_parents, uppercase_attr) __metaclass__ = upper_attr # this will affect all classes in the module class Foo(): # global __metaclass__ won't work with "object" though # but we can define __metaclass__ here instead to affect only this class # and this will work with "object" children bar = 'bip' print(hasattr(Foo, 'bar')) # Out: False print(hasattr(Foo, 'BAR')) # Out: True f = Foo() print(f.BAR) # Out: 'bip' 

Sada učinimo isto, ali koristeći pravu klasu za metaklas:

 # remember that 'type' is actually a class like 'str' and 'int' # so you can inherit from it class UpperAttrMetaclass(type): # __new__ is the method called before __init__ # it the method that creates the object and returns it # while __init__ just initializes the object passed as parameter # you rarely use __new__, except when you want to control how the object # is created. # here the created object is the class, and we want to customize it # so we override __new__ # you can do some stuff in __init__ too if you wish # some advanced use involves overriding __call__ as well, but we won't # see this def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr): uppercase_attr = {} for name, val in future_class_attr.items(): if not name.startswith('__'): uppercase_attr[name.upper()] = val else: uppercase_attr[name] = val return type(future_class_name, future_class_parents, uppercase_attr) 

Ali to nije točno PLO. type pozivamo izravno, a ne __new__ ili ne zovemo roditelja __new__ . Učinimo to:

 class UpperAttrMetaclass(type): def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr): uppercase_attr = {} for name, val in future_class_attr.items(): if not name.startswith('__'): uppercase_attr[name.upper()] = val else: uppercase_attr[name] = val # reuse the type.__new__ method # this is basic OOP, nothing magic in there return type.__new__(upperattr_metaclass, future_class_name, future_class_parents, uppercase_attr) 

Možda ste primijetili dodatni argument upperattr_metaclass . Ne postoji ništa posebno u ovome: __new__ uvijek dobiva klasu u kojoj je definiran kao prvi parametar. Baš kao što imate self za normalne metode koje dobivaju instancu kao prvi parametar, ili definirajuću klasu za metode razreda.

Naravno, imena koja sam koristio ovdje su duga za jasnoćom, ali, što se tiče self , svi argumenti imaju uvjetna imena. Stoga će stvarni metaklas proizvodnje izgledati ovako:

 class UpperAttrMetaclass(type): def __new__(cls, clsname, bases, dct): uppercase_attr = {} for name, val in dct.items(): if not name.startswith('__'): uppercase_attr[name.upper()] = val else: uppercase_attr[name] = val return type.__new__(cls, clsname, bases, uppercase_attr) 

Možemo ga učiniti još čišćim koristeći super , što olakšava nasljeđivanje (jer da, možete imati metaklasu, naslijedivši od metaklasa, naslijedivši od tipa):

 class UpperAttrMetaclass(type): def __new__(cls, clsname, bases, dct): uppercase_attr = {} for name, val in dct.items(): if not name.startswith('__'): uppercase_attr[name.upper()] = val else: uppercase_attr[name] = val return super(UpperAttrMetaclass, cls).__new__(cls, clsname, bases, uppercase_attr) 

Da, iu Pythonu 3, ako ovaj poziv izvedete s argumentima ključnih riječi, na primjer:

 class Foo(object, metaclass=Thing, kwarg1=value1): ... 

To ga prevodi u metaklas kako bi ga koristio:

 class Thing(type): def __new__(class, clsname, bases, dct, kwargs1=default): ... 

Jeste. Nema ništa više u metaklasama.

Razlog složenosti kodiranja pomoću metaklasa nisu metaklasi, već činjenica da obično koristite metaklasu za uvijanje, oslanjajući se na introspekciju, manipuliranje nasljeđem, varijable poput __dict__ , itd.

Doista, metaklasi su posebno korisni za crnu magiju i, prema tome, za složene stvari. Ali sami su jednostavni.

  • stvaranje klase presretanja
  • promjena klase
  • vrati promijenjenu klasu

Zašto koristite klase metaklasa umjesto funkcija?

Budući da __metaclass__ može prihvatiti bilo koji __metaclass__ , zašto trebate koristiti klasu jer je očito složenija?

Za to postoji nekoliko razloga:

  • Namjera je jasna. Kada čitate UpperAttrMetaclass(type) , znate što slijedi.
  • Možete koristiti OOP. Metaclass može naslijediti od metaclassa, nadjačati roditeljske metode. Metaklasi mogu čak koristiti metaklasse.
  • Podklasa klase bit će primjerci njezina metaklasa ako ste naveli klasu metaklasa, ali ne s metaklasnom funkcijom.
  • Kôd možete bolje strukturirati. Nikada ne koristite metaklasse za nešto tako trivijalno kao što je to gore navedeno. Ovo je obično za nešto komplicirano. Sposobnost da se napravi nekoliko metoda i grupira u jednu klasu vrlo je korisna za lakše čitanje koda.
  • Možete se spojiti na __new__ , __init__ i __call__ . To će vam omogućiti da radite različite stvari. Čak i ako to obično možete učiniti u __new__ , samo je nekim korisnicima prikladnije koristiti __init__ .
  • To se zove metaklas, prokletstvo! To bi trebalo značiti nešto!

Zašto koristite metaklasse?

Sada je veliko pitanje. Zašto su vam potrebne neke skrivene greške?

Pa, obično to ne radite:

Metaklasa je dublja čarolija, o kojoj 99% korisnika ne bi smjelo brinuti. Ako se pitate trebate li ih, ne trebate ga (ljudi koji ih stvarno trebaju znaju sa sigurnošću da ih trebaju i ne trebaju objašnjenje zašto).

Python Guru Tim Peters

Glavni slučaj uporabe metaklasa je stvaranje API-ja. Tipičan primjer za to je Django ORM.

To vam omogućuje da definirate nešto poput ovoga:

 class Person(models.Model): name = models.CharField(max_length=30) age = models.IntegerField() 

Ali ako to učinite:

 guy = Person(name='bob', age='35') print(guy.age) 

Neće se vratiti objekt IntegerField . Vratit će int i možda će ga preuzeti izravno iz baze podataka.

To je moguće zato što models.Model definira __metaclass__ i koristi magiju koja Person koju ste upravo definirali, koristeći jednostavne operatore, pretvara u kompleksno povezivanje s poljem baze podataka.

Django čini nešto komplicirano jednostavnim pružanjem jednostavnog API-ja i korištenjem metaklasa, ponovno kreirajući kod iz ovog API-ja za pravi posao iza kulisa.

Posljednja riječ

Prvo, znate da su klase objekti koji mogu stvarati instance.

Zapravo, sami razredi su slučajevi. Metaclasses.

 >>> class Foo(object): pass >>> id(Foo) 142630324 

U Pythonu, sve je objekt, a sve su to instance klasa ili primjera metaklasa.

Osim za type .

type je zapravo vlastiti metaklas. To nije nešto što biste mogli reproducirati u čistom Pythonu, a to se radi varanjem na razini implementacije.

Drugo, metaklasi su složeni. Možda ih ne želite koristiti za vrlo jednostavne promjene u razredu. Klasu možete promijeniti pomoću dvije različite metode:

U 99% slučajeva trebate promijeniti razred, bolje je koristiti ih.

Ali u 98% slučajeva uopće ne morate mijenjati razred.

6009
05 июля '11 в 14:29 2011-07-05 14:29 odgovor je dao e-satis 5. srpnja '11 u 14:29 2011-07-05 14:29

Imajte na umu da je ovaj odgovor za Python 2.x, kao što je napisan u 2008, metaclasses su malo drugačiji u 3.x, vidi komentare.

Metaclasses su tajni umak koji radi "klasu". Zadani metaclass za novi objekt stila naziva se "type".

var _tmr = window._tmr || (window._tmr = []);_tmr.push({id: "2334768", type: "pageView", start: (new Date()).getTime()});(function (d, w, id) {  if (d.getElementById(id)) return;  var ts = d.createElement("script"); ts.type = "text/javascript"; ts.async = true; ts.id = id;  ts.src = (d.location.protocol == "https:" ? "https:" : "http:") + "//top-fwz1.mail.ru/js/code.js";  var f = function () {var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ts, s);};  if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); }})(document, window, "topmailru-code");