Python中的Classes和Metaclasses详解

类和对象

类和函数一样都是Python中的对象。当一个类定义完成之后,Python将创建一个“类对象”并将其赋值给一个同名变量。类是type类型的对象(是不是有点拗口?)。

类对象是可调用的(callable,实现了 __call__方法),并且调用它能够创建类的对象。你可以将类当做其他对象那么处理。例如,你能够给它们的属性赋值,你能够将它们赋值给一个变量,你可以在任何可调用对象能够用的地方使用它们,比如在一个map中。事实上当你在使用map(str, [1,2,3])的时候,是将一个整数类型的list转换为字符串类型的list,因为str是一个类。可以看看下面的代码:

>>> class C(object):
...   def __init__(self, s):
...       print s
...
>>> myclass = C
>>> type(C)
<type 'type'>
>>> type(myclass)
<type 'type'>
>>> myclass(2)
2
<__main__.C object at 0x10e2bea50>
>>> map(myclass, [1,2,3])
1
2
3
[<__main__.C object at 0x10e2be9d0>, <__main__.C object at 0x10e2bead0>, <__main__.C object at 0x10e2beb10>]
>>> map(C, [1,2,3])
1
2
3
[<__main__.C object at 0x10e2be950>, <__main__.C object at 0x10e2beb50>, <__main__.C object at 0x10e2beb90>]
>>> C.test_attribute = True
>>> myclass.test_attribute
True

正因如此,Python中的“class”关键字不像其他语言(例如C++)那样必须出现在代码main scope中。在Python中,它能够在一个函数中嵌套出现,举个例子,我们能够这样在函数运行的过程中动态的创建类。看代码:

>>> def make_class(class_name):
...   class C(object):
...       def print_class_name(self):
...           print class_name
...   C.__name__ = class_name
...   return C
...
>>> C1, C2 = map(make_class, ["C1", "C2"])
>>> c1, c2 = C1(), C2()
>>> c1.print_class_name()
C1
>>> c2.print_class_name()
C2
>>> type(c1)
<class '__main__.C1'>
>>> type(c2)
<class '__main__.C2'>
>>> c1.print_class_name.__closure__
(<cell at 0x10ab6dbe8: str object at 0x10ab71530>,)

请注意,在这里通过make_class创建的两个类是不同的对象,因此通过它们创建的对象就不属于同一个类型。正如我们在装饰器中做的那样,我们在类被创建之后手动设置了类名。同样也请注意所创建类的print_class_name方法在一个closure cell中捕捉到了类的closure和class_name。如果你对closure的概念还不是很清楚,那么最好去看看前篇,复习一下closures和decorators相关的内容。
Metaclasses

如果类是能够制造对象的对象,那制造类的对象又该叫做什么呢(相信我,这并不是一个先有鸡还是先有蛋的问题)?答案是元类(Metaclasses)。大部分常见的基础元类都是type。当输入一个参数时,type将简单的返回输入对象的类型,这就不涉及元类。然而当输入三个参数时,type将扮演元类的角色,基于输入参数创建一个类并返回。输入参数相当简单:类名,父类及其参数的字典。后面两者可以为空,来看一个例子:

>>> MyClass = type("MyClass", (object,), {"my_attribute": 0})
>>> type(MyClass)
<type 'type'>
>>> o = MyClass()
>>> o.my_attribute
0

特别注意第二个参数是一个tuple(语法看起来很奇怪,以逗号结尾)。如果你需要在类中安排一个方法,那么创建一个函数并且将其以属性的方式传递作为第三个参数,像这样:

>>> def myclass_init(self, my_attr):
...   self.my_attribute = my_attr
...
>>> MyClass = type("MyClass", (object,), {"my_attribute": 0, "__init__": myclass_init})
>>> o = MyClass("Test")
>>> o.my_attribute
'Test'
>>> o.__init__
<bound method MyClass.myclass_init of <__main__.MyClass object at 0x10ab72150>>

我们可以通过一个可调用对象(函数或是类)来自定义元类,这个对象需要三个输入参数并返回一个对象。这样一个元类在一个类上实现只要定义了它的__metaclass__属性。第一个例子,让我们做一些有趣的事情看看我们能够用元类做些什么:

