2016-03-01 18 views
6

Ich habe eine grobe Vorstellung davon, was Meta-Klassen sind. Sie sind die Klassen, auf denen Klassenobjekte basieren (weil Klassen Objekte in Python sind). Aber könnte jemand (mit Code) erklären, wie man einen erstellt.Wie erstellt man eine Metaklasse?

+0

Für einen eingehenden Blick auf die * warum * der Metaklassen, siehe [diese Antwort] (http://stackoverflow.com/a/6581949/208880). –

+0

@PeterMortensen: Nein. Diese Frage sucht nach einer Off-Site-Ressource. –

Antwort

4

Es gibt (an dieser Stelle) zwei wichtige Methoden in einer Metaklasse:

  • __prepare__ und
  • __new__

__prepare__ können Sie eine benutzerdefinierte Zuordnung (wie eine OrderedDict) liefern Wird während der Erstellung der Klasse als Namespace verwendet. Sie müssen eine Instanz des von Ihnen ausgewählten Namespaces zurückgeben. Wenn Sie __prepare__ nicht implementieren, wird ein normaler dict verwendet.

__new__ ist verantwortlich für die eigentliche Erstellung/Änderung der endgültigen Klasse.

Eine nackte Knochen, do-nichts-extra metaclass würde wie folgt aussehen:

class Meta(type): 

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

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

Ein einfaches Beispiel:

Sagen Sie einige einfache Validierung Code auf Ihrer Attribute ausgeführt werden soll - wie es muss immer ein int oder ein str sein. Ohne eine Metaklasse, würde Ihre Klasse in etwa so aussehen:

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

Wie Sie sehen können, haben Sie zweimal den Namen des Attributs zu wiederholen. Dies macht Tippfehler zusammen mit irritierenden Bugs möglich.

Ein einfaches metaclass dieses Problem angehen können:

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

Dies ist, was die Metaklasse aussehen würde (nicht __prepare__ verwenden, da es nicht benötigt wird):

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) 

Ein Probelauf von:

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

produziert:

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

Hinweise

Dieses Beispiel ist einfach genug, um es auch mit einer Klasse Dekorateur erreicht worden sein könnte, aber vermutlich eine tatsächliche metaclass würde viel mehr tun.

In Python 2.x, die __prepare__ Methode nicht existiert, und die Klasse seiner speficies Metaklasse durch eine Klassenvariable einschließlich __metaclass__ = ..., wie folgt aus:

class Person(object): 
    __metaclass__ = ValidateType 

Die Referenz 'ValidateType' Klasse:

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 
0

I‘ Ich habe gerade ein vollständig kommentiertes Beispiel einer Metaklasse geschrieben. Es ist in Python 2.7. Ich teile es hier und hoffe, dass es Ihnen helfen kann, mehr über die Methoden __new__, __init__, __call__, __dict__ und das Konzept der begrenzten/unbegrenzten in Python, sowie die Verwendung von Metaklassen zu verstehen.

Das Problem mit einem metaclass, ich fühle mich ist, dass es zu viele Orte, wo man die gleichen Dinge tun kann oder ähnliche noch mit einigen leichten Unterschieden. So betont meine Kommentare und Testfälle hauptsächlich wo zu schreiben was, was zu wo an bestimmten Punkten geht, und was sind zugänglich zu einem bestimmten Objekt.

Das Beispiel versucht, eine Klassenfactory zu erstellen, während wohldefinierte Klassendefinitionen beibehalten werden.

from pprint import pprint 
from types import DictType 

