On m'a posé la même question récemment et j'ai trouvé plusieurs réponses. J'espère que vous pouvez relancer ce fil, car je voulais élaborer sur quelques-uns des cas d'utilisation mentionnés et en ajouter quelques nouveaux.
La plupart des métaclasses que j'ai vues font l'une des deux choses suivantes:
Inscription (ajout d'une classe à une structure de données):
models = {}
class ModelMetaclass(type):
def __new__(meta, name, bases, attrs):
models[name] = cls = type.__new__(meta, name, bases, attrs)
return cls
class Model(object):
__metaclass__ = ModelMetaclass
Chaque fois que vous sous Model
- classez , votre classe est enregistrée dans le models
dictionnaire:
>>> class A(Model):
... pass
...
>>> class B(A):
... pass
...
>>> models
{'A': <__main__.A class at 0x...>,
'B': <__main__.B class at 0x...>}
Cela peut également être fait avec des décorateurs de classe:
models = {}
def model(cls):
models[cls.__name__] = cls
return cls
@model
class A(object):
pass
Ou avec une fonction d'enregistrement explicite:
models = {}
def register_model(cls):
models[cls.__name__] = cls
class A(object):
pass
register_model(A)
En fait, c'est à peu près la même chose: vous mentionnez défavorablement les décorateurs de classe, mais ce n'est vraiment rien de plus que du sucre syntaxique pour une invocation de fonction sur une classe, donc il n'y a pas de magie à ce sujet.
Quoi qu'il en soit, l'avantage des métaclasses dans ce cas est l'héritage, car elles fonctionnent pour toutes les sous-classes, alors que les autres solutions ne fonctionnent que pour les sous-classes explicitement décorées ou enregistrées.
>>> class B(A):
... pass
...
>>> models
{'A': <__main__.A class at 0x...> # No B :(
Refactoring (modification des attributs de classe ou ajout de nouveaux):
class ModelMetaclass(type):
def __new__(meta, name, bases, attrs):
fields = {}
for key, value in attrs.items():
if isinstance(value, Field):
value.name = '%s.%s' % (name, key)
fields[key] = value
for base in bases:
if hasattr(base, '_fields'):
fields.update(base._fields)
attrs['_fields'] = fields
return type.__new__(meta, name, bases, attrs)
class Model(object):
__metaclass__ = ModelMetaclass
Chaque fois que vous sous Model
- classez et définissez des Field
attributs, ils sont injectés avec leurs noms (pour des messages d'erreur plus informatifs, par exemple), et regroupés dans un _fields
dictionnaire (pour une itération facile, sans avoir à parcourir tous les attributs de classe et toutes ses classes de base '' attributs à chaque fois):
>>> class A(Model):
... foo = Integer()
...
>>> class B(A):
... bar = String()
...
>>> B._fields
{'foo': Integer('A.foo'), 'bar': String('B.bar')}
Encore une fois, cela peut être fait (sans héritage) avec un décorateur de classe:
def model(cls):
fields = {}
for key, value in vars(cls).items():
if isinstance(value, Field):
value.name = '%s.%s' % (cls.__name__, key)
fields[key] = value
for base in cls.__bases__:
if hasattr(base, '_fields'):
fields.update(base._fields)
cls._fields = fields
return cls
@model
class A(object):
foo = Integer()
class B(A):
bar = String()
# B.bar has no name :(
# B._fields is {'foo': Integer('A.foo')} :(
Ou explicitement:
class A(object):
foo = Integer('A.foo')
_fields = {'foo': foo} # Don't forget all the base classes' fields, too!
Bien que, contrairement à votre plaidoyer pour une programmation non-méta lisible et maintenable, cela est beaucoup plus encombrant, redondant et sujet aux erreurs:
class B(A):
bar = String()
# vs.
class B(A):
bar = String('bar')
_fields = {'B.bar': bar, 'A.foo': A.foo}
Après avoir examiné les cas d'utilisation les plus courants et les plus concrets, les seuls cas où vous DEVEZ absolument utiliser des métaclasses sont ceux où vous souhaitez modifier le nom de la classe ou la liste des classes de base, car une fois définis, ces paramètres sont intégrés à la classe, et aucun décorateur ou la fonction peut les défaire.
class Metaclass(type):
def __new__(meta, name, bases, attrs):
return type.__new__(meta, 'foo', (int,), attrs)
class Baseclass(object):
__metaclass__ = Metaclass
class A(Baseclass):
pass
class B(A):
pass
print A.__name__ # foo
print B.__name__ # foo
print issubclass(B, A) # False
print issubclass(B, int) # True
Cela peut être utile dans les frameworks pour émettre des avertissements chaque fois que des classes avec des noms similaires ou des arbres d'héritage incomplets sont définies, mais je ne peux pas penser à une raison autre que le trolling pour changer réellement ces valeurs. Peut-être que David Beazley le peut.
Quoi qu'il en soit, dans Python 3, les métaclasses ont également la __prepare__
méthode, qui vous permet d'évaluer le corps de la classe dans un mappage autre que a dict
, prenant ainsi en charge les attributs ordonnés, les attributs surchargés et d'autres trucs super sympas:
import collections
class Metaclass(type):
@classmethod
def __prepare__(meta, name, bases, **kwds):
return collections.OrderedDict()
def __new__(meta, name, bases, attrs, **kwds):
print(list(attrs))
# Do more stuff...
class A(metaclass=Metaclass):
x = 1
y = 2
# prints ['x', 'y'] rather than ['y', 'x']
class ListDict(dict):
def __setitem__(self, key, value):
self.setdefault(key, []).append(value)
class Metaclass(type):
@classmethod
def __prepare__(meta, name, bases, **kwds):
return ListDict()
def __new__(meta, name, bases, attrs, **kwds):
print(attrs['foo'])
# Do more stuff...
class A(metaclass=Metaclass):
def foo(self):
pass
def foo(self, x):
pass
# prints [<function foo at 0x...>, <function foo at 0x...>] rather than <function foo at 0x...>
Vous pourriez affirmer que les attributs ordonnés peuvent être obtenus avec des compteurs de création et que la surcharge peut être simulée avec des arguments par défaut:
import itertools
class Attribute(object):
_counter = itertools.count()
def __init__(self):
self._count = Attribute._counter.next()
class A(object):
x = Attribute()
y = Attribute()
A._order = sorted([(k, v) for k, v in vars(A).items() if isinstance(v, Attribute)],
key = lambda (k, v): v._count)
class A(object):
def _foo0(self):
pass
def _foo1(self, x):
pass
def foo(self, x=None):
if x is None:
return self._foo0()
else:
return self._foo1(x)
En plus d'être beaucoup plus laid, il est également moins flexible: que faire si vous voulez des attributs littéraux ordonnés, comme des entiers et des chaînes? Et si None
est une valeur valide pour x
?
Voici une façon créative de résoudre le premier problème:
import sys
class Builder(object):
def __call__(self, cls):
cls._order = self.frame.f_code.co_names
return cls
def ordered():
builder = Builder()
def trace(frame, event, arg):
builder.frame = frame
sys.settrace(None)
sys.settrace(trace)
return builder
@ordered()
class A(object):
x = 1
y = 'foo'
print A._order # ['x', 'y']
Et voici une façon créative de résoudre le second:
_undefined = object()
class A(object):
def _foo0(self):
pass
def _foo1(self, x):
pass
def foo(self, x=_undefined):
if x is _undefined:
return self._foo0()
else:
return self._foo1(x)
Mais c'est beaucoup, BEAUCOUP vaudou-euh qu'une simple métaclasse (surtout la première, qui fait vraiment fondre votre cerveau). Mon point est que vous considérez les métaclasses comme inconnues et contre-intuitives, mais vous pouvez également les considérer comme la prochaine étape de l'évolution des langages de programmation: il vous suffit d'ajuster votre état d'esprit. Après tout, vous pourriez probablement tout faire en C, y compris définir une structure avec des pointeurs de fonction et la transmettre comme premier argument à ses fonctions. Une personne qui voit C ++ pour la première fois pourrait dire: "Qu'est-ce que cette magie? Pourquoi le compilateur passe-t-il implicitementthis
aux méthodes, mais pas aux fonctions régulières et statiques? Il vaut mieux être explicite et verbeux à propos de vos arguments. "Mais alors, la programmation orientée objet est beaucoup plus puissante une fois que vous l'avez; et c'est aussi, euh ... programmation quasi-orientée aspect, je suppose. Et une fois que vous comprendre les métaclasses, elles sont en fait très simples, alors pourquoi ne pas les utiliser quand cela vous convient?
Et enfin, les métaclasses sont géniales et la programmation doit être amusante. Utiliser en permanence des constructions de programmation et des modèles de conception standard est ennuyeux et sans intérêt et entrave votre imagination. Vis un peu! Voici une métamétaclasse, juste pour vous.
class MetaMetaclass(type):
def __new__(meta, name, bases, attrs):
def __new__(meta, name, bases, attrs):
cls = type.__new__(meta, name, bases, attrs)
cls._label = 'Made in %s' % meta.__name__
return cls
attrs['__new__'] = __new__
return type.__new__(meta, name, bases, attrs)
class China(type):
__metaclass__ = MetaMetaclass
class Taiwan(type):
__metaclass__ = MetaMetaclass
class A(object):
__metaclass__ = China
class B(object):
__metaclass__ = Taiwan
print A._label # Made in China
print B._label # Made in Taiwan
Éditer
C'est une question assez ancienne, mais elle suscite toujours des votes positifs, alors j'ai pensé ajouter un lien vers une réponse plus complète. Si vous souhaitez en savoir plus sur les métaclasses et leurs utilisations, je viens de publier un article à ce sujet ici .