Python MetaClass深入分析

alpha_panda發表於2018-10-26

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
<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可以通過指定:

  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):<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原始碼來更深入的學習元類,畢竟原始碼面前了無祕密。

但是我們學習的目的就是掌握知識來解決實際問題的,畢竟要先學會使用麼。等到了一定程度需要的時候在閱讀原始碼或許會有更好的效果。

相關文章