class FactoryMeta(type): 
    """ Factory Metaclass """ 

    # @ Anything "static" (bounded to the classes rather than the instances) 
    # goes in here. Or use "@classmethod" decorator to bound it to meta. 
    # @ Note that these members won't be visible to instances, you have to 
    # manually add them to the instances in metaclass' __call__ if you wish 
    # to access them through a instance directly (see below). 
    extra = "default extra" 
    count = 0 

    def clsVar(cls): 
     print "Class member 'var': " + str(cls.var) 

    @classmethod 
    def metaVar(meta): 
     print "Metaclass member 'var': " + str(meta.var) 

    def __new__(meta, name, bases, dict): 
     # @ Metaclass' __new__ serves as a bi-functional slot capable for 
     # initiating the classes as well as alternating the meta. 
     # @ Suggestion is putting majority of the class initialization code 
     # in __init__, as you can directly reference to cls there; saving 
     # here for anything you want to dynamically added to the meta (such 
     # as shared variables or lazily GC'd temps). 
     # @ Any changes here to dict will be visible to the new class and their 
     # future instances, but won't affect the metaclass. While changes 
     # directly through meta will be visible to all (unless you override 
     # it later). 
     dict['new_elem'] = "effective" 
     meta.var = "Change made to %s by metaclass' __new__" % str(meta) 
     meta.count += 1 
     print "================================================================" 
     print " Metaclass's __new__ (creates class objects)" 
     print "----------------------------------------------------------------" 
     print "Bounded to object: " + str(meta) 
     print "Bounded object's __dict__: " 
     pprint(DictType(meta.__dict__), depth = 1) 
     print "----------------------------------------------------------------" 
     print "Parameter 'name': " + str(name) 
     print "Parameter 'bases': " + str(bases) 
     print "Parameter 'dict': " 
     pprint(dict, depth = 1) 
     print "\n" 
     return super(FactoryMeta, meta).__new__(meta, name, bases, dict) 

    def __init__(cls, name, bases, dict): 
     # @ Metaclass' __init__ is the standard slot for class initialization. 
     # Classes' common variables should mainly goes in here. 
     # @ Any changes here to dict won't actually affect anything. While 
     # changes directly through cls will be visible to the created class 
     # and its future instances. Metaclass remains untouched. 
     dict['init_elem'] = "defective" 
     cls.var = "Change made to %s by metaclass' __init__" % str(cls) 
     print "================================================================" 
     print " Metaclass's __init__ (initiates class objects)" 
     print "----------------------------------------------------------------" 
     print "Bounded to object: " + str(cls) 
     print "Bounded object's __dict__: " 
     pprint(DictType(cls.__dict__), depth = 1) 
     print "----------------------------------------------------------------" 
     print "Parameter 'name': " + str(name) 
     print "Parameter 'bases': " + str(bases) 
     print "Parameter 'dict': " 
     pprint(dict, depth = 1) 
     print "\n" 
     return super(FactoryMeta, cls).__init__(name, bases, dict) 

    def __call__(cls, *args): 
     # @ Metaclass' __call__ gets called when a class name is used as a 
     # callable function to create an instance. It is called before the 
     # class' __new__. 
     # @ Instance's initialization code can be put in here, although it 
     # is bounded to "cls" rather than instance's "self". This provides 
     # a slot similar to the class' __new__, where cls' members can be 
     # altered and get copied to the instances. 
     # @ Any changes here through cls will be visible to the class and its 
     # instances. Metaclass remains unchanged. 
     cls.var = "Change made to %s by metaclass' __call__" % str(cls) 
     # @ "Static" methods defined in the meta which cannot be seen through 
     # instances by default can be manually assigned with an access point 
     # here. This is a way to create shared methods between different 
     # instances of the same metaclass. 
     cls.metaVar = FactoryMeta.metaVar 
     print "================================================================" 
     print " Metaclass's __call__ (initiates instance objects)" 
     print "----------------------------------------------------------------" 
     print "Bounded to object: " + str(cls) 
     print "Bounded object's __dict__: " 
     pprint(DictType(cls.__dict__), depth = 1) 
     print "\n" 
     return super(FactoryMeta, cls).__call__(*args) 