>>> def mymetaclass(name, parents, attributes):
...   return "Hello"
...
>>> class C(object):
...   __metaclass__ = mymetaclass
...
>>> print C
Hello
>>> type(C)
<type 'str'>

请注意以上的代码,C只是简单地将一个变量引用指向了字符串“Hello”。当然了,没人会在实际中写这样的代码,这只是为了演示元类的用法而举的一个简单例子。接下来我们来做一些更有用的操作。在本系列的第二部分我们曾看到如何使用装饰器类来记录目标类每个方法的输出,现在我们来做同样的事情,不过这一次我们使用元类。我们借用之前的装饰器定义:

def log_everything_metaclass(class_name, parents, attributes):
  print "Creating class", class_name
  myattributes = {}
  for name, attr in attributes.items():
    myattributes[name] = attr
    if hasattr(attr, '__call__'):
      myattributes[name] = logged("%b %d %Y - %H:%M:%S",
                    class_name + ".")(attr)
  return type(class_name, parents, myattributes)

class C(object):
  __metaclass__ = log_everything_metaclass

  def __init__(self, x):
    self.x = x

  def print_x(self):
    print self.x

# Usage:
print "Starting object creation"
c = C("Test")
c.print_x()

# Output:
Creating class C
Starting object creation
- Running 'C.__init__' on Aug 05 2013 - 13:50:58
- Finished 'C.__init__', execution time = 0.000s
- Running 'C.print_x' on Aug 05 2013 - 13:50:58
Test
- Finished 'C.print_x', execution time = 0.000s

如你所见,类装饰器与元类有着很多共同点。事实上,任何能够用类装饰器完成的功能都能够用元类来实现。类装饰器有着很简单的语法结构易于阅读,所以提倡使用。但就元类而言,它能够做的更多,因为它在类被创建之前就运行了,而类装饰器则是在类创建之后才运行的。记住这点,让我们来同时运行一下两者,请注意运行的先后顺序:

def my_metaclass(class_name, parents, attributes):
  print "In metaclass, creating the class."
  return type(class_name, parents, attributes)

def my_class_decorator(class_):
  print "In decorator, chance to modify the class."
  return class_

@my_class_decorator
class C(object):
  __metaclass__ = my_metaclass

  def __init__(self):
    print "Creating object."

c = C()

# Output:
In metaclass, creating the class.
In decorator, chance to modify the class.
Creating object.

元类的一个实际用例

让我们来考虑一个更有用的实例。假设我们正在构思一个类集合来处理MP3音乐文件中使用到的ID3v2标签Wikipedia。简而言之,标签由帧(frames)组成,而每帧通过一个四字符的识别码(identifier)进行标记。举个例子,TOPE标识了原作者帧,TOAL标识了原专辑名称等。如果我们希望为每个帧类型写一个单独的类,并且允许ID3v2标签库用户自定义他们自己的帧类。那么我们可以使用元类来实现一个类工厂模式,具体实现方式可以这样:

frametype_class_dict = {}

class ID3v2FrameClassFactory(object):
  def __new__(cls, class_name, parents, attributes):
    print "Creating class", class_name
    # Here we could add some helper methods or attributes to c
    c = type(class_name, parents, attributes)
    if attributes['frame_identifier']:
      frametype_class_dict[attributes['frame_identifier']] = c
    return c

  @staticmethod
  def get_class_from_frame_identifier(frame_identifier):
    return frametype_class_dict.get(frame_identifier)

class ID3v2Frame(object):
  frame_identifier = None
  __metaclass__ = ID3v2FrameClassFactory
  pass

class ID3v2TitleFrame(ID3v2Frame):
  __metaclass__ = ID3v2FrameClassFactory
  frame_identifier = "TIT2"

class ID3v2CommentFrame(ID3v2Frame):
  __metaclass__ = ID3v2FrameClassFactory
  frame_identifier = "COMM"

title_class = ID3v2FrameClassFactory.get_class_from_frame_identifier('TIT2')
comment_class = ID3v2FrameClassFactory.get_class_from_frame_identifier('COMM')
print title_class
print comment_class

