如何创建元类?

时间:2016-03-01 12:40:34

标签: python python-3.x metaclass

我粗略地了解了什么是元类。它们是类对象所基于的类(因为类是Python中的对象)。但是,有人可以解释(使用代码)如何创建一个。

2 个答案:

答案 0 :(得分:6)

(在这一点上)元类中有两个关键方法:

  • __prepare__
  • __new__

__prepare__允许您提供在创建类时用作命名空间的自定义映射(例如OrderedDict)。您必须返回您选择的任何名称空间的实例。如果您未实施__prepare__,则会使用正常dict

__new__负责最终课程的实际创建/修改。

一个简单的,无所事事的额外元类看起来像:

class Meta(type):

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

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

一个简单的例子:

假设您希望在您的属性上运行一些简单的验证代码 - 例如它必须始终为intstr。没有元类,您的类看起来像:

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

如您所见,您必须重复两次属性的名称。这使得拼写错误成为可能,同时也会出现烦躁的错误。

一个简单的元类可以解决这个问题:

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

这就是元类的样子(不使用__prepare__,因为它不需要):

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)

示例运行:

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

产生

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

备注

这个例子很简单,它也可以用类装饰器完成,但可能实际的元类会做得更多。

在Python 2.x中,__prepare__方法不存在,并且该类通过包含类变量__metaclass__ = ...来配置其元类,如下所示:

class Person(object):
    __metaclass__ = ValidateType

'ValidateType'类供参考:

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

答案 1 :(得分:0)

我刚刚编写了一个完全注释的元类示例。它在Python 2.7中。我在这里分享它并希望它可以帮助您更好地了解__new____init____call____dict__方法以及Python中有界/无界的概念,以及元类的使用。

我觉得,元类的问题在于它有太多的地方你可以做同样的事情,或类似的但有一些轻微差异。所以我的评论和测试用例主要强调在某些对象上写在哪里,在某些对象上可访问的内容

该示例尝试在维护格式良好的类定义的同时构建类工厂。

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.

输出看起来像这样(适用于更好的演示):

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

调用序列是元类“__new__,然后是__init__。此时不会调用__call__

如果我们创建一个实例,

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

输出结果为:

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

首先调用元类“__call__,然后调用”__new____init__类。

比较每个对象的打印成员,您可以发现它们被添加或更改的时间和位置,就像我在代码中注释一样。

我还运行以下测试用例:

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

您可以自己运行以查看其工作原理。

请注意,我故意将动态生成的类的名称与它们分配给的变量名不同。这是为了显示哪些名称实际有效。

另一个注意事项是我将“静态”放在引号中,我将其称为C ++中的概念,而不是Python装饰器。传统上我是一名C ++程序员,所以我仍然想以它的方式思考。