class Factory(object): 
    """ Factory Class """ 

    # @ Anything declared here goes into the "dict" argument in the metaclass' 
    # __new__ and __init__ methods. This provides a chance to pre-set the 
    # member variables desired by the two methods, before they get run. 
    # @ This also overrides the default values declared in the meta. 
    __metaclass__ = FactoryMeta 
    extra = "overridng extra" 

    def selfVar(self): 
     print "Instance member 'var': " + str(self.var) 

    @classmethod 
    def classFactory(cls, name, bases, dict): 
     # @ With a factory method embedded, the Factory class can act like a 
     # "class incubator" for generating other new classes. 
     # @ The dict parameter here will later be passed to the metaclass' 
     # __new__ and __init__, so it is the right place for setting up 
     # member variables desired by these two methods. 
     dict['class_id'] = cls.__metaclass__.count # An ID starts from 0. 
     # @ Note that this dict is for the *factory product classes*. Using 
     # metaclass as callable is another way of writing class definition, 
     # with the flexibility of employing dynamically generated members 
     # in this dict. 
     # @ Class' member methods can be added dynamically by using the exec 
     # keyword on dict. 
     exec(cls.extra, dict) 
     exec(dict['another_func'], dict) 
     return cls.__metaclass__(name + ("_%02d" % dict['class_id']), bases, dict) 

    def __new__(cls, function): 
     # @ Class' __new__ "creates" the instances. 
     # @ This won't affect the metaclass. But it does alter the class' member 
     # as it is bounded to cls. 
     cls.extra = function 
     print "================================================================" 
     print " Class' __new__ (\"creates\" instance objects)" 
     print "----------------------------------------------------------------" 
     print "Bounded to object: " + str(cls) 
     print "Bounded object's __dict__: " 
     pprint(DictType(cls.__dict__), depth = 1) 
     print "----------------------------------------------------------------" 
     print "Parameter 'function': \n" + str(function) 
     print "\n" 
     return super(Factory, cls).__new__(cls) 

    def __init__(self, function, *args, **kwargs): 
     # @ Class' __init__ initializes the instances. 
     # @ Changes through self here (normally) won't affect the class or the 
     # metaclass; they are only visible locally to the instances. 
     # @ However, here you have another chance to make "static" things 
     # visible to the instances, "locally". 
     self.classFactory = self.__class__.classFactory 
     print "================================================================" 
     print " Class' __init__ (initiates instance objects)" 
     print "----------------------------------------------------------------" 
     print "Bounded to object: " + str(self) 
     print "Bounded object's __dict__: " 
     pprint(DictType(self.__dict__), depth = 1) 
     print "----------------------------------------------------------------" 
     print "Parameter 'function': \n" + str(function) 
     print "\n" 
     return super(Factory, self).__init__(*args, **kwargs) 
# @ The metaclass' __new__ and __init__ will be run at this point, where the 
# (manual) class definition hitting its end. 
# @ Note that if you have already defined everything well in a metaclass, the 
# class definition can go dummy with simply a class name and a "pass". 
# @ Moreover, if you use class factories extensively, your only use of a 
# manually defined class would be to define the incubator class. 

Die Ausgabe sieht wie folgt aus (für eine bessere Demonstration angepasst):

================================================================ 
Metaclass's __new__ (creates class objects) 
---------------------------------------------------------------- 
Bounded to object: <class '__main__.FactoryMeta'> 
Bounded object's __dict__: 
{ ..., 
'clsVar': <function clsVar at 0x00000000029BC828>, 
'count': 1, 
'extra': 'default extra', 
'metaVar': <classmethod object at 0x00000000029B4B28>, 
'var': "Change made to <class '__main__.FactoryMeta'> by metaclass' __new__"} 
---------------------------------------------------------------- 
Parameter 'name': Factory 
Parameter 'bases': (<type 'object'>,) 
Parameter 'dict': 
{ ..., 
'classFactory': <classmethod object at 0x00000000029B4DC8>, 
'extra': 'overridng extra', 
'new_elem': 'effective', 
'selfVar': <function selfVar at 0x00000000029BC6D8>} 

================================================================ 
Metaclass's __init__ (initiates class objects) 
---------------------------------------------------------------- 
Bounded to object: <class '__main__.Factory'> 
Bounded object's __dict__: 
{ ..., 
'classFactory': <classmethod object at 0x00000000029B4DC8>, 
'extra': 'overridng extra', 
'new_elem': 'effective', 
'selfVar': <function selfVar at 0x00000000029BC6D8>, 
'var': "Change made to <class '__main__.Factory'> by metaclass' __init__"} 
---------------------------------------------------------------- 
Parameter 'name': Factory 
Parameter 'bases': (<type 'object'>,) 
Parameter 'dict': 
{ ..., 
'classFactory': <classmethod object at 0x00000000029B4DC8>, 
'extra': 'overridng extra', 
'init_elem': 'defective', 
'new_elem': 'effective', 
'selfVar': <function selfVar at 0x00000000029BC6D8>} 

