前言
第十二篇了,擼起袖子,就是干。
目錄
一、Python 中類也是對象
在了解元類之前,我們先進一步理解 Python 中的類,在大多數(shù)編程語言中,類就是一組用來描述如何生成一個對象的代碼段。在 Python 中這一點也是一樣的。
class ObjectCreator(object):
pass
mObject = ObjectCreator()
print(mObject)
輸出結(jié)果:
<__main__.ObjectCreator object at 0x00000000023EE048>
但是,Python 中的類有一點跟大多數(shù)的編程語言不同,在 Python 中,可以把類理解成也是一種對象。對的,這里沒有寫錯,就是對象。
為什么呢?
因為只要使用關鍵字 class ,Python 解釋器在執(zhí)行的時候就會創(chuàng)建一個對象。
如:
class ObjectCreator(object):
pass
當程序運行這段代碼的時候,就會在內(nèi)存中創(chuàng)建一個對象,名字就是ObjectCreator。這個對象(類)自身擁有創(chuàng)建對象(類實例)的能力,而這就是為什么它是一個類的原因。但是,它的本質(zhì)仍然是一個對象,于是我們可以對它做如下的操作:
class ObjectCreator(object):
pass
def echo(ob):
print(ob)
mObject = ObjectCreator()
print(mObject)
# 可以直接打印一個類,因為它其實也是一個對象
print(ObjectCreator)
# 可以直接把一個類作為參數(shù)傳給函數(shù)(注意這里是類,是沒有實例化的)
echo(ObjectCreator)
# 也可以直接把類賦值給一個變量
objectCreator = ObjectCreator
print(objectCreator)
輸出的結(jié)果如下:
<__main__.ObjectCreator object at 0x000000000240E358>
<class '__main__.ObjectCreator'>
<class '__main__.ObjectCreator'>
<class '__main__.ObjectCreator'>
二、使用 type() 動態(tài)創(chuàng)建類
因為類也是對象,所以我們可以在程序運行的時候創(chuàng)建類。Python 是動態(tài)語言。動態(tài)語言和靜態(tài)語言最大的不同,就是函數(shù)和類的定義,不是編譯時定義的,而是運行時動態(tài)創(chuàng)建的。在之前,我們先了了解下 type() 函數(shù)。
首先我們新建一個 hello.py 的模塊,然后定義一個 Hello 的 class ,
class Hello(object):
def hello(self, name='Py'):
print('Hello,', name)
然后在另一個模塊中引用 hello 模塊,并輸出相應的信息。其中 type() 函數(shù)的作用是可以查看一個類型和變量的類型。
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from com.twowater.hello import Hello
h = Hello()
h.hello()
print(type(Hello))
print(type(h))
輸出的結(jié)果是怎樣的呢?
Hello, Py
<class 'type'>
<class 'com.twowater.hello.Hello'>
上面也提到過,type() 函數(shù)可以查看一個類型或變量的類型,Hello 是一個 class ,它的類型就是 type ,而 h 是一個實例,它的類型就是 com.twowater.hello.Hello。前面的 com.twowater 是我的包名,hello 模塊在該包名下。
在這里還要細想一下,上面的例子中,我們使用 type() 函數(shù)查看一個類型或者變量的類型。其中查看了一個 Hello class 的類型,打印的結(jié)果是: <class 'type'> 。其實 type() 函數(shù)不僅可以返回一個對象的類型,也可以創(chuàng)建出新的類型。class 的定義是運行時動態(tài)創(chuàng)建的,而創(chuàng)建 class 的方法就是使用 type() 函數(shù)。比如我們可以通過 type() 函數(shù)創(chuàng)建出上面例子中的 Hello 類,具體看下面的代碼:
# -*- coding: UTF-8 -*-
def printHello(self, name='Py'):
# 定義一個打印 Hello 的函數(shù)
print('Hello,', name)
# 創(chuàng)建一個 Hello 類
Hello = type('Hello', (object,), dict(hello=printHello))
# 實例化 Hello 類
h = Hello()
# 調(diào)用 Hello 類的方法
h.hello()
# 查看 Hello class 的類型
print(type(Hello))
# 查看實例 h 的類型
print(type(h))
輸出的結(jié)果如下:
Hello, Py
<class 'type'>
<class '__main__.Hello'>
在這里,需先了解下通過 type() 函數(shù)創(chuàng)建 class 對象的參數(shù)說明:
1、class 的名稱,比如例子中的起名為 Hello
2、繼承的父類集合,注意 Python 支持多重繼承,如果只有一個父類,tuple 要使用單元素寫法;例子中繼承 object 類,因為是單元素的 tuple ,所以寫成 (object,)
3、class 的方法名稱與函數(shù)綁定;例子中將函數(shù) printHello 綁定在方法名 hello 中
具體的模式如下:
type(類名, 父類的元組(針對繼承的情況,可以為空),包含屬性的字典(名稱和值))
好了,了解完具體的參數(shù)使用之外,我們看看輸出的結(jié)果,可以看到,通過 type() 函數(shù)創(chuàng)建的類和直接寫 class 是完全一樣的,因為Python 解釋器遇到 class 定義時,僅僅是掃描一下 class 定義的語法,然后調(diào)用 type() 函數(shù)創(chuàng)建出 class 的 。
不過一般的情況下,我們都是使用 class ***... 的方法來定義類的,不過 type() 函數(shù)也可以讓我們創(chuàng)建出類來。也就是說,動態(tài)語言本身支持運行期動態(tài)創(chuàng)建類,這和靜態(tài)語言有非常大的不同,要在靜態(tài)語言運行期創(chuàng)建類,必須構造源代碼字符串再調(diào)用編譯器,或者借助一些工具生成字節(jié)碼實現(xiàn),本質(zhì)上都是動態(tài)編譯,會非常復雜。
可以看到,在 Python 中,類也是對象,你可以動態(tài)的創(chuàng)建類。其實這也就是當你使用關鍵字 class 時 Python 在幕后做的事情,而這就是通過元類來實現(xiàn)的。
三、什么是元類
通過上面的介紹,終于模模糊糊的帶到元類這里來了??墒俏覀兊浆F(xiàn)在還不知道元類是什么東東。
我們創(chuàng)建類的時候,大多數(shù)是為了創(chuàng)建類的實例對象。那么元類呢?元類就是用來創(chuàng)建類的。也可以換個理解方式就是:元類就是類的類。
通過上面 type() 函數(shù)的介紹,我們知道可以通過 type() 函數(shù)創(chuàng)建類:
MyClass = type('MyClass', (), {})
實際上 type() 函數(shù)是一個元類。type() 就是 Python 在背后用來創(chuàng)建所有類的元類。
那么現(xiàn)在我們也可以猜到一下為什么 type() 函數(shù)是 type 而不是 Type呢?
這可能是為了和 str 保持一致性,str 是用來創(chuàng)建字符串對象的類,而 int 是用來創(chuàng)建整數(shù)對象的類。type 就是創(chuàng)建類對象的類。你可以通過檢查 __class__ 屬性來看到這一點。Python 中所有的東西,注意喔,這里是說所有的東西,他們都是對象。這包括整數(shù)、字符串、函數(shù)以及類。它們?nèi)慷际菍ο螅宜鼈兌际菑囊粋€類創(chuàng)建而來。
# 整形
age = 23
print(age.__class__)
# 字符串
name = '兩點水'
print(name.__class__)
# 函數(shù)
def fu():
pass
print(fu.__class__)
# 實例
class eat(object):
pass
mEat = eat()
print(mEat.__class__)
輸出的結(jié)果如下:
<class 'int'>
<class 'str'>
<class 'function'>
<class '__main__.eat'>
可以看到,上面的所有東西,也就是所有對象都是通過類來創(chuàng)建的,那么我們可能會好奇,__class__ 的 __class__ 會是什么呢?換個說法就是,創(chuàng)建這些類的類是什么呢?
我們可以繼續(xù)在上面的代碼基礎上新增下面的代碼:
print(age.__class__.__class__)
print(name.__class__.__class__)
print(fu.__class__.__class__)
print(mEat.__class__.__class__)
輸出的結(jié)果如下:
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>
認真觀察,再理清一下,上面輸出的結(jié)果是我們把整形 age ,字符創(chuàng) name ,函數(shù) fu 和對象實例 mEat 里 __class__ 的 __class__ 打印出來的結(jié)果。也可以說是他們類的類打印結(jié)果。發(fā)現(xiàn)打印出來的 class 都是 type 。
一開始也提到了,元類就是類的類。也就是元類就是負責創(chuàng)建類的一種東西。你也可以理解為,元類就是負責生成類的。而 type 就是內(nèi)建的元類。也就是 Python 自帶的元類。
四、自定義元類
到現(xiàn)在,我們已經(jīng)知道元類是什么東東了。那么,從始至終我們還不知道元類到底有啥用。只是了解了一下元類。在了解它有啥用的時候,我們先來了解下怎么自定義元類。因為只有了解了怎么自定義才能更好的理解它的作用。
首先我們來了解下 __metaclass__ 屬性
metaclass,直譯為元類,簡單的解釋就是:
當我們定義了類以后,就可以根據(jù)這個類創(chuàng)建出實例,所以:先定義類,然后創(chuàng)建實例。
但是如果我們想創(chuàng)建出類呢?那就必須根據(jù)metaclass創(chuàng)建出類,所以:先定義metaclass,然后創(chuàng)建類。
連接起來就是:先定義metaclass,就可以創(chuàng)建類,最后創(chuàng)建實例。
所以,metaclass允許你創(chuàng)建類或者修改類。換句話說,你可以把類看成是metaclass創(chuàng)建出來的“實例”。
class MyObject(object):
__metaclass__ = something…
[…]
如果是這樣寫的話,Python 就會用元類來創(chuàng)建類 MyObject。當你寫下 class MyObject(object),但是類對象 MyObject 還沒有在內(nèi)存中創(chuàng)建。Python 會在類的定義中尋找 __metaclass__ 屬性,如果找到了,Python 就會用它來創(chuàng)建類 MyObject,如果沒有找到,就會用內(nèi)建的 type 函數(shù)來創(chuàng)建這個類。如果還不怎么理解,看下下面的流程圖:
再舉個實例:
class Foo(Bar):
pass
它的判斷流程是怎樣的呢?
首先判斷 Foo 中是否有 __metaclass__ 這個屬性?如果有,Python 會在內(nèi)存中通過 __metaclass__ 創(chuàng)建一個名字為 Foo 的類對象(注意,這里是類對象)。如果 Python 沒有找到__metaclass__ ,它會繼續(xù)在 Bar(父類)中尋找__metaclass__ 屬性,并嘗試做和前面同樣的操作。如果 Python在任何父類中都找不到 __metaclass__ ,它就會在模塊層次中去尋找 __metaclass__ ,并嘗試做同樣的操作。如果還是找不到 __metaclass__ ,Python 就會用內(nèi)置的 type 來創(chuàng)建這個類對象。
其實 __metaclass__ 就是定義了 class 的行為。類似于 class 定義了 instance 的行為,metaclass 則定義了 class 的行為。可以說,class 是 metaclass 的 instance。
現(xiàn)在,我們基本了解了 __metaclass__ 屬性,但是,也沒講過如何使用這個屬性,或者說這個屬性可以放些什么?
答案就是:可以創(chuàng)建一個類的東西。那么什么可以用來創(chuàng)建一個類呢?type,或者任何使用到 type 或者子類化 type 的東東都可以。
元類的主要目的就是為了當創(chuàng)建類時能夠自動地改變類。通常,你會為API 做這樣的事情,你希望可以創(chuàng)建符合當前上下文的類。假想一個很傻的例子,你決定在你的模塊里所有的類的屬性都應該是大寫形式。有好幾種方法可以辦到,但其中一種就是通過在模塊級別設定__metaclass__ 。采用這種方法,這個模塊中的所有類都會通過這個元類來創(chuàng)建,我們只需要告訴元類把所有的屬性都改成大寫形式就萬事大吉了。
幸運的是,__metaclass__ 實際上可以被任意調(diào)用,它并不需要是一個正式的類。所以,我們這里就先以一個簡單的函數(shù)作為例子開始。
# 元類會自動將你通常傳給‘type’的參數(shù)作為自己的參數(shù)傳入
def upper_attr(future_class_name, future_class_parents, future_class_attr):
'''返回一個類對象,將屬性都轉(zhuǎn)為大寫形式'''
# 選擇所有不以'__'開頭的屬性
attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
# 將它們轉(zhuǎn)為大寫形式
uppercase_attr = dict((name.upper(), value) for name, value in attrs)
# 通過'type'來做類對象的創(chuàng)建
return type(future_class_name, future_class_parents, uppercase_attr)
__metaclass__ = upper_attr
# 這會作用到這個模塊中的所有類
class Foo(object):
# 我們也可以只在這里定義__metaclass__,這樣就只會作用于這個類中
bar = 'bip'
print hasattr(Foo, 'bar')
# 輸出: False
print hasattr(Foo, 'BAR')
# 輸出:True
f = Foo()
print f.BAR
# 輸出:'bip'
用 class 當做元類的做法:
# 請記住,'type'實際上是一個類,就像'str'和'int'一樣
# 所以,你可以從type繼承
class UpperAttrMetaClass(type):
# __new__ 是在__init__之前被調(diào)用的特殊方法
# __new__是用來創(chuàng)建對象并返回之的方法
# 而__init__只是用來將傳入的參數(shù)初始化給對象
# 你很少用到__new__,除非你希望能夠控制對象的創(chuàng)建
# 這里,創(chuàng)建的對象是類,我們希望能夠自定義它,所以我們這里改寫__new__
# 如果你希望的話,你也可以在__init__中做些事情
# 還有一些高級的用法會涉及到改寫__call__特殊方法,但是我們這里不用
def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr):
attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
uppercase_attr = dict((name.upper(), value) for name, value in attrs)
return type(future_class_name, future_class_parents, uppercase_attr)
但是,這種方式其實不是 OOP。我們直接調(diào)用了 type,而且我們沒有改寫父類的 __new__ 方法。現(xiàn)在讓我們這樣去處理:
class UpperAttrMetaclass(type):
def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr):
attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
uppercase_attr = dict((name.upper(), value) for name, value in attrs)
# 復用type.__new__方法
# 這就是基本的OOP編程,沒什么魔法
return type.__new__(upperattr_metaclass, future_class_name, future_class_parents, uppercase_attr)
你可能已經(jīng)注意到了有個額外的參數(shù) upperattr_metaclass ,這并沒有什么特別的。類方法的第一個參數(shù)總是表示當前的實例,就像在普通的類方法中的 self 參數(shù)一樣。當然了,為了清晰起見,這里的名字我起的比較長。但是就像 self 一樣,所有的參數(shù)都有它們的傳統(tǒng)名稱。因此,在真實的產(chǎn)品代碼中一個元類應該是像這樣的:
class UpperAttrMetaclass(type):
def __new__(cls, name, bases, dct):
attrs = ((name, value) for name, value in dct.items() if not name.startswith('__')
uppercase_attr = dict((name.upper(), value) for name, value in attrs)
return type.__new__(cls, name, bases, uppercase_attr)
如果使用 super 方法的話,我們還可以使它變得更清晰一些,這會緩解繼承(是的,你可以擁有元類,從元類繼承,從 type 繼承)
class UpperAttrMetaclass(type):
def __new__(cls, name, bases, dct):
attrs = ((name, value) for name, value in dct.items() if not name.startswith('__'))
uppercase_attr = dict((name.upper(), value) for name, value in attrs)
return super(UpperAttrMetaclass, cls).__new__(cls, name, bases, uppercase_attr)
通常我們都會使用元類去做一些晦澀的事情,依賴于自省,控制繼承等等。確實,用元類來搞些“黑暗魔法”是特別有用的,因而會搞出些復雜的東西來。但就元類本身而言,它們其實是很簡單的:
- 攔截類的創(chuàng)建
- 修改類
- 返回修改之后的類
五、使用元類
終于到了使用元類了,可是一般來說,我們根本就用不上它,就像Python 界的領袖 Tim Peters 說的:
元類就是深度的魔法,99% 的用戶應該根本不必為此操心。如果你想搞清楚究竟是否需要用到元類,那么你就不需要它。那些實際用到元類的人都非常清楚地知道他們需要做什么,而且根本不需要解釋為什么要用元類。
元類的主要用途是創(chuàng)建 API。一個典型的例子是 Django ORM。它允許你像這樣定義:
class Person(models.Model):
name = models.CharField(max_length=30)
age = models.IntegerField()
但是如果你這樣做的話:
guy = Person(name='bob', age='35')
print guy.age
這并不會返回一個 IntegerField 對象,而是會返回一個 int,甚至可以直接從數(shù)據(jù)庫中取出數(shù)據(jù)。這是有可能的,因為 models.Model 定義了 __metaclass__ , 并且使用了一些魔法能夠?qū)⒛銊倓偠x的簡單的Person類轉(zhuǎn)變成對數(shù)據(jù)庫的一個復雜 hook。Django 框架將這些看起來很復雜的東西通過暴露出一個簡單的使用元類的 API 將其化簡,通過這個 API 重新創(chuàng)建代碼,在背后完成真正的工作。
Python 中的一切都是對象,它們要么是類的實例,要么是元類的實例,除了 type。type 實際上是它自己的元類,在純 Python 環(huán)境中這可不是你能夠做到的,這是通過在實現(xiàn)層面耍一些小手段做到的。
參考:
https://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python
最后如果對本文有興趣,可以關注公眾號: