返回介绍

建议62:掌握 metaclass

发布于 2024-01-30 22:19:09 字数 9200 浏览 0 评论 0 收藏 0

什么是元类(metaclass)?也许我们对下面这些说法都不陌生:

元类是关于类的类,是类的模板。

元类是用来控制如何创建类的,正如类是创建对象的模板一样。

元类的实例为类,正如类的实例为对象。

这些说法都没有错,在概念之外我们来进行一些更深入的探讨:元类是如何来控制类的创建的?用户该如何定义自己的元类?在哪些情况下需要用到元类?使用元类可以解决什么问题?

我们知Python中一切皆对象,类也是对象,可以在运行的时候动态创建。当使用关键字class的时候,Python解释器在执行的时候就会创建一个对象(这里的对象是指类而非类的实例)。

>>> def dynamic_class(name):
...  if name == 'A':
...      class A(object):
...        pass
...      return A
...  else:
...      class B(object):
...        pass
...      return B
...
>>>
>>> UserClass = dynamic_class('A')
>>> print UserClass
<class '__main__.A'>
>>> UserClass()
<__main__.A object at 0x00D67CF0>
>>>

既然类是对象,那么它就有其所属的类型,也一定还有什么东西能够控制它的生成。通过type查看会发现UserClass的类型为type,而其对象UserClass()的类型为类A。

>>> type(UserClass)
<type 'type'>
>>> type(UserClass())
<class '__main__.A'>

同时我们知道type还可以这样使用:

type(
类名,
父类的元组(针对继承的情况,可以为空),
包含属性的字典(名称和值))

例如:

>>> A=type('A',(object,),{'value':2})
>>> A.value
>>> print A
<class '__main__.A'>
>>> class C(A):
...  pass
...
>>> print C
<class '__main__.C'>
>>>
>>> print C.__class__
<type 'type'>

上例中type通过接受类的描述作为参数返回一个对象,这个对象可以被继承,属性能够被访问,它实际是一个类,其创建由type控制,由type所创建的对象的__class__属性为type。type实际上是Python的一个内建元类,用来直接指导类的生成。当然,除了使用内建元类type,用户也可以通过继承type来自定义元类。我们来看一个利用元类实现强制类型检查的例子。

class TypeSetter(object):
     def __init__(self,fieldtype):
         print "set attribute type",fieldtype
         self.fieldtype = fieldtype
     def is_valid(self,value):
         return isinstance(value,self.fieldtype)
class TypeCheckMeta(type):
     def __new__(cls,name,bases,dict):
         print '-----------------------------------'
         print "Allocating memory for class", name
         print name
         print bases
         print dict
         return super(TypeCheckMeta, cls).__new__(cls,name,bases,dict)
     def __init__(cls,name,bases,dict):
         cls._fields = {}
         for key,value in dict.items():
            if isinstance(value,TypeSetter):
                 cls._fields[key] = value
def sayHi(cls):
         print "Hi"

TypeSetter用来设置属性的类型,TypeCheckMeta为用户自定义的元类,覆盖了type元类中的__new__()方法和__init__()方法。虽然也可以直接使用TypeCheckMeta(name,bases,dict)这种方式来创建类,但更为常见的是在需要被生成的类中设置__metaclass__属性,两种用法是等价的。

class TypeCheck(object):
     __metaclass__ = TypeCheckMeta
     def __setattr__(self,key,value):
         print "set attribute value"
         if key in self._fields:
             if not self._fields[key].is_valid(value):
                 raise TypeError('Invalid type for field')
         super(TypeCheck,self).__setattr__(key,value)
class MetaTest(TypeCheck):
     name = TypeSetter(str)
     num = TypeSetter(int)
mt = MetaTest()
mt.name = "apple"
mt.num = "test"

当类中设置了__metaclass__属性的时候,所有继承自该类的子类都将使用所设置的元类来指导类的生成,因此上述程序的输出如下:

-----------------------------------
Allocating memory for class TypeCheck
TypeCheck
(<type 'object'>,)
{'__module__':'__main__','__metaclass__':<class '__main__.TypeCheckMeta'>, '_
_setattr__': <function __setattr__ at 0x00D61830>}
set attribute type <type 'str'>
set attribute type <type 'int'>
-----------------------------------
Allocating memory for class MetaTest
MetaTest
(<class '__main__.TypeCheck'>,)
{'__module__':'__main__','num': <__main__.TypeSetter object at 0x00D67E70>, 'n
ame': <__main__.TypeSetter object at 0x00D67E50>}
set attribute value
set attribute value
Traceback (most recent call last):
 File "metatest.py", line 38, in <module>
  mt.num = "test"
 File "metatest.py", line 28, in __setattr__
  raise TypeError('Invalid type for field')
TypeError: Invalid type for field

实际上,在新式类中当一个类未设置__metaclass__属性的时候,它将使用默认的type元类来生成类。而当该属性被设置时查找规则如下:

1)如果存在dict['__metaclass__'],则使用对应的值来构建类;否则使用其父类dict['__metaclass__']中所指定的元类来构建类,当父类中也不存在指定的metaclass的情形下使用默认元类type。

2)对于古典类,条件1不满足的情况下,如果存在全局变量__metaclass__,则使用该变量所对应的元类来构建类;否则使用types.ClassType。

读者可以通过将上述例子中__metaclass__=TypeCheckMeta设置为模块级别或者将TypeCheck改为古典类来验证上述查找规则。

需要额外提醒的是,元类中所定义的方法为其所创建的类的类方法,并不属于该类的对象。因此上例中mt.sayHi()会抛出AttributeError: 'MetaTest' object has no attribute 'sayHi'错误,而调用该方法的正确途径为MetaTest.sayHi()。

