Python中的Classes和Metaclasses详解


Posted in Python onApril 02, 2015

类和对象

类和函数一样都是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中类和对象的行为,现实中使用它们的情况可能比文中的例子要复杂得多。大部分元类完成的功能都可以使用装饰器来实现。所以当你的第一直觉是使用元类来解决你的问题,那么请你停下来先想想这是否必要。如果不是非要使用元类,那么请三思而行。这会使你的代码更易懂,更易调试和维护。

Python 相关文章推荐
Python中为feedparser设置超时时间避免堵塞
Sep 28 Python
python求解水仙花数的方法
May 11 Python
浅谈Python 字符串格式化输出(format/printf)
Jul 21 Python
Python变量和字符串详解
Apr 29 Python
django 常用orm操作详解
Sep 13 Python
numpy数组拼接简单示例
Dec 15 Python
tensorflow创建变量以及根据名称查找变量
Mar 10 Python
Python中filter与lambda的结合使用详解
Dec 24 Python
Python Opencv中用compareHist函数进行直方图比较对比图片
Apr 07 Python
keras绘制acc和loss曲线图实例
Jun 15 Python
Python自动化之UnitTest框架实战记录
Sep 08 Python
Python截图并保存的具体实例
Jan 14 Python
详解Python中的装饰器、闭包和functools的教程
Apr 02 #Python
详解Python的迭代器、生成器以及相关的itertools包
Apr 02 #Python
用Python实现通过哈希算法检测图片重复的教程
Apr 02 #Python
仅用500行Python代码实现一个英文解析器的教程
Apr 02 #Python
python下载文件时显示下载进度的方法
Apr 02 #Python
Python使用正则匹配实现抓图代码分享
Apr 02 #Python
用Python展示动态规则法用以解决重叠子问题的示例
Apr 02 #Python
You might like
咖啡豆的最常见发酵处理方法,详细了解一下
2021/03/03 冲泡冲煮
phplock(php进程锁) v1.0 beta1
2009/11/24 PHP
解析zend studio中直接导入svn中的项目的方法步骤
2013/06/21 PHP
PHP数组与对象之间使用递归实现转换的方法
2015/06/24 PHP
JavaScript中的集合及效率
2010/01/08 Javascript
JavaScript和JQuery实用代码片段(一)
2010/04/07 Javascript
JQuery实现的在新窗口打开链接的方法小结
2010/04/22 Javascript
javascript string字符串优化问题
2011/07/31 Javascript
jQuery EasyUI API 中文文档 - Calendar日历使用
2011/10/19 Javascript
JS根据变量保存方法名并执行方法示例
2014/04/04 Javascript
从零学JSON之JSON数据结构
2014/05/19 Javascript
jQuery的css() 方法使用指南
2015/05/03 Javascript
深入浅析同源策略和跨域访问
2015/11/26 Javascript
jQuery模拟select实现下拉菜单功能
2016/06/20 Javascript
浅谈react+es6+webpack的基础配置
2017/08/09 Javascript
图片懒加载imgLazyLoading.js使用详解
2020/09/15 Javascript
在vue+element ui框架里实现lodash的debounce防抖
2019/11/13 Javascript
js重写alert事件(避免alert弹框标题出现网址)
2020/12/04 Javascript
[02:25]DOTA2英雄基础教程 生死判决瘟疫法师
2013/12/06 DOTA
[34:39]DOTA2上海特级锦标赛主赛事日 - 4 败者组第四轮#1COL VS EG第二局
2016/03/05 DOTA
python数据库操作常用功能使用详解(创建表/插入数据/获取数据)
2013/12/06 Python
让 python 命令行也可以自动补全
2014/11/30 Python
python实现的简单猜数字游戏
2015/04/04 Python
简单介绍Python下自己编写web框架的一些要点
2015/04/29 Python
一步步解析Python斗牛游戏的概率
2016/02/12 Python
在windows下使用python进行串口通讯的方法
2019/07/02 Python
在tensorflow中实现屏蔽输出的log信息
2020/02/04 Python
Python3将ipa包中的文件按大小排序
2020/04/17 Python
python 实现一个图形界面的汇率计算器
2020/11/09 Python
2014年村党支部工作总结
2014/12/04 职场文书
教师节简报
2015/07/20 职场文书
2019个人年度目标制定攻略!
2019/07/12 职场文书
导游词之永泰公主墓
2019/12/04 职场文书
python实现监听键盘
2021/04/26 Python
安装配置mysql及Navicat prenium的详细流程
2021/06/10 MySQL
Python数据处理的三个实用技巧分享
2022/04/01 Python