# Output:
Creating class ID3v2Frame
Creating class ID3v2TitleFrame
Creating class ID3v2CommentFrame
<class '__main__.ID3v2TitleFrame'>
<class '__main__.ID3v2CommentFrame'>

当然了,以上的代码同样可以用类装饰器来完成,以下是对应代码:

frametype_class_dict = {}

class ID3v2FrameClass(object):
  def __init__(self, frame_id):
    self.frame_id = frame_id

  def __call__(self, cls):
    print "Decorating class", cls.__name__
    # Here we could add some helper methods or attributes to c
    if self.frame_id:
      frametype_class_dict[self.frame_id] = cls
    return cls

  @staticmethod
  def get_class_from_frame_identifier(frame_identifier):
    return frametype_class_dict.get(frame_identifier)

@ID3v2FrameClass(None)
class ID3v2Frame(object):
  pass

@ID3v2FrameClass("TIT2")
class ID3v2TitleFrame(ID3v2Frame):
  pass

@ID3v2FrameClass("COMM")
class ID3v2CommentFrame(ID3v2Frame):
  pass

title_class = ID3v2FrameClass.get_class_from_frame_identifier('TIT2')
comment_class = ID3v2FrameClass.get_class_from_frame_identifier('COMM')
print title_class
print comment_class

Decorating class ID3v2Frame
Decorating class ID3v2TitleFrame
Decorating class ID3v2CommentFrame
<class '__main__.ID3v2TitleFrame'>
<class '__main__.ID3v2CommentFrame'>

如你所见,我们可以直接给装饰器传递参数,而元类却不能。给元类传递参数必须通过属性。正因如此,这里装饰器的解决方案更为清晰,同时也更容易维护。然而,同时也需要注意当装饰器被调用的时候,类已经建立完毕,这意味着此时就不能够修改其属性了。例如,一旦类建立完成,你就不能够修改__doc__。来看实际例子:

>>> def mydecorator(cls):
...   cls.__doc__ = "Test!"
...   return cls
...
>>> @mydecorator
... class C(object):
...   """Docstring to be replaced with Test!"""
...   pass
...
Traceback (most recent call last):
 File "<stdin>", line 2, in <module>
 File "<stdin>", line 2, in mydecorator
AttributeError: attribute '__doc__' of 'type' objects is not writable
>>> def mymetaclass(cls, parents, attrs):
...   attrs['__doc__'] = 'Test!'
...   return type(cls, parents, attrs)
...
>>> class D(object):
...   """Docstring to be replaced with Test!"""
...   __metaclass__ = mymetaclass
...
>>> D.__doc__
'Test!'

通过type生成元类

正如我们所说,最基本的元类就是type并且类通常都是type类型。那么问题很自然来了,type类型本身是一种什么类型呢?答案也是type。这也就是说type就是它自身的元类。虽然听起来有点诡异,但这在Python解释器层面而言是可行的。

type自身就是一个类,并且我们可以从它继承出新类。这些生成的类也能作为元类,并且使用它们的类可以得到跟使用type一样的类型。来看以下的例子:

>>> class meta(type):
...   def __new__(cls, class_name, parents, attributes):
...       print "meta.__new__"
...       return super(meta, cls).__new__(cls, class_name, parents, attributes)
...   def __call__(self, *args, **kwargs):
...       print "meta.__call__"
...       return super(meta, self).__call__(*args, **kwargs)
...
>>> class C(object):
...   __metaclass__ = meta
...
meta.__new__
>>> c = C()
meta.__call__
>>> type(C)
<class '__main__.meta'>

请注意当类创建对象时,元类的__call__函数就被调用,进而调用type.__call__创建对象。在下一节,我们将把上面的内容融合在一起。
要点集合

假定一个类C自己的元类为my_metaclass并被装饰器my_class_decorator装饰。并且,假定my_metaclass本身就是一个类,从type生成。让我们将上面提到的内容融合到一起做一个总结来显示C类以及它的对象都是怎么被创建的。首先,让我们来看看代码:

class my_metaclass(type):
  def __new__(cls, class_name, parents, attributes):
    print "- my_metaclass.__new__ - Creating class instance of type", cls
    return super(my_metaclass, cls).__new__(cls,
                        class_name,
                        parents,
                        attributes)

  def __init__(self, class_name, parents, attributes):
    print "- my_metaclass.__init__ - Initializing the class instance", self
    super(my_metaclass, self).__init__(self)

  def __call__(self, *args, **kwargs):
    print "- my_metaclass.__call__ - Creating object of type ", self
    return super(my_metaclass, self).__call__(*args, **kwargs)

def my_class_decorator(cls):
  print "- my_class_decorator - Chance to modify the class", cls
  return cls

@my_class_decorator
class C(object):
  __metaclass__ = my_metaclass

  def __new__(cls):
    print "- C.__new__ - Creating object."
    return super(C, cls).__new__(cls)

  def __init__(self):
    print "- C.__init__ - Initializing object."

c = C()
print "Object c =", c

现在,你可以花几分钟时间测试一下你的理解,并且猜一猜打印输出的顺序。

首先,让我们来看看Python的解释器是如何阅读这部分代码的,然后我们会对应输出来加深我们的理解。

1. Python首先看类声明,准备三个传递给元类的参数。这三个参数分别为类名(class_name),父类(parent)以及属性列表(attributs)。

2. Python会检查__metaclass__属性,如果设置了此属性,它将调用metaclass,传递三个参数,并且返回一个类。

3. 在这个例子中,metaclass自身就是一个类,所以调用它的过程类似创建一个新类。这就意味着my_metaclass.__new__将首先被调用,输入四个参数,这将新建一个metaclass类的实例。然后这个实例的my_metaclass.__init__将被调用调用结果是作为一个新的类对象返回。所以此时C将被设置成这个类对象。

4. 接下来Python将查看所有装饰了此类的装饰器。在这个例子中,只有一个装饰器。Python将调用这个装饰器,将从元类哪里得到的类传递给它作为参数。然后这个类将被装饰器返回的对象所替代。

5. 装饰器返回的类类型与元类设置的相同。

6. 当类被调用创建一个新的对象实例时,因为类的类型是metaclass,因此Python将会调用元类的__call__方法。在这个例子中,my_metaclass.__call__只是简单的调用了type.__call__,目的是创建一个传递给它的类的对象实例。

7. 下一步type.__call__通过C.__new__创建一个对象。

8. 最后type.__call__通过C.__new__返回的结果运行C.__init__。

9. 返回的对象已经准备完毕。

所以基于以上的分析,我们可以看到调用的顺序如下:my_metaclass.__new__首先被调用,然后是my_metaclass.__init__,然后是my_class_decorator。至此C类已经准备完毕(返回结果就是C)。当我们调用C来创建一个对象的时候,首先会调用my_metaclass.__call__(任何对象被创建的时候,Python都首先会去调用其类的__call__方法),然后C.__new__将会被type.__call__调用(my_metaclass.__call__简单调用了type.__call__),最后是C.__init__被调用。现在让我们来看看输出:

- my_metaclass.__new__ - Creating class instance of type <class '__main__.my_metaclass'>
- my_metaclass.__init__ - Initializing the class instance <class '__main__.C'>
- my_class_decorator - Chance to modify the class <class '__main__.C'>
- my_metaclass.__call__ - Creating object of type <class '__main__.C'>
- C.__new__ - Creating object.
- C.__init__ - Initializing object.
Object c = <__main__.C object at 0x1043feb90> <class '__main__.C'>

关于元类多说几句

元类,一门强大而晦涩的技法。在GitHub上搜索__metaclass__得到的结果多半是指向”cookbook”或其他Python教学材料的链接。一些测试用例(诸如Jython中的一些测试用例),或是其他一些写有__metaclass__ = type的地方只是为了确保新类被正常使用了。坦白地说,这些用例都没有真正地使用元类。过滤了下结果,我只能找到两个地方真正使用了元类:ABCMeta和djangoplugins。

ABCMeta是一个允许注册抽象基类的元类。如果想了解多些请查看其官方文档,本文将不会讨论它。