那么在什么情况下会用到元类呢?有句话是这么说的:当你面临一个问题还在纠结要不要使用元类的时候,往往会有其他的更为简单的解决方案。

Python界的领袖Tim Peters曾这样说过:“元类就是深度的魔法,99%的用户应该根本不必为此操心。如果你想搞清楚究竟是否需要用到元类,那么你就不需要它。那些实际用到元类的人都非常清楚地知道他们需要做什么,而且根本不需要解释为什么要用元类。”

我们来看几个使用元类的场景。

1)利用元类来实现单例模式。

class Singleton(type):
     def __init__(cls,name,bases,dic):
         super(Singleton,cls).__init__(name,bases,dic)
         cls.instance = None  
     def __call__(cls,*args,**kwargs):
         if cls.instance is None:
               print "creating a new instance"
               cls.instance = super(Singleton,cls).__call__
                 (*args,**kwargs)
         else:
               print "warning:only allowed to create one 
                 instance,minstance already exists!"
         return cls.instance
class MySingleton(object):
     __metaclass__ = Singleton 

2)第二个例子来源于Python的标准库string.Template.string,它提供简单的字符串替换功能。常见的使用例子如下:

Template('$name $age').substitute({'name':'admin'}, age=26)

该标准库的源代码中就用到了元类,Template的元类为_TemplateMetaclass。_TemplateMetaclass的__init__()方法通过查找属性(pattern、delimiter和idpattern)并将其构建为一个编译好的正则表达式存放在pattern属性中。用户如果需要自定义分隔符(delimiter)可以通过继承Template并覆盖它的类属性delimiter来实现。string.Template的部分源代码如下:

class Template:
  """A string class for supporting $-substitutions."""
  __metaclass__ = _TemplateMetaclass
  delimiter = '$'
  idpattern = r'[_a-z][_a-z0-9]*'
  def __init__(self, template):
     self.template = template
class _TemplateMetaclass(type):
  pattern = r"""
  %(delim)s(?:
   (?P<escaped>%(delim)s) |   # Escape sequence of two delimiters
   (?P<named>%(id)s)    |   # delimiter and a Python identifier
   {(?P<braced>%(id)s)}   |   # delimiter and a braced identifier
   (?P<invalid>)        # Other ill-formed delimiter exprs
  )
  """
  def __init__(cls, name, bases, dct):
    super(_TemplateMetaclass, cls).__init__(name, bases, dct)
    if 'pattern' in dct:
      pattern = cls.pattern
    else:
      pattern = _TemplateMetaclass.pattern % {
        'delim' : _re.escape(cls.delimiter),
        'id'  : cls.idpattern,
      }
    cls.pattern = _re.compile(pattern, _re.IGNORECASE | _re.VERBOSE)

另外在Django ORM、AOP编程中也有大量使用元类的情形。最后来谈谈关于元类需要注意的几点:

1)区别类方法与元方法(定义在元类中的方法)。我们先来看一个例子:Meta和SubMeta都为元类,其中SubMeta继承自Meta。因此f1、f2都为元方法,而Test为普通类,其元类设置为SubMeta,f3为类方法。

>>> class Meta(type):
...  def f1(cls):
...      print "This is f1()"
...
>>> class SubMeta(Meta):
...  def f2(cls):
...      print "This is f2()"
...
>>>
>>> class Test(object):
...  __metaclass__ = SubMeta
...  @classmethod
...  def f3(cls):
...      print " I am f3()"
...
>>>
>>> t= Test()
>>> SubMeta.f1(Test)
This is f1()
>>> Meta.f1(Test)
This is f1()
>>> Test.f1()
This is f1()
>>> SubMeta.f2(Test)
This is f2()
>>> Test.f2()
This is f2()
>>> t.f2()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
AttributeError: 'Test' object has no attribute 'f2'
>>> Test.f3()
 I am f3()
>>> t.f3()
 I am f3()
>>>

上面的例子说明,元方法可以从元类或者类中调用,而不能从类的实例中调用;但类方法可以从类中调用,也可以从类的实例中调用。

2)多继承需要严格限制,否则会产生冲突。

>>> class M1(type):
...  def __new__(meta, name, bases, atts):
...      print "M1 called for " + name
...      return super(M1, meta).__new__(meta, name, bases, atts)
...
>>> class C1(object):
...  __metaclass__ = M1
...
M1 called for C1
>>>
>>> class Sub1(C1):pass
...
M1 called for Sub1
>>> class M2(type):
...  def __new__(meta, name, bases, atts):
...      print "M2 called for " + name
...      return super(M2, meta).__new__(meta, name, bases, atts)
...
>>> class C2(object):
...  __metaclass__ = M2
...
M2 called for C2
>>> class Sub2(C1, C2):
...  pass
...
M1 called for Sub2
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<stdin>", line 4, in __new__
TypeError: Error when calling the metaclass bases
   metaclass conflict: the metaclass of a derived class must be a (non-strict)
subclass of the metaclasses of all its bases

上面的例子中当Sub2同时继承自元类C1和C2的时候会抛出异常,这是因为Python解释器并不知道C1和C2是否兼容,因此会发出冲突警告。解决冲突的办法是重新定义一个派生自M1和M2的元类,并在C3中将其__metaclass__属性设置为该派生类。

>>> class M3(M1, M2):
...  def __new__(meta, name, bases, atts):
...      print "M3 called for " + name
...      return super(M3, meta).__new__(meta, name, bases, atts)
...
>>> class C3(C1, C2):
...  __metaclass__ = M3
...
M3 called for C3
M1 called for C3
M2 called for C3

注意

元类用来指导类的生成,元方法可以从元类或者类中调用,不能从类的实例中调用,而类方法既可以从类中调用也可以从类的实例中调用。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文