Cosa sono le metaclassi in Python?



Answers

Classi come oggetti

Prima di capire le metaclassi, è necessario padroneggiare le classi in Python. E Python ha un'idea molto particolare di quali sono le classi, prese in prestito dal linguaggio Smalltalk.

Nella maggior parte delle lingue, le classi sono solo parti di codice che descrivono come produrre un oggetto. Questo è abbastanza vero anche in Python:

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

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

Ma le classi sono più di questo in Python. Anche le classi sono oggetti.

Sì, oggetti.

Non appena si utilizza la class parola chiave, Python la esegue e crea un OBJECT. Le istruzioni

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

crea in memoria un oggetto con il nome "ObjectCreator".

Questo oggetto (la classe) è esso stesso in grado di creare oggetti (le istanze), ed è per questo che è una classe .

Ma ancora, è un oggetto, e quindi:

  • puoi assegnarlo a una variabile
  • puoi copiarlo
  • è possibile aggiungere attributi ad esso
  • puoi passarlo come parametro di funzione

per esempio:

>>> print(ObjectCreator) # you can print a class because it's 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>

Creare classi dinamicamente

Poiché le classi sono oggetti, puoi crearle al volo, come qualsiasi oggetto.

Innanzitutto, puoi creare una classe in una funzione usando la 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>

Ma non è così dinamico, dal momento che devi ancora scrivere l'intera classe da solo.

Poiché le classi sono oggetti, devono essere generate da qualcosa.

Quando si utilizza la parola chiave class , Python crea automaticamente questo oggetto. Ma come con la maggior parte delle cose in Python, ti dà un modo per farlo manualmente.

Ricorda il type funzione? La buona vecchia funzione che ti permette di sapere di che tipo è un oggetto:

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

Bene, il type ha un'abilità completamente diversa, può anche creare classi al volo. type può prendere la descrizione di una classe come parametri e restituire una classe.

(Lo so, è sciocco che la stessa funzione possa avere due usi completamente diversi in base ai parametri che si passano ad esso. È un problema dovuto alla retrocompatibilità in Python)

type funziona in questo modo:

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

per esempio:

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

può essere creato manualmente in questo modo:

>>> 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>

Noterai che usiamo "MyShinyClass" come nome della classe e come variabile per contenere il riferimento alla classe. Possono essere diversi, ma non c'è motivo di complicare le cose.

type accetta un dizionario per definire gli attributi della classe. Così:

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

Può essere tradotto in:

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

E usato come una classe normale:

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

E naturalmente, puoi ereditare da esso, quindi:

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

sarebbe:

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

Alla fine ti consigliamo di aggiungere metodi alla tua classe. Basta definire una funzione con la firma appropriata e assegnarla come attributo.

>>> 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

Ed è possibile aggiungere ancora più metodi dopo aver creato dinamicamente la classe, proprio come aggiungere metodi a un oggetto di classe creato normalmente.

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

Vedi dove stiamo andando: in Python, le classi sono oggetti e puoi creare una classe al volo, in modo dinamico.

Questo è ciò che Python fa quando si utilizza la class parola chiave, e lo fa usando una metaclasse.

Cosa sono i metaclassi (finalmente)

I metaclassi sono le "cose" che creano classi.

Definisci le classi per creare oggetti, giusto?

Ma abbiamo imparato che le classi Python sono oggetti.

Bene, le metaclassi sono ciò che creano questi oggetti. Sono le classi delle classi, puoi immaginarle in questo modo:

MyClass = MetaClass()
my_object = MyClass()

Hai visto quel type ti permette di fare qualcosa di simile:

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

È perché il type funzione è in realtà un metaclasse. type è il metaclass usato da Python per creare tutte le classi dietro le quinte.

Ora ti chiedi perché diavolo è scritto in lettere minuscole, e non Type ?

Bene, immagino sia una questione di coerenza con str , la classe che crea gli oggetti di stringhe e int la classe che crea oggetti interi. type è solo la classe che crea oggetti di classe.

Lo vedi controllando l'attributo __class__ .

Tutto, e intendo tutto, è un oggetto in Python. Ciò include ints, stringhe, funzioni e classi. Tutti loro sono oggetti. E tutti sono stati creati da una classe:

>>> 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'>

Ora, qual è il __class__ di qualsiasi __class__ ?

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

Quindi, un metaclasse è solo il materiale che crea oggetti di classe.

Puoi chiamarlo una "fabbrica di classe", se lo desideri.