Die Aufrufsequenz metaclass' __new__ dann __init__ seine ist. __call__ wird zu diesem Zeitpunkt nicht aufgerufen.

Und wenn wir eine Instanz erstellen,

func1 = (
    "def printElems(self):\n" 
    " print \"Member new_elem: \" + self.new_elem\n" 
    " print \"Member init_elem: \" + self.init_elem\n" 
    ) 
factory = Factory(func1) 

Die Ausgabe lautet:

================================================================ 
Metaclass's __call__ (initiates instance objects) 
---------------------------------------------------------------- 
Bounded to object: <class '__main__.Factory'> 
Bounded object's __dict__: 
{ ..., 
'classFactory': <classmethod object at 0x00000000029B4DC8>, 
'extra': 'overridng extra', 
'metaVar': <bound method type.metaVar of <class '__main__.FactoryMeta'>>, 
'new_elem': 'effective', 
'selfVar': <function selfVar at 0x00000000029BC6D8>, 
'var': "Change made to <class '__main__.Factory'> by metaclass' __call__"} 

================================================================ 
Class' __new__ ("creates" instance objects) 
---------------------------------------------------------------- 
Bounded to object: <class '__main__.Factory'> 
Bounded object's __dict__: 
{ ..., 
'classFactory': <classmethod object at 0x00000000029B4DC8>, 
'extra': 'def printElems(self):\n print "Member new_elem: " + self.new_elem\n print "Member init_elem: " + self.init_elem\n', 
'metaVar': <bound method type.metaVar of <class '__main__.FactoryMeta'>>, 
'new_elem': 'effective', 
'selfVar': <function selfVar at 0x00000000029BC6D8>, 
'var': "Change made to <class '__main__.Factory'> by metaclass' __call__"} 
---------------------------------------------------------------- 
Parameter 'function': 
def printElems(self): 
    print "Member new_elem: " + self.new_elem 
    print "Member init_elem: " + self.init_elem 

================================================================ 
Class' __init__ (initiates instance objects) 
---------------------------------------------------------------- 
Bounded to object: <__main__.Factory object at 0x00000000029BB7B8> 
Bounded object's __dict__: 
{'classFactory': <bound method FactoryMeta.classFactory of <class '__main__.Factory'>>} 
---------------------------------------------------------------- 
Parameter 'function': 
def printElems(self): 
    print "Member new_elem: " + self.new_elem 
    print "Member init_elem: " + self.init_elem 

Die Metaklasse __call__ zuerst aufgerufen wird, dann wird die Klasse __new__ und __init__.

Vergleichen Sie die gedruckten Elemente jedes Objekts und Sie können feststellen, wann und wo sie hinzugefügt oder geändert werden, genau wie ich im Code kommentiert habe.

Ich betreibe auch folgende Testfälle:

factory.clsVar() # Will raise exception 
Factory.clsVar() 
factory.metaVar() 
factory.selfVar() 

func2 = (
    "@classmethod\n" 
    "def printClassID(cls):\n" 
    " print \"Class ID: %02d\" % cls.class_id\n" 
    ) 
ProductClass1 = factory.classFactory("ProductClass", (object,), { 'another_func': func2 }) 

product = ProductClass1() 
product.printClassID() 
product.printElems() # Will raise exception 

ProductClass2 = Factory.classFactory("ProductClass", (Factory,), { 'another_func': "pass" }) 
ProductClass2.printClassID() # Will raise exception 
ProductClass3 = ProductClass2.classFactory("ProductClass", (object,), { 'another_func': func2 }) 

, die Sie selbst ausführen können, um zu sehen, wie es funktioniert.

Beachten Sie, dass ich die Namen der dynamisch generierten Klassen absichtlich von den Variablennamen, denen sie zugewiesen haben, unterscheidet. Dies zeigt an, welche Namen tatsächlich wirksam sind.

Ein weiterer Hinweis ist, dass ich "static" in Anführungszeichen gesetzt habe, die ich auf das Konzept wie in C++ statt der Python-Dekorator verweisen. Traditionsgemäß bin ich ein C++ - Programmierer, also denke ich immer noch auf seine Weise.

Verwandte Themen