对于djangoplugins而言,基本的思想是基于这篇文章article on a simple plugin framework for Python,使用元类是为了创建一个插件挂载系统。我并没有对其有深入的研究,不过我感觉这个功能可以使用装饰器来实现。如果你有相关的想法请在 本文后留言。
总结笔记

通过理解元类能够帮助我们更深入的理解Python中类和对象的行为,现实中使用它们的情况可能比文中的例子要复杂得多。大部分元类完成的功能都可以使用装饰器来实现。所以当你的第一直觉是使用元类来解决你的问题,那么请你停下来先想想这是否必要。如果不是非要使用元类,那么请三思而行。这会使你的代码更易懂,更易调试和维护。

(0)

相关推荐

  • Python使用metaclass实现Singleton模式的方法

    本文实例讲述了Python使用metaclass实现Singleton模式的方法.分享给大家供大家参考.具体实现方法如下: class Singleton(type): def __call__(cls, *args, **kwargs): print "Singleton call" if not hasattr(cls, 'instance'): cls.instance = super(Singleton, cls).__call__(*args, **kwargs) retur

  • 详解python单例模式与metaclass

    单例模式的实现方式 将类实例绑定到类变量上 class Singleton(object): _instance = None def __new__(cls, *args): if not isinstance(cls._instance, cls): cls._instance = super(Singleton, cls).__new__(cls, *args) return cls._instance 但是子类在继承后可以重写__new__以失去单例特性 class D(Singleto

  • 举例讲解Python中metaclass元类的创建与使用

    元类是可以让你定义某些类是如何被创建的.从根本上说,赋予你如何创建类的控制权. 元类也是一个类,是一个type类.   元类一般用于创建类.在执行类定义时,解释器必须要知道这个类的正确的元类,如果此属性没有定义,它会向上查找父类中的__metaclass__属性.如果还没发现,就查找全局变量.   对于传统类来说,它们的元类是types.ClassType.   元类也有构造器,传递三个参数:类名,从基类继承数据的元组,和类属性字典. 下面我们来定义一个元类,要求写类的时候必须给类提供一个__s

  • 深入理解Python中的元类(metaclass)

    译注:这是一篇在Stack overflow上很热的帖子.提问者自称已经掌握了有关Python OOP编程中的各种概念,但始终觉得元类(metaclass)难以理解.他知道这肯定和自省有关,但仍然觉得不太明白,希望大家可以给出一些实际的例子和代码片段以帮助理解,以及在什么情况下需要进行元编程.于是e-satis同学给出了神一般的回复,该回复获得了985点的赞同点数,更有人评论说这段回复应该加入到Python的官方文档中去.而e-satis同学本人在Stack Overflow中的声望积分也高达6

  • Python探索之Metaclass初步了解

    先以一个大牛的一段关于Python Metapgramming的著名的话来做开头: Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't (the people who actually need them know with certainty that they need them, and don't nee

  • Python中的Classes和Metaclasses详解

    类和对象 类和函数一样都是Python中的对象.当一个类定义完成之后,Python将创建一个"类对象"并将其赋值给一个同名变量.类是type类型的对象(是不是有点拗口?). 类对象是可调用的(callable,实现了 __call__方法),并且调用它能够创建类的对象.你可以将类当做其他对象那么处理.例如,你能够给它们的属性赋值,你能够将它们赋值给一个变量,你可以在任何可调用对象能够用的地方使用它们,比如在一个map中.事实上当你在使用map(str, [1,2,3])的时候,是将一个

  • 基于python中的TCP及UDP(详解)

    python中是通过套接字即socket来实现UDP及TCP通信的.有两种套接字面向连接的及无连接的,也就是TCP套接字及UDP套接字. TCP通信模型 创建TCP服务器 伪代码: ss = socket() # 创建服务器套接字 ss.bind() # 套接字与地址绑定 ss.listen() # 监听连接 inf_loop: # 服务器无限循环 cs = ss.accept() # 接受客户端连接 comm_loop: # 通信循环 cs.recv()/cs.send() # 对话(接收/发

  • python中模块的__all__属性详解

    python模块中的__all__属性,可用于模块导入时限制,如: from module import * 此时被导入模块若定义了__all__属性,则只有__all__内指定的属性.方法.类可被导入. 若没定义,则导入模块内的所有公有属性,方法和类 # kk.py class A(): def __init__(self,name,age): self.name=name self.age=age class B(): def __init__(self,name,id): self.nam

  • Python 中迭代器与生成器实例详解

    Python 中迭代器与生成器实例详解 本文通过针对不同应用场景及其解决方案的方式,总结了Python中迭代器与生成器的一些相关知识,具体如下: 1.手动遍历迭代器 应用场景:想遍历一个可迭代对象中的所有元素,但是不想用for循环 解决方案:使用next()函数,并捕获StopIteration异常 def manual_iter(): with open('/etc/passwd') as f: try: while True: line=next(f) if line is None: br

  • Python中%r和%s的详解及区别

    Python中%r和%s的详解 %r用rper()方法处理对象 %s用str()方法处理对象 有些情况下,两者处理的结果是一样的,比如说处理int型对象. 例一: print "I am %d years old." % 22 print "I am %s years old." % 22 print "I am %r years old." % 22 返回结果: I am 22 years old. I am 22 years old. I a

  • Python中的变量和作用域详解

    作用域介绍 python中的作用域分4种情况: L:local,局部作用域,即函数中定义的变量: E:enclosing,嵌套的父级函数的局部作用域,即包含此函数的上级函数的局部作用域,但不是全局的: G:globa,全局变量,就是模块级别定义的变量: B:built-in,系统固定模块里面的变量,比如int, bytearray等. 搜索变量的优先级顺序依次是:作用域局部>外层作用域>当前模块中的全局>python内置作用域,也就是LEGB. x = int(2.9) # int bu

  • python中实现k-means聚类算法详解

    算法优缺点: 优点:容易实现 缺点:可能收敛到局部最小值,在大规模数据集上收敛较慢 使用数据类型:数值型数据 算法思想 k-means算法实际上就是通过计算不同样本间的距离来判断他们的相近关系的,相近的就会放到同一个类别中去. 1.首先我们需要选择一个k值,也就是我们希望把数据分成多少类,这里k值的选择对结果的影响很大,Ng的课说的选择方法有两种一种是elbow method,简单的说就是根据聚类的结果和k的函数关系判断k为多少的时候效果最好.另一种则是根据具体的需求确定,比如说进行衬衫尺寸的聚

  • Python中协程用法代码详解

    本文研究的主要是python中协程的相关问题,具体介绍如下. Num01–>协程的定义 协程,又称微线程,纤程.英文名Coroutine. 首先我们得知道协程是啥?协程其实可以认为是比线程更小的执行单元. 为啥说他是一个执行单元,因为他自带CPU上下文.这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程. 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的. Num02–>协程和线程的差异 那么这个过程看起来和线程差不多.其实不然, 线程切换从系统层面远不止保存和恢复 CP

  • Python中函数参数匹配模型详解

    当我们的函数接收参数为任意个,或者不能确定参数个数时,我们,可以利用 * 来定义任意数目的参数,这个函数调用时,其所有不匹配的位置参数会被赋值为元组,我们可以在函数利用循环或索引进行使用 def f(*args): # 直接打印元组参数 print(args) print('-'*20) # 循环打印元组参数 [print(i) for i in args] ... # 传递一个参数 f(1) print('='*20) # 传递5个参数 f(1, 2, 3, 4, 5) 示例结果: (1,)

  • python中的decimal类型转换实例详解

    [Python标准库]decimal--定点数和浮点数的数学运算 作用:使用定点数和浮点数的小数运算.         Python 版本:2.4 及以后版本 decimal 模块实现了定点和浮点算术运算符,使用的是大多数人所熟悉的模型,而不是程序员熟悉的模型,即大多数计算机硬件实现的 IEEE 浮点数运算.Decimal 实例可以准确地表示任何数,对其上取整或下取整,还可以对有效数字个数加以限制. Decimal 小数值表示为 Decimal 类的实例.构造函数取一个整数或字符串作为参数.使用

随机推荐