type è la metaclea incorporata utilizzata da Python, ma ovviamente è possibile creare il proprio metaclasse.

L'attributo __metaclass__

Puoi aggiungere un attributo __metaclass__ quando scrivi una classe:

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

Se lo fai, Python userà il metaclass per creare la classe Foo .

Attento, è difficile.

Scrivi prima la class Foo(object) , ma l'oggetto classe Foo non è ancora stato creato in memoria.

Python cercherà __metaclass__ nella definizione della classe. Se lo trova, lo userà per creare la classe dell'oggetto Foo . In caso contrario, utilizzerà il type per creare la classe.

Leggi più volte.

Quando lo fai:

class Foo(Bar):
    pass

Python fa quanto segue:

Esiste un attributo __metaclass__ in Foo ?

Se sì, crea in memoria un oggetto di classe (ho detto un oggetto di classe, rimani con me qui), con il nome Foo usando ciò che è in __metaclass__ .

Se Python non riesce a trovare __metaclass__ , cercherà un __metaclass__ a livello di MODULO e proverà a fare lo stesso (ma solo per le classi che non ereditano nulla, in pratica le classi vecchio stile).

Quindi, se non riesce a trovare alcun __metaclass__ , utilizzerà il metaclasse proprio della Bar (il primo genitore) (che potrebbe essere il type predefinito) per creare l'oggetto classe.

__metaclass__ attenzione che l'attributo __metaclass__ non sarà ereditato, sarà il metaclasse del genitore ( Bar.__class__ ). Se Bar utilizzato un attributo __metaclass__ che ha creato Bar con type() (e non type.__new__() ), le sottoclassi non erediteranno tale comportamento.

Ora la grande domanda è: cosa puoi inserire in __metaclass__ ?

La risposta è: qualcosa che può creare una classe.

E cosa può creare una classe? type , o qualsiasi cosa che sottoclassi o usi.

Metaclassi personalizzati

Lo scopo principale di un metaclass è di cambiare automaticamente la classe, quando viene creata.

Di solito lo fai per le API, dove vuoi creare classi che corrispondono al contesto corrente.

Immagina uno stupido esempio, in cui decidi che tutte le classi del tuo modulo dovrebbero avere i loro attributi scritti in maiuscolo. Ci sono diversi modi per farlo, ma un modo è quello di impostare __metaclass__ a livello di modulo.

In questo modo, tutte le classi di questo modulo verranno create usando questo metaclasse, e dobbiamo solo dire al metaclass di trasformare tutti gli attributi in maiuscolo.

Fortunatamente, __metaclass__ può effettivamente essere un qualsiasi chiamabile, non ha bisogno di essere una classe formale (lo so, qualcosa con "classe" nel suo nome non ha bisogno di essere una classe, vai a capire ... ma è utile).

Quindi inizieremo con un semplice esempio, utilizzando una funzione.

# 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'

Ora, facciamo esattamente la stessa cosa, ma usando una vera classe per un metaclasse:

# 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's 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)

Ma questo non è davvero OOP. Chiamiamo direttamente il type e non __new__ o chiamiamo il genitore __new__ . Facciamolo:

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)

Potresti aver notato l'argomento extra upperattr_metaclass . Non c'è niente di speciale al riguardo: __new__ riceve sempre la classe in cui è definita, come primo parametro. Proprio come te self per i metodi ordinari che ricevono l'istanza come primo parametro, o la classe che definisce per i metodi di classe.

Naturalmente, i nomi che ho usato qui sono lunghi per motivi di chiarezza, ma come per self , tutti gli argomenti hanno nomi convenzionali. Quindi un metaclass di produzione reale sarebbe simile a questo:

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)

Possiamo renderlo ancora più pulito usando super , che faciliterà l'ereditarietà (perché sì, puoi avere metaclassi, ereditando da metaclassi, ereditando dal tipo):

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)

Questo è tutto. Non c'è davvero niente di più sulle metaclassi.

La ragione dietro la complessità del codice che utilizza metaclassi non è dovuta ai metaclassi, è perché di solito si usano i metaclassi per fare cose contorte basandosi sull'introspezione, manipolando l'ereditarietà, vars come __dict__ , ecc.

In effetti, le metaclassi sono particolarmente utili per fare magia nera e quindi cose complicate. Ma da soli, sono semplici:

  • intercettare una creazione di classe
  • modificare la classe
  • restituire la classe modificata

Perché dovresti usare classi di metaclassi invece di funzioni?

Dal momento che __metaclass__ può accettare qualsiasi chiamabile, perché dovresti usare una classe dal momento che è ovviamente più complicata?

