Python中的Classes和Metaclasses详解

 类和对象

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

>>> 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中,它能够在一个函数中嵌套出现,举个例子,我们能够这样在函数运行的过程中动态的创建类。看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

>>> 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将扮演元类的角色,基于输入参数创建一个类并返回。输入参数相当简单:类名,父类及其参数的字典。后面两者可以为空,来看一个例子:

1
2
3
4
5
6

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

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

1
2
3
4
5
6
7
8
9

>>> 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__属性。第一个例子,让我们做一些有趣的事情看看我们能够用元类做些什么:

1
2
3
4
5
6
7
8
9
10

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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标签库用户自定义他们自己的帧类。那么我们可以使用元类来实现一个类工厂模式,具体实现方式可以这样:

?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

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__。来看实际例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

>>> 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一样的类型。来看以下的例子:

?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

>>> 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类以及它的对象都是怎么被创建的。首先,让我们来看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

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__被调用。现在让我们来看看输出:

1
2
3
4
5
6
7

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

时间: 2024-11-03 21:50:19

Python中的Classes和Metaclasses详解的相关文章

Python中的Classes和Metaclasses详解_python

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

Python中的深拷贝和浅拷贝详解

  这篇文章主要介绍了Python中的深拷贝和浅拷贝详解,本文讲解了变量-对象-引用.可变对象-不可变对象.拷贝等内容,需要的朋友可以参考下 要说清楚Python中的深浅拷贝,需要搞清楚下面一系列概念: 变量-引用-对象(可变对象,不可变对象)-切片-拷贝(浅拷贝,深拷贝) [变量-对象-引用] 在Python中一切都是对象,比如说:3, 3.14, 'Hello', [1,2,3,4],{'a':1}...... 甚至连type其本身都是对象,type对象 Python中变量与C/C++/Ja

Python中的推导式使用详解

  这篇文章主要介绍了Python中的推导式使用详解,本文分别讲解了列表推导式.字典推导式.集合推导式使用实例,需要的朋友可以参考下 推导式是Python中很强大的.很受欢迎的特性,具有语言简洁,速度快等优点.推导式包括: 1.列表推导式 2.字典推导式 3.集合推导式 嵌套列表推导式 NOTE: 字典和集合推导是最近才加入到Python的(Python 2.7 和Python 3.1以上版). 下面简要介绍下: [列表推导式] 列表推导能非常简洁的构造一个新列表:只用一条简洁的表达式即可对得到

Python中的zipfile模块使用详解

  这篇文章主要介绍了Python中的zipfile模块使用详解,zipfile模块是用来操作zip文件,需要的朋友可以参考下 zip文件格式是通用的文档压缩标准,在ziplib模块中,使用ZipFile类来操作zip文件,下面具体介绍一下: class zipfile.ZipFile(file[, mode[, compression[, allowZip64]]]) 创建一个ZipFile对象,表示一个zip文件.参数file表示文件的路径或类文件对象(file-like object);参

Python中tell()方法的使用详解

  这篇文章主要介绍了Python中tell()方法的使用详解,是Python入门学习中的基础知识,需要的朋友可以参考下 tell()方法返回的文件内的文件读/写指针的当前位置. 语法 以下是tell()方法的语法: ? 1 fileObject.tell() 参数 NA 返回值 此方法返回该文件中读出的文件/写指针的当前位置. 例子 下面的例子显示了tell()方法的使用. ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #

Python中的rfind()方法使用详解

  这篇文章主要介绍了Python中的rfind()方法使用详解,是Python入门中的基础知识,需要的朋友可以参考下 rfind()方法返回所在子str 被找到的最后一个索引,或者-1,如果没有这样的索引不存在,可选择限制搜索字符串string[beg:end]. 语法 以下是rfind()方法的语法: ? 1 str.rfind(str, beg=0 end=len(string)) 参数 str -- 此选项指定要搜索的字符串 beg -- 这是开始索引,默认情况下为 0 end -- 这

Python中的rjust()方法使用详解

  这篇文章主要介绍了Python中的rjust()方法使用详解,是Python学习入门中的基础知识,需要的朋友可以参考下 rjust()该方法返回字符串合理字符串的右边的长度宽度.填充是通过使用指定的fillchar(默认为空格).如果宽度小于len(s)返回原始字符串. 语法 以下是rjust()方法的语法: ? 1 str.rjust(width[, fillchar]) 参数 width -- 这是字符串填充后总共的长度. fillchar -- 这是填充字符,默认为空格. 返回值 此方

Python中的choice()方法使用详解

 这篇文章主要介绍了Python中的choice()方法使用详解,是Python入门中的基础知识,需要的朋友可以参考下     choice()方法从一个列表,元组或字符串返回一个随机项. 语法 以下是choice()方法的语法: ? 1 choice( seq ) 注意:此函数是无法直接访问的,所以我们需要导入random模块,然后我们需要使用random对象来调用这个函数. 参数 seq -- 这可能是一个列表,元组或字符串... 返回值 该方法返回一个随机项. 例子 下面的例子显示了cho

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

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