Python MetaClass深入分析

python元类是比较难理解和使用的。但是在一些特定的场合使用MetaClass又非常的方便。本文本着先拿来用的精神,将对元类的概念作简要介绍,并通过深入分析一个元类的例子,来体会其功能,并能够在实际需要时灵活运用。

首先,先了解一下必要的知识点。

1. 函数__new__和__init__

元类的实现可以使用这两个函数。在创建类的过程中会调用这两个函数,类定义中这两个函数可有可无。具体可参照官网 Basic customization 

先来简要说明一下两者的区别:

  1. __new__ 是在__init__之前被调用的特殊方法
  2. __new__是用来创建当前类的对象并返回,然后会自动调用__init__函数
  3. 如果自定义了__new__函数但是没有返回值,那么不会调用该类的__init__的函数
  4. 而__init__只是创建类对象过程中根据调用者传入的参数初始化对象
  5. 在__new__函数中可以控制并定义类的生成,这个一般可以通过在类中定义静态数据成员和成员函数的方式实现,因此很少用
  6. 如果生成的对象是类类型的话,__new__可以根据实际的需要进行类的定制并控制类的生成过程
  7. 如果你希望的话,也可以在__init__中做些事情
  8. 还有一些高级的用法会涉及到改写__call__特殊方法,定义有该函数会让对象具有可调用性,但是__call__函数并不牵涉到类对象的生成过程

通过下面的这个例子可以简单的介绍一下这两个函数:

 1 class MTC(object):
 2     STATIC_MEMBER = "STATIC MEMBER of MTC"
 3 
 4     def __new__(cls, *args, **kwargs):
 5         print "this is MTC __new__ func"
 6         print cls, args, kwargs
 7         cls.NEW_STATIC_MEMBER = 'NEW STATIC MEMBER of MTC'
 8         cls.test_func = lambda self, x = 'args': x
 9         return super(MTC, cls).__new__(cls, *args, **kwargs)
10 
11     def __init__(self, *args, **kwargs):
12         print "this is MTC __init__ func"
13         print self, args, kwargs
14 
15 init_val = (1,2,3,4)
16 instance = MTC(*init_val, my_key = 'my_value')
17 print instance.NEW_STATIC_MEMBER
18 print instance.test_func('This func added in __new__ func!')

运行结果:

this is MTC __new__ func
(1, 2, 3, 4) {'my_key': 'my_value'}
this is MTC __init__ func
<__main__.MTC object at 0x029E5CD0> (1, 2, 3, 4) {'my_key': 'my_value'}
NEW STATIC MEMBER of MTC
This func added in __new__ func!

函数__new__可以使我们动态的定义类或者修改类的某些属性。实际定义Class很少使用到函数__new__,因为绝大多数的时候我们可以直接在定义类时修改类的定义,而不会使用到这个函数的一些特性。

2. 关于MetaClass

MetaClass是一个较为抽象的概念,可以从一个简单的角度来理解,否者还没讲明白,自己先绕晕了。先看一下官方给出的术语解释。metaclass

The class of a class. Class definitions create a class name, a class dictionary, and a list of base classes. The metaclass is responsible for taking those three arguments and creating the class. Most object oriented programming languages provide a default implementation. What makes Python special is that it is possible to create custom metaclasses. Most users never need this tool, but when the need arises, metaclasses can provide powerful, elegant solutions. They have been used for logging attribute access, adding thread-safety, tracking object creation, implementing singletons, and many other tasks.

意思是说metaclass可以通过指定:

  1. class name
  2. class dictionary
  3. a list of base classes

来改变类的默认生成方式,进行类的自定义。

简单来说就是MetaClass是用来创建类的,就好比类是用来创建对象的。如果说类是对象的模板,那么metaclass就是类的模板。

关于MetaClass是如何创建类的,可以参考官网简单精炼的解释:Customizing-class-creation

MetaClass作为创建类的类,可以通过定义__new__和__init__来分别创建类对象和初始化(修改)类的属性。实际定义metaclass的过程中只需要实现二者中的一个即可。

3. MetaClass应用

MetaClass可以应用于需要动态的根据输入参数创建类的场景。