Ci sono diversi motivi per farlo:

  • L'intenzione è chiara. Quando leggi UpperAttrMetaclass(type) , sai cosa seguirà
  • Puoi usare OOP. Metaclass può ereditare dal metaclasse, sovrascrivere i metodi padre. I metaclassi possono persino usare metaclassi.
  • Le sottoclassi di una classe saranno istanze del suo metamato se hai specificato una classe metaclasse, ma non con una metaclass.
  • Puoi strutturare meglio il tuo codice. Non usi mai le metaclassi per qualcosa di banale come nell'esempio sopra. Di solito è per qualcosa di complicato. Avere la possibilità di creare diversi metodi e raggrupparli in una classe è molto utile per facilitare la lettura del codice.
  • Puoi collegarti a __new__ , __init__ e __call__ . Che ti permetterà di fare cose diverse. Anche se di solito puoi fare tutto in __new__ , alcune persone si sentono più a loro agio usando __init__ .
  • Questi sono chiamati metaclassi, dannazione! Deve significare qualcosa!

Perché dovresti usare i metaclassi?

Ora la grande domanda. Perché dovresti utilizzare qualche oscura funzione soggetta a errori?

Beh, di solito non lo fai:

I metaclassi sono una magia più profonda di cui il 99% degli utenti non dovrebbe mai preoccuparsi. Se ti chiedi se ne hai bisogno, non lo fai (le persone che effettivamente ne hanno bisogno sanno con certezza che ne hanno bisogno, e non hanno bisogno di spiegazioni sul perché).

Python Guru Tim Peters

Il caso d'uso principale per un metaclasse sta creando un'API. Un tipico esempio di questo è l'ORM Django.

Ti permette di definire qualcosa di simile a questo:

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

Ma se lo fai:

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

Non restituirà un oggetto IntegerField . Restituirà un int e può anche prenderlo direttamente dal database.

Questo è possibile perché models.Model definisce __metaclass__ e usa un po 'di magia che trasformerà la Person appena definita con semplici istruzioni in un gancio complesso in un campo di database.

Django rende semplice l'aspetto di qualcosa di complesso esponendo una semplice API e utilizzando metaclassi, ricreando il codice da questa API per fare il vero lavoro dietro le quinte.

L'ultima parola

Innanzitutto, sai che le classi sono oggetti che possono creare istanze.

Beh, in effetti, le classi sono esse stesse istanze. Di metaclassi

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

Tutto è un oggetto in Python e sono tutte o istanze di classi o istanze di metaclassi.

Tranne per il type .

type è in realtà il suo metaclasse. Questo non è qualcosa che potresti riprodurre in puro Python, e viene fatto imbrogliando un po 'a livello di implementazione.

In secondo luogo, le metaclassi sono complicate. Potresti non volere usarli per alterazioni di classe molto semplici. Puoi cambiare classe usando due tecniche differenti:

Il 99% delle volte hai bisogno di un'alterazione di classe, stai meglio usando questi.

Ma il 98% delle volte non hai bisogno di alterazioni di classe.

Question

Cosa sono le metaclassi e per cosa le usiamo?




Role of a metaclass's __call__() method when creating a class instance

If you've done Python programming for more than a few months you'll eventually stumble upon code that looks like this:

# define a class
class SomeClass(object):
    # ...
    # some definition here ...
    # ...

# create an instance of it
instance = SomeClass()

# then call the object as if it's a function
result = instance('foo', 'bar')

The latter is possible when you implement the __call__() magic method on the class.

class SomeClass(object):
    # ...
    # some definition here ...
    # ...

    def __call__(self, foo, bar):
        return bar + foo

The __call__() method is invoked when an instance of a class is used as a callable. But as we've seen from previous answers a class itself is an instance of a metaclass, so when we use the class as a callable (ie when we create an instance of it) we're actually calling its metaclass's __call__() method. At this point most Python programmers are a bit confused because they've been told that when creating an instance like this instance = SomeClass() you're calling it's __init__() method. Some who've dug a bit deeper know that before __init__() there's __new__() . Well, today another layer of truth is being revealed, before __new__() there's the metaclass's __call__() .

Let's study the method call chain from specifically the perspective of creating an instance of a class.

This is a metaclass that logs exactly the moment before an instance is created and the moment it's about to return it.

class Meta_1(type):
    def __call__(cls):
        print "Meta_1.__call__() before creating an instance of ", cls
        instance = super(Meta_1, cls).__call__()
        print "Meta_1.__call__() about to return instance."
        return instance

