python元類是比較難理解和使用的。但是在一些特定的場合使用MetaClass又非常的方便。本文本着先拿來用的精神,將對元類的概念作簡要介紹,並通過深入分析一個元類的例子,來體會其功能,並能夠在實際需要時靈活運用。
首先,先了解一下必要的知識點。
1. 函數__new__和__init__
元類的實現可以使用這兩個函數。在創建類的過程中會調用這兩個函數,類定義中這兩個函數可有可無。具體可參照官網 Basic customization
先來簡要說明一下兩者的區別:
- __new__ 是在__init__之前被調用的特殊方法
- __new__是用來創建當前類的對象並返回,然后會自動調用__init__函數
- 如果自定義了__new__函數但是沒有返回值,那么不會調用該類的__init__的函數
- 而__init__只是創建類對象過程中根據調用者傳入的參數初始化對象
- 在__new__函數中可以控制並定義類的生成,這個一般可以通過在類中定義靜態數據成員和成員函數的方式實現,因此很少用
- 如果生成的對象是類類型的話,__new__可以根據實際的需要進行類的定制並控制類的生成過程
- 如果你希望的話,也可以在__init__中做些事情
- 還有一些高級的用法會涉及到改寫__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
<class '__main__.MTC'> (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可以通過指定:
- class name
- class dictionary
- 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)的方式來創建該類。
但是繼續思考該類的設計,常見的組合有兩種:
- 直接將需要的屬性全部作為新組合類的成員列出來;
- 分別將各個獨立的屬性集定義成單個的類,然后通過在組合類添加每個屬性類的實例(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相關的屬性。
注意:
- 被綁定到組合類中的各個子類如果直接訪問其他的子類的屬性,顯然該子類將無法單獨作為類創建對象;
- 通過元類metaroom,我們只能將Class Wall和 Door中的成員函數和靜態數據成員綁定到了Class Room,因為在創建對象前無法訪問類的非靜態數據成員;
- 需要約定一種方式(某種樣式的函數定義,如上面定義的init_cls_name),在創建組合類對象過程中,將所有子類中非靜態數據成員綁定到組合類對象中;
- 上面屬性類中定義的函數init_cls_name之外新綁定的數據成員將無法在該類之外被訪問,因為其並沒有綁定到組合類對象中;
- 在setattr(cls, func_name, func.im_func)中func是綁定函數,func.im_func的實際上相當於將原類中的成員函數解綁,然后綁定到組合類中,這樣非靜態成員函數中的self參數實際上表示的是新的組合類對象;
- 靜態成員函數是無法進行綁定的;
- 元類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)
在這段代碼中我們額外添加了兩個屬性:
- dic['room_mem_func'] = lambda self, x: x
- 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):<function __init__ at 0x03493470> '__metaclass__' (6049648):<class '__main__.MetaRoom'> '__module__' (5499680):'__main__' 'add_cls_member' (55094592):<function add_cls_member at 0x03493130> 'door_info' (55337408):<function door_info at 0x034933B0> 'init_door' (55337312):<function init_door at 0x03493370> 'init_wall' (55337152):<function init_wall at 0x03493770> 'room_cls_func' (55094752):<classmethod object at 0x034C6DB0> 'room_mem_func' (55094832):<function <lambda> at 0x034936F0> 'room_static_func' (55094712):<staticmethod object at 0x034C6D90> 'wall_info' (55337248):<function wall_info at 0x034937B0> __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源碼來更深入的學習元類,畢竟源碼面前了無秘密。
但是我們學習的目的就是掌握知識來解決實際問題的,畢竟要先學會使用么。等到了一定程度需要的時候在閱讀源碼或許會有更好的效果。