The potential uses for metaclasses are boundless. Some ideas that have been explored including logging, interface checking, automatic delegation, automatic property creation, proxies, frameworks, and automatic resource locking/synchronization.

我们实际可能遇到这样一个场景,有一个类有若干个相互独立的属性集。我们可以使用组合(component)的方式来创建该类。

但是继续思考该类的设计,常见的组合有两种:

  1. 直接将需要的属性全部作为新组合类的成员列出来;
  2. 分别将各个独立的属性集定义成单个的类,然后通过在组合类添加每个属性类的实例(instance)的方式来引用各个属性类中定义的属性;

实际上第二种方式使用的较为广泛,因为相比于第一种方式,通过拆分的方式实现,降低了代码的耦合性,提高了可维护性,更便于代码的阅读。

但是实际上第二种方式也有其的缺陷:

  • 因为作为组合类的属性,我们应该可以通过组合类的一个实例来直接访问其属性,现在需要通过一个间接proxy来访问其属性,显然并不直观。
  • 最为关键的问题是我们人为的拆分一个类的属性,导致在一个属性类中无法访问其他属性类中的成员,也就是属性类之间是不可见的。

那python中有没有一种方式可以以自动添加的方式将各个属性类中定义的成员全部都绑定到组合类中?答案便是MetaClass.

 下面我们通过介绍一个例子来说明如何使用元类的方式将各个类的属性绑定到组合类中。

3.1 使用__init__修改类属性

考虑一个房屋的构成,我们先假设房屋的组成为Wall和Door,下面简单定义他们的属性

 1 class Wall(object):
 2     STATIC_WALL_ATTR = "static wall"
 3 
 4     def init_wall(self):
 5         self.wall = "attr wall"
 6     
 7     def wall_info(self):
 8         print "this is wall of room"
 9     
10     @staticmethod
11     def static_wall_func():
12         print 'static wall info'
13 
14 class Door(object):
15 
16     def init_door(self):
17         self.door = "attr door"
18         
19     def door_info(self):
20         print "this is door of room"
21         print self.door, self.wall, self.STATIC_WALL_ATTR

下面定义元类和房屋:

 1 import inspect, sys, types
 2 
 3 class metaroom(type):
 4     meta_members = ('Wall', "Door")
 5     exclude_funcs = ('__new__', '__init__')
 6     attr_types = (types.IntType, basestring, types.ListType, types.TupleType, types.DictType)
 7 
 8     def __init__(cls, name, bases, dic):
 9         super(metaroom, cls).__init__(name, bases, dic)  # type.__init__(cls, name, bases, dic)
10         for cls_name in metaroom.meta_members:
11             cur_mod = sys.modules[__name__]
12             # cur_mod = sys.modules[metaroom.__module__]
13             cls_def = getattr(cur_mod, cls_name)
14             for func_name, func in inspect.getmembers(cls_def, inspect.ismethod):
15                 # 添加成员函数
16                 if func_name not in metaroom.exclude_funcs:
17                     assert not hasattr(cls, func_name), func_name
18                     setattr(cls, func_name, func.im_func)
19             for attr_name, value in inspect.getmembers(cls_def):
20                 # 添加静态数据成员
21                 if isinstance(value, metaroom.attr_types) and attr_name not in ('__module__', '__doc__'):
22                     assert not hasattr(cls, attr_name), attr_name
23                     setattr(cls, attr_name, value)

下面是房屋Room的定义:

 1 class Room(object):
 2     __metaclass__ = MetaRoom
 3 
 4     def __init__(self):
 5         self.room = "attr room"
 6         # print self.__metaclass__.meta_members
 7         self.add_cls_member()
 8     
 9     def add_cls_member(self):
10         """ 分别调用各个组合类中的init_cls_name的成员函数 """
11         for cls_name in self.__metaclass__.meta_members:
12             init_func_name = "init_%s" % cls_name.lower()
13             init_func_imp = getattr(self, init_func_name, None)
14             if init_func_imp:
15                 init_func_imp()

 我们看一下Class Room的属性列表:

['STATIC_WALL_ATTR', '__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__metaclass__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'add_cls_member', 'door_info', 'init_door', 'init_wall', 'wall_info']

作为区分,再看一下Room的实例的属性列表:

['STATIC_WALL_ATTR', '__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__metaclass__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'add_cls_member', 'door', 'door_info', 'init_door', 'init_wall', 'room', 'wall', 'wall_info']

这样我们便将类Wall和Door的属性绑定到了Room。如果后面新加属性,比如Window、Floor等,只需要各自实现其定义然后添加到metaroom的meta_members列表中,新定义的属性便可直接在Room中访问。

此外,这些属性类也可以直接访问其他属性类中定义的属性,比如我们在Class Door中可以直接通过self.wall和self.wall_info()的方式获取房屋Wall相关的属性。

注意:

  1. 被绑定到组合类中的各个子类如果直接访问其他的子类的属性,显然该子类将无法单独作为类创建对象;
  2. 通过元类metaroom,我们只能将Class Wall和 Door中的成员函数和静态数据成员绑定到了Class Room,因为在创建对象前无法访问类的非静态数据成员;
  3. 需要约定一种方式(某种样式的函数定义,如上面定义的init_cls_name),在创建组合类对象过程中,将所有子类中非静态数据成员绑定到组合类对象中;
  4. 上面属性类中定义的函数init_cls_name之外新绑定的数据成员将无法在该类之外被访问,因为其并没有绑定到组合类对象中;
  5. 在setattr(cls, func_name, func.im_func)中func是绑定函数,func.im_func的实际上相当于将原类中的成员函数解绑,然后绑定到组合类中,这样非静态成员函数中的self参数实际上表示的是新的组合类对象;
  6. 静态成员函数是无法进行绑定的;
  7. 元类metaroom中的函数__init__(cls, name, bases, dic)的参数分别表示:cls(创建的类Room),name(类Room的名称),bases(类Room的基类列表),dict(类Room的属性)

读到这里自然而然的会有这样一个问题,有没有什么方法将原类中的staticmethod和classmethod绑定到组合类当中。下面给出代码,可以花时间好好思考一下为什么可以这样写。

 1 class MetaRoom(type):
 2     ... ...
 3     def __init__(cls, name, bases, dic):
 4     ... ...
 5     for cls_name in MetaRoom.meta_members:
 6             ... ...
 7             for func_name, func in inspect.getmembers(cls_def, inspect.ismethod):
 8                 # 添加成员函数
 9                 if func_name not in MetaRoom.exclude_funcs:
10                     if func.im_self:
11                         # 添加原类中定义的classmethod
12                         setattr(cls, func_name, classmethod(func.im_func))
13                     else:
14                         setattr(cls, func_name, func.im_func)
15         ... ...
16             for func_name, func in inspect.getmembers(cls_def, inspect.isfunction):
17                 # 添加静态成员函数
18                 assert not hasattr(cls, func_name), func_name
19                 setattr(cls, func_name, staticmethod(func))    

 3.2 使用__new__指定类的属性

 下面使用__new__函数来重写一下元类MetaRoom。 

如果说使用__init__函数是通过动态修改类属性的方式来定制类,那么使用__new__函数则是在类创建之前通过指定其属性列表的方式来创建类。

从本文最初对两个函数的介绍也可以看出,函数__new__返回被创建的对象,而后会自动调用__init__函数对__new__返回的对象根据传入的参数进行初始化。只不过元类中两个函数分别创建和初始化的是类。

 1 class MetaRoom(type):
 2     meta_members = ('Wall', "Door")
 3     exclude_funcs = ('__new__', '__init__')
 4     attr_types = (types.IntType, basestring, types.ListType, types.TupleType, types.DictType)
 5 
 6     def __new__(typ, name, bases, dic):
 7         for cls_name in MetaRoom.meta_members:
 8             cur_mod = sys.modules[__name__]
 9             cls_def = getattr(cur_mod, cls_name)
10             for func_name, func in inspect.getmembers(cls_def, inspect.ismethod):
11                 if func_name not in MetaRoom.exclude_funcs:
12                     assert func_name not in dic, func_name
13                     dic[func_name] = func.im_func
14             for attr_name, value in inspect.getmembers(cls_def):
15                 if isinstance(value, MetaRoom.attr_types) and attr_name not in('__module__', '__doc__'):
16                     assert attr_name not in dic, attr_name
17                     dic[attr_name] = value
18             dic['room_mem_func'] = lambda self, x: x
19             dic['STATIC_ROOM_VAR'] = 'static room var'
20         return type.__new__(typ, name, bases, dic)
21         # return super(MetaRoom, typ).__new__(name, bases, dic)

在这段代码中我们额外添加了两个属性:

  1. dic['room_mem_func'] = lambda self, x: x
  2. dic['STATIC_ROOM_VAR'] = 'static room var'

其实是为了更直观的说明我们在属性字典中指定的属性,最终会成为被创建类的数据成员和成员函数。可以通过在类Room中定义静态数据成员STATIC_ROOM_VAR和成员函数room_mem_func的方式达到同样的效果。

1 class Room(object):
2     __metaclass__ = MetaRoom
3     STATIC_ROOM_VAR = "static room var"
4 
5     def room_mem_func(self, x):
6         return x
7 
8     ... ...

元类中通过修改属性列表dic的方式添加的成员为:静态数据成员和非静态成员函数。这个地方可能会有一些疑问。

静态数据成员可能会比较好理解一些,因为我们创建的是类,只能看到类的属性,非静态数据成员只和类对象有关系。

那为什么在dic中添加的函数是绑定到类实例的成员函数,而不是只和类相关的staticmethod。这个暂时没有很好的解释,唯一可以合理说明的可能只有使用显示的@staticmethod装饰的函数才会被作为类的静态成员函数。

如果在类Room中在添加定义一个静态成员函数和一个类函数:

 1 class Room(object):
 2     __metaclass__ = MetaRoom
 3     ... ...
 4     
 5     @staticmethod
 6     def room_static_func():
 7         print 'This is room static func'
 8     
 9     @classmethod
10     def room_cls_func(cls):
11         print 'This is room cls func'

那么在元类metaroom的函数__new__返回前,程序实际运行过程中获取的dic中的属性列表如下:

'STATIC_ROOM_VAR' (55094792):'static room var'
'STATIC_WALL_ATTR' (55094272):'static wall'
'__init__' (5497536):__init__ at 0x03493470>
'__metaclass__' (6049648):<class '__main__.MetaRoom'>
'__module__' (5499680):'__main__'
'add_cls_member' (55094592):
'door_info' (55337408):
'init_door' (55337312):
'init_wall' (55337152):
'room_cls_func' (55094752):
'room_mem_func' (55094832):lambda> at 0x034936F0>
'room_static_func' (55094712):
'wall_info' (55337248):
__len__:13

上面十三个属性中除了'__module__'之外,其余均为我们自定义的属性。

创建的Class Room完整的属性列表如下:

['STATIC_ROOM_VAR', 'STATIC_WALL_ATTR', '__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__metaclass__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'add_cls_member', 'door_info', 'init_door', 'init_wall', 'room_cls_func', 'room_mem_func', 'room_static_func', 'wall_info']

对比一下两种方式,最直接也是最根本的差别就是:

  • __new__是在生成类之前通过修改其属性列表的方式来控制类的创建,此时类还没有被创建;
  • __init__是在__new__函数返回被创建的类之后,通过直接增删类的属性的方式来修改类,此时类已经被创建;
  • __new__函数的第一个参数typ代表的是元类MetaRooM(注意不是元类的对象);
  • __init__函数的第一个参数cls表示的是类Room,也就是元类MetaRoom的一个实例(对象);

为了更好的理解通过metaclass的方式创建类,强烈建议使用熟悉的Python IDE通过设置断点的方式来查看元类metaroom创建room的过程。

通过上面的这个例子应该能够比较清楚的理解元类创建类的过程,平时工作中能够在需要的时候灵活的使用元类处理需求可以达到事半功倍的效果。

如果还觉得不够透彻,可以参照python源码来更深入的学习元类,毕竟源码面前了无秘密。

但是我们学习的目的就是掌握知识来解决实际问题的,毕竟要先学会使用么。等到了一定程度需要的时候在阅读源码或许会有更好的效果。

你可能感兴趣的:(Python MetaClass深入分析)