This is a class that uses that metaclass

class Class_1(object):

    __metaclass__ = Meta_1

    def __new__(cls):
        print "Class_1.__new__() before creating an instance."
        instance = super(Class_1, cls).__new__(cls)
        print "Class_1.__new__() about to return instance."
        return instance

    def __init__(self):
        print "entering Class_1.__init__() for instance initialization."
        super(Class_1,self).__init__()
        print "exiting Class_1.__init__()."

And now let's create an instance of Class_1

instance = Class_1()
# Meta_1.__call__() before creating an instance of <class '__main__.Class_1'>.
# Class_1.__new__() before creating an instance.
# Class_1.__new__() about to return instance.
# entering Class_1.__init__() for instance initialization.
# exiting Class_1.__init__().
# Meta_1.__call__() about to return instance.

The code above doesn't actually do anything other than logging the task and then delegating the actual work to the parent (ie keeping the default behavior). So with type being Meta_1 's parent class, we can imagine that this would be the pseudo implementation of type.__call__() :

class type:
    def __call__(cls, *args, **kwarg):

        # ... maybe a few things done to cls here

        # then we call __new__() on the class to create an instance
        instance = cls.__new__(cls, *args, **kwargs)

        # ... maybe a few things done to the instance here

        # then we initialize the instance with its __init__() method
        instance.__init__(*args, **kwargs)

        # ... maybe a few more things done to instance here

        # then we return it
        return instance

We can see that the metaclass's __call__() method is the one that's called first. It then delegates creation of the instance to the class's __new__() method and initialization to the instance's __init__() . It's also the one that ultimately returns the instance.

From the above it stems that the metaclass's __call__() is also given the opportunity to decide whether or not a call to Class_1.__new__() or Class_1.__init__() will eventually be made. Over the course of its execution it could actually return an object that hasn't been touched by either of these methods. Take for example this approach to the singleton pattern:

class Meta_2(type):
    singletons = {}

    def __call__(cls, *args, **kwargs):
        if cls in Meta_2.singletons:
            # we return the only instance and skip a call to __new__() 
            # and __init__()
            print ("{} singleton returning from Meta_2.__call__(), "
                   "skipping creation of new instance.".format(cls))
            return Meta_2.singletons[cls]

        # else if the singleton isn't present we proceed as usual
        print "Meta_2.__call__() before creating an instance."
        instance = super(Meta_2, cls).__call__(*args, **kwargs)
        Meta_2.singletons[cls] = instance
        print "Meta_2.__call__() returning new instance."
        return instance

class Class_2(object):

    __metaclass__ = Meta_2

    def __new__(cls, *args, **kwargs):
        print "Class_2.__new__() before creating instance."
        instance = super(Class_2, cls).__new__(cls)
        print "Class_2.__new__() returning instance."
        return instance

    def __init__(self, *args, **kwargs):
        print "entering Class_2.__init__() for initialization."
        super(Class_2, self).__init__()
        print "exiting Class_2.__init__()."

Let's observe what happens when repeatedly trying to create an object of type Class_2

a = Class_2()
# Meta_2.__call__() before creating an instance.
# Class_2.__new__() before creating instance.
# Class_2.__new__() returning instance.
# entering Class_2.__init__() for initialization.
# exiting Class_2.__init__().
# Meta_2.__call__() returning new instance.

b = Class_2()
# <class '__main__.Class_2'> singleton returning from Meta_2.__call__(), skipping creation of new instance.

c = Class_2()
# <class '__main__.Class_2'> singleton returning from Meta_2.__call__(), skipping creation of new instance.

a is b is c # True



Un uso per i metaclassi è l'aggiunta automatica di nuove proprietà e metodi a un'istanza.

Ad esempio, se guardi i modelli di Django , la loro definizione sembra un po 'confusa. Sembra che tu stia solo definendo le proprietà della classe:

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

Tuttavia, durante il runtime gli oggetti Person sono riempiti con tutti i tipi di metodi utili. Guarda la source di alcune incredibili metacallerie.




Penso che l'introduzione di ONLamp alla programmazione di metaclassi sia ben scritta e dia un'ottima introduzione all'argomento nonostante abbia già diversi anni.

http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html

In breve: una classe è un progetto per la creazione di un'istanza, un metaclasse è un progetto per la creazione di una classe. Si può facilmente vedere che in Python le classi devono essere anche oggetti di prima classe per abilitare questo comportamento.

