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中统计函数运行耗时的方法
May 05 Python
win系统下为Python3.5安装flask-mongoengine 库
Dec 20 Python
python简单商城购物车实例代码
Mar 15 Python
python 获取指定文件夹下所有文件名称并写入列表的实例
Apr 23 Python
浅谈tensorflow1.0 池化层(pooling)和全连接层(dense)
Apr 27 Python
Pandas 数据处理,数据清洗详解
Jul 10 Python
VPS CENTOS 上配置python,mysql,nginx,uwsgi,django的方法详解
Jul 01 Python
快速查找Python安装路径方法
Feb 06 Python
python3 deque 双向队列创建与使用方法分析
Mar 24 Python
5款实用的python 工具推荐
Oct 13 Python
python爬虫中PhantomJS加载页面的实例方法
Nov 12 Python
浅析pandas随机排列与随机抽样
Jan 22 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
PHP操作数组相关函数
2011/02/03 PHP
JScript中的&quot;this&quot;关键字使用方式补充材料
2007/03/08 Javascript
javascript IE中的DOM ready应用技巧
2008/07/23 Javascript
数组Array进行原型prototype扩展后带来的for in遍历问题
2010/02/07 Javascript
jquery-easyui关闭tab自动切换到前一个tab
2010/07/29 Javascript
JS替换字符串中空格方法
2015/04/17 Javascript
jqGrid中文文档之选项设置
2015/12/02 Javascript
JQuery datepicker 用法详解
2015/12/25 Javascript
js动态添加的DIV中的onclick事件简单实例
2016/07/25 Javascript
JS正则表达式修饰符global(/g)用法分析
2016/12/27 Javascript
JS倒计时实例_天时分秒
2017/08/22 Javascript
浅谈Vuejs中nextTick()异步更新队列源码解析
2017/12/31 Javascript
React-native桥接Android原生开发详解
2018/01/17 Javascript
Vue动态生成el-checkbox点击无法赋值的解决方法
2019/02/21 Javascript
JS制作简易计算器的实例代码
2020/07/04 Javascript
JavaScript实现点击切换功能
2021/01/27 Javascript
Python实现树的先序、中序、后序排序算法示例
2017/06/23 Python
Python RabbitMQ消息队列实现rpc
2018/05/30 Python
Appium+Python自动化测试之运行App程序示例
2019/01/23 Python
tensorflow 获取所有variable或tensor的name示例
2020/01/04 Python
Pycharm中Python环境配置常见问题解析
2020/01/16 Python
Python使用pyyaml模块处理yaml数据
2020/04/14 Python
Python tkinter制作单机五子棋游戏
2020/09/14 Python
Flask-SocketIO服务端安装及使用代码示例
2020/11/26 Python
澳大利亚婴儿、幼儿和儿童在线设计师商店:Smooch Baby
2019/02/16 全球购物
四下基层实施方案
2014/03/28 职场文书
推广普通话共筑中国梦演讲稿
2014/09/21 职场文书
世界红十字日活动总结
2015/02/10 职场文书
简历自荐信范文
2015/03/09 职场文书
自我评价优缺点范文
2015/03/11 职场文书
2015年数学教师工作总结
2015/05/20 职场文书
2015年秋季开学典礼校长致辞
2015/07/16 职场文书
防震减灾主题班会
2015/08/14 职场文书
《狼王梦》读后感:可怜天下父母心
2019/11/01 职场文书
Nginx反向代理学习实例教程
2021/10/24 Servers
Element-ui Layout布局(Row和Col组件)的实现
2021/12/06 Vue.js