Python之元類筆記
轉載:http://kissg.me/2016/04/25/python-metaclass/
引文
自上一次寫部落格到現在已經過去整整15天了.這期間,我看過許多材料,也有許多想付諸筆端與大家分享的.但苦於前人幾乎已經把該講的不該講的都講了,而且講得非常透徹,鞭僻入裡,有他們的珠玉在前,加上我又希望堅持原創,一時竟不知從何落筆了.思前想後,不如就寫一篇”讀書筆記”吧.於是就選定了關於python3 metaclass
的這篇文章(中文版請看這裡).(為敘述方便,後文用“原文”來指代這篇文章)
正文
按照慣例,先介紹一些預備知識,以便讀者能更好地理解.
首先,我想簡單講下關鍵字
(keyword)的概念(有時也叫保留字(reserved word))
關鍵字,是程式語言的一類語法結構.它們在語言設計之初就被定義了.
說白了,關鍵字就是程式語言已經為我們準備好的工具,我們可以直接拿過來用.舉一個例子,比如,要定義一個類,我們會這樣寫:
>>> class Myclass(object):
... pass
此處class
關鍵字就相當於告訴python直譯器:”直譯器大哥,使用者自定義了一個類,類名就叫Myclass,引數是…麻煩您給構造一下”.然後,Myclass類就自動被建立了.
另一個需要澄清的概念是動態程式語言
(dynamic programming language)
動態程式語言,是一類在執行時可以改變程式結構的語言,例如新的函式,物件甚至程式碼可以被引進,已有的函式可以被刪除或者其他結構上的變化.
下面是一個簡單的例子:
>>> class Myclass(object):
... pass
...
>>> mc = Myclass()
>>> mc.name = "kissg" # Myclass類本身並沒有name屬性,在執行期間為其新增了name屬性
>>> print(mc.name)
kissg
(注:動態程式語言 != 動態型別語言.動態型別語言是指在執行時確定變數的型別)
眾所周知,python是物件導向的程式語言.對此,我們要清楚並始終牢記一點:
在python的世界裡,一切皆為物件.
整型浮點型字串型變數是物件,函式也是物件,類還是物件。只不過,類作為物件有點特殊,它是自身具有建立物件(類例項)能力的物件.
那麼,類是一個物件有何意義呢?這意味著,我們完全可以像操縱普通物件一樣操縱一個類:
- 可以將它賦給一個變數
- 可以對它進行拷貝
- 可以為它增加屬性
- 可以將它作為引數傳遞給某個函式
正是由於python的這些特點,為動態程式設計提供一個可能,一種思路。
前文已經提到,使用class
關鍵字,python直譯器就為我們自動地建立了一個類。其實,我們還可以手動地建立一個類,即呼叫type
函式。
實際上,當我們使用class
關鍵字定義好一個類,python直譯器就是通過呼叫type
函式來構造類的,它以我們寫好的類定義(包括類名,父類,屬性)作為引數,並返回一個類。官方文件對此描述如下:
class type(name, bases, dict)
With three arguments, return a new type object. This is essentially a dynamic form of the class statement. The name string is the class name and becomes the __name__ attribute; the bases tuple itemizes the base classes and becomes the__bases__ attribute; and the dict dictionary is the namespace containing definitions for class body and becomes the __dict__ attribute.
以下2種方法建立類的方法完全相同。
>>> class X(object):
... a = 1
...
>>> X = type('X', (object,), dict(a=1))
那麼,如何為動態建立的類新增method(為了不混淆視聽,此處用method來表示類的方法)呢?有兩種方法,一種是在建立類的時候指定,一種是在後期動態地新增。方法與前文介紹的新增屬性基本類似。實際上,完全可以將method看作是特殊一點的屬性,這樣,處理method的時候,想想屬性是如何處理的,就會簡單許多。
# 動態建立類時,指定method
>>> def echo_bar(self):
... print(self.bar)
...
>>> Foo = type('Foo', (object,), {'echo_bar': echo_bar})
#=======================================================
# 動態地新增method
>>> def echo_bar_more(self):
... print('yet another method')
...
>>> Foo.echo_bar_more = echo_bar_more
值得注意的是,我們使用class
關鍵字定義類的時候,method的第一個引數一般總是self
,它指向呼叫method的物件(類例項)本身。因此,在我們動態地新增method之前,定義函式時,千萬別忘了self
關鍵字。否則,錯誤將超乎你的想象.(思考一下,這個method並沒有繫結到類例項上,這是你想要的效果嗎?如果是,當我沒說)
細心的同學可能已經發現了,type(name, bases, dict)
其實是一個建構函式(見上文官方文件的引用:
return a new type object
)。而我之前卻說,它返回一個類。其實,一個類就是一個type
的物件。這個結果不算驚世駭俗:我們已經知道類例項是由類建立的一個物件,那麼既然類也是一個物件,理應有一個更加強大的存在能夠建立類。我們稱這個更加強大的存在為元類(metaclass)
,即類的類。
(注如果你在其他地方接觸過“元xx”,應該很容易就能理解。比如“後設資料”就是描述資料的資料)
無疑,type
就是一個元類,並且它還是所有類的元類:
# 通過__class__屬性可獲得物件的類
>>> age = 35
>>> age.__class__
<type 'int'>
>>> name = 'bob'
>>> name.__class__
<type 'str'>
>>> def foo(): pass
>>>foo.__class__
<type 'function'>
>>> class Bar(object): pass
> b = Bar()
>>> b.__class__
<class '__main__.Bar'>
# 通過檢視物件的__class__.__class__,可以看出所有類的類都是type
>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> b.__class__.__class__
<type 'type'>
現在你知道,為什麼是type
,而不是Type
了吧。(提示,對比下str
,int
你就懂啦)
注意到,上面的程式碼段有一句
<type 'function'>
,這表示存在一個內建的function
類
所以為什麼說函式也是一個物件,因為它們都是function類的一個例項。
聯絡關鍵字
的知識,當我們使用def
關鍵字時,就是在告訴python直譯器“請給我一個function例項”
除了使用type(name, bases, dict)
來動態地建立類外,我們還可以自定義元類,並用自定義的元類來控制類的建立行為。
比如說,我希望類的所有屬性都加上字首kissg_
。當然我可以在定義類的時候為每個屬性手動地加上kiss_
字首。而另一種行之有效的方法就是自定義一個元類,由它在建立類的時候,自動地給每個屬性加上kissg_
字首。這就是所謂的“控制類的建立行為”,並且它是自動進行的。
那麼,如何來自定義元類呢?其實只要我們明白了type
是如何建立類的,自定義元類就是非常簡單的一件事,無非就是接收類定義,並修改類定義,再返回一個類。而且,我們完全可以呼叫type
函式來返回這個類,從而簡化操作。
# 我們已經知道type是一個元類,因此自定義元類應繼承自type或其子類
# 有一個約定俗成的習慣,自定義元類一般以Metaclass作為字尾,以明確表示這是一個元類
class AddPrefixMetaclass(type):
# __new__方法在__init__方法之前被呼叫
# 因此,當我們想要控制類的建立行為時,一般使用__new__方法
# 定義普通類的方法時,我們用self作為第一個引數,來指向呼叫方法的類例項本身
# 此處addprefix_metaclass的意義與self類似,用於指向使用該元類建立的類本身
# 其他引數就是類的定義了,依次是類名,父類的元組,屬性的字典
def __new__(addprefix_metaclass, class_name, class_bases, class_dict):
prefix = "kissg_"
addprefix_dict = {} # 我們用一個新的字典來儲存加了字首的屬性
# 遍歷類的屬性,為所有非特殊屬性與私有屬性加上字首
for name, val in class_dict.items():
if not name.startswith('_'):
addprefix_dict[prefix + name] = val
else:
addprefix_dict[name] = val
# 呼叫type函式來返回類,此時我們使用的是加了字首的屬性字典
return type(class_name, class_bases, addprefix_dict)
# 指定metaclass為自定義的元類,將在建立類時使用該自定義元類
class Myclass(object, metaclass=AddPrefixMetaclass):
name = "kissg"
kg = Myclass()
print(hasattr(Myclass, "name"))
# 輸出: False
print(hasattr(Myclass, "kissg_name"))
# 輸出: True
print(kg.kissg_name)
# 輸出: kissg
如你所見,自定義元類就這麼簡單,而元類的使用同樣簡單,只需在類定義時像使用關鍵字引數一樣,指定metaclass為自定義的元類即可。
按照原文的說法,以上自定義元類不是物件導向程式設計(Object-oriented programming,簡稱OOP)的正確寫法。正確的寫法應該是這樣的:
class AddPrefixMetaclass(type):
# 此處__new__的引數也是約定俗成的寫法,就像用**kw表示關鍵字引數一樣
# cls - 使用自定義元類要建立的類,你可以就簡單地記成self
# clsname - 類名
# bases - 父類的元組的(tuple)
# dct - 類屬性的字典
def __new__(cls, clsname, bases, dct):
prefix = "kissg_"
addprefix_dict = {}
for name, val in dct.items():
if not name.startswith('_'):
addprefix_dict[prefix + name] = val
else:
addprefix_dict[name] = val
# 元類也是可以被繼承的。
# 呼叫父類的__new__方法來建立類,簡化繼承
return super(AddPrefixMetaclass, cls).__new__(cls, clsname, bases, addprefix_dict)
是不是比之前的優雅了許多?既避免了直接呼叫type
函式,又使用super
使繼承顯得更容易了,而且使用約定俗稱的命名方法立顯規範與高大上氣息。
最後,建立類的元類是可以被繼承的,有點拗口,但請區別於元類是可以被繼承的。這句話的意思是:定義子類時沒有指定元類(即沒有metaclass=XXXMetaclass
),將自動使用其父類的元類來建立該子類。
注: python2還可以通過指定__metaclass__屬性為元類或其他任何能返回類的東西(比如函式)來控制類的建立。python3雖然保留了__metaclass__屬性,但其實並無用處,因此本文不展開講。有興趣的同學可以看看原文,但我覺得沒太大必要
小結
元類被譽為python的黑魔法(black magic)之一,一方面強調了元類使用的困難,另一方面也強調了元類的強大。如果你仔細看過正文的內容,會發現元類的使用似乎也不太難。如果覺得仍有難度,再看一遍,或者可以看下原文。當然本文所舉的例子都比較簡單,你完全可以用元類實現一些更加強大的功能,比如自定義一個ORM(Object Relational Mapping,物件關係對映),我也是基於此,才找了一些材料學習元類的。
總的來說,元類就做了以下3件事:
- 攔截類的建立
- 修改類定義
- 返回修改後的類
也許這樣將步驟拆分了,你能更好得理解記憶。而元類真的就這麼簡單。
引用原文的一句話作結:
Everything is an object in python, and they are all either instances of classes or instances of metaclasses.
(在python的世界裡,一切皆為物件,它們要麼是類的例項,要麼是元類的例項。)
相關文章
- Python 學習筆記之類「物件導向,超類,抽象」Python筆記物件抽象
- Python 3 學習筆記之類與例項Python筆記
- 【菜鳥教程筆記】python基礎之元組的使用筆記Python
- Python 元類Python
- Python筆記之SqlAlchemy使用Python筆記SQL
- python3筆記---抽象類例子Python筆記抽象
- Python的元類Python
- TypeScript學習筆記之五類(Class)TypeScript筆記
- Java學習筆記之類和物件Java筆記物件
- Python學習筆記|Python之程式Python筆記
- Python學習筆記8——列表、字典、元組Python筆記
- Python元類與列舉類Python
- Python學習筆記之序列Python筆記
- [心得]python元類解密Python解密
- Python元類再談Python
- python元類淺析Python
- Java學習筆記之介面和抽象類Java筆記抽象
- Scala 學習筆記(2)之類和物件筆記物件
- Python學習筆記|Python之yield理解Python筆記
- Python學習筆記|Python之索引迭代Python筆記索引
- Python學習筆記|Python之特殊方法Python筆記
- 【廖雪峰python進階筆記】定製類Python筆記
- Python 筆記之膽大還蠢Python筆記
- Python學習筆記|Python之檔案操作Python筆記
- Python學習筆記|Python之物件導向Python筆記物件
- Python學習筆記|Python之特殊檔案Python筆記
- Python學習筆記|Python之推導式Python筆記
- Python學習筆記|Python之執行緒Python筆記執行緒
- 由ORM談Python元類ORMPython
- python中的元類MetaclassPython
- 草根學Python(十二)元類Python
- 理解Python中的元類Python
- python學習筆記:第4天 列表和元組Python筆記
- python3 筆記18.構建類classPython筆記
- 【廖雪峰python進階筆記】類的繼承Python筆記繼承
- Android筆記之Kotlin、Java的內部類?巢狀類?Android筆記KotlinJava巢狀
- Python之——網路程式設計筆記Python程式設計筆記
- Python筆記一之excel的讀取Python筆記Excel