Non ne ho mai scritto uno anch'io, ma penso che uno degli usi più belli delle metaclassi possa essere visto nel framework Django . Le classi modello usano un approccio metaclass per abilitare uno stile dichiarativo di scrittura di nuovi modelli o classi di form. Mentre il metaclasse sta creando la classe, tutti i membri hanno la possibilità di personalizzare la classe stessa.

La cosa che rimane da dire è: se non sai quali sono i metaclassi, la probabilità che tu non ne abbia bisogno è del 99%.




The tl;dr version

The type(obj) function gets you the type of an object.

The type() of a class is its metaclass .

To use a metaclass:

class Foo(object):
    __metaclass__ = MyMetaClass



Python 3 update

There are (at this point) two key methods in a metaclass:

  • __prepare__ , and
  • __new__

__prepare__ lets you supply a custom mapping (such as an OrderedDict ) to be used as the namespace while the class is being created. You must return an instance of whatever namespace you choose. If you don't implement __prepare__ a normal dict is used.

__new__ is responsible for the actual creation/modification of the final class.

A bare-bones, do-nothing-extra metaclass would like:

class Meta(type):

    def __prepare__(metaclass, cls, bases):
        return dict()

    def __new__(metacls, cls, bases, clsdict):
        return super().__new__(metacls, cls, bases, clsdict)

Un semplice esempio:

Say you want some simple validation code to run on your attributes -- like it must always be an int or a str . Without a metaclass, your class would look something like:

class Person:
    weight = ValidateType('weight', int)
    age = ValidateType('age', int)
    name = ValidateType('name', str)

As you can see, you have to repeat the name of the attribute twice. This makes typos possible along with irritating bugs.

A simple metaclass can address that problem:

class Person(metaclass=Validator):
    weight = ValidateType(int)
    age = ValidateType(int)
    name = ValidateType(str)

This is what the metaclass would look like (not using __prepare__ since it is not needed):

class Validator(type):
    def __new__(metacls, cls, bases, clsdict):
        # search clsdict looking for ValidateType descriptors
        for name, attr in clsdict.items():
            if isinstance(attr, ValidateType):
                attr.name = name
                attr.attr = '_' + name
        # create final class and return it
        return super().__new__(metacls, cls, bases, clsdict)

A sample run of:

p = Person()
p.weight = 9
print(p.weight)
p.weight = '9'

produce:

9
Traceback (most recent call last):
  File "simple_meta.py", line 36, in <module>
    p.weight = '9'
  File "simple_meta.py", line 24, in __set__
    (self.name, self.type, value))
TypeError: weight must be of type(s) <class 'int'> (got '9')

Note : This example is simple enough it could have also been accomplished with a class decorator, but presumably an actual metaclass would be doing much more.

The 'ValidateType' class for reference:

class ValidateType:
    def __init__(self, type):
        self.name = None  # will be set by metaclass
        self.attr = None  # will be set by metaclass
        self.type = type
    def __get__(self, inst, cls):
        if inst is None:
            return self
        else:
            return inst.__dict__[self.attr]
    def __set__(self, inst, value):
        if not isinstance(value, self.type):
            raise TypeError('%s must be of type(s) %s (got %r)' %
                    (self.name, self.type, value))
        else:
            inst.__dict__[self.attr] = value



The type() function can return the type of an object or create a new type,

for example, we can create a Hi class with the type() function and do not need to use this way with class Hi(object):

def func(self, name='mike'):
    print('Hi, %s.' % name)

Hi = type('Hi', (object,), dict(hi=func))
h = Hi()
h.hi()
Hi, mike.

type(Hi)
type

type(h)
__main__.Hi

In addition to using type() to create classes dynamically, you can control creation behavior of class and use metaclass.

According to the Python object model, the class is the object, so the class must be an instance of another certain class. By default, a Python class is instance of the type class. That is, type is metaclass of most of the built-in classes and metaclass of user-defined classes.

class ListMetaclass(type):
    def __new__(cls, name, bases, attrs):
        attrs['add'] = lambda self, value: self.append(value)
        return type.__new__(cls, name, bases, attrs)

class CustomList(list, metaclass=ListMetaclass):
    pass

lst = CustomList()
lst.add('custom_list_1')
lst.add('custom_list_2')

lst
['custom_list_1', 'custom_list_2']

Magic will take effect when we passed keyword arguments in metaclass, it indicates the Python interpreter to create the CustomList through ListMetaclass. new (), at this point, we can modify the class definition, for example, and add a new method and then return the revised definition.




Links