1、類也是對象
所有對象都是實例化或者調(diào)用類而得到的,Python中一切都是對象,通過class關(guān)鍵字定義的類本質(zhì)也是對象,對象又是通過調(diào)用類得到的,因此通過class關(guān)鍵字定義的類肯定也是調(diào)用了一個類得到的,這個類就是元類。type就是Python內(nèi)置的元類
在理解元類之前,你需要先掌握Python中的類。Python中類的概念借鑒于Smalltalk語言,這顯得有些奇特。在大多數(shù)編程語言中,類就是一組用來描述如何生成一個對象的代碼段。在Python中這一點仍然成立:
class ObjectCreator(object):
pass
obj = ObjectCreator()
print(obj) # <__main__.ObjectCreator object at 0x0000021098A4AFB0>
但是,Python中的類還遠不止如此。類同樣也是一種對象。是的,沒錯,就是對象。只要你使用關(guān)鍵字class,Python解釋器在執(zhí)行的時候就會創(chuàng)建一個對象
例如,上面class代碼段,將在內(nèi)存中創(chuàng)建對象ObjectCreator。這個對象(類)自身擁有創(chuàng)建對象(類實例)的能力,而這就是它為什么是類也是對象的原因
但是,它本質(zhì)上仍然是一個對象,于是乎你可以對它做如下操作:
- 1)你可以將它賦值給一個變量
- 2)你可以拷貝它
- 3)你可以為它增加屬性
- 4)你可以將它作為函數(shù)參數(shù)進行傳遞
下面是示例:
# 你可以打印一個類,因為它就是一個對象
print(ObjectCreator) # <class '__main__.ObjectCreator'>
# 你可以將類作為參數(shù)傳給函數(shù)
def echo(o):
print(o)
echo(ObjectCreator) # <class '__main__.ObjectCreator'>
# 你可以為類增加屬性
ObjectCreator.field = 'value'
print(hasattr(ObjectCreator, 'field')) # True
print(ObjectCreator.field) # value
# 你可以將類復(fù)制給一個變量
var = ObjectCreator
print(var()) # <__main__.ObjectCreator object at 0x0000026AF00EABF0>
動態(tài)地創(chuàng)建類:
因為類也是對象,你可以在運行時動態(tài)的創(chuàng)建它們,就像其他任何對象一樣。首先,你可以在函數(shù)中創(chuàng)建類,使用class關(guān)鍵字即可
def choose_class(name):
match name:
case 'stu':
class Stu(object):
pass
return Stu
case 'emp':
class Emp(object):
pass
return Emp
stu = choose_class('stu')
# 返回類,而不是類的實例
print(stu) # <class '__main__.choose_class.<locals>.Stu'>
# 可以通過這個類創(chuàng)建類的實例(類對象)
print(stu()) # <__main__.choose_class.<locals>.Stu object at 0x000001D7E6D9AAA0>
但這還不夠動態(tài),因為你仍然需要自己編寫整個類的代碼。由于類也是對象,所以它們應(yīng)該也是通過什么東西來生成的才對。當(dāng)你使用class關(guān)鍵字時,Python解釋器自動創(chuàng)建這個對象。但就和Python中的大多數(shù)事情一樣,Python仍然提供給你手動處理的方法
還記得內(nèi)建函數(shù)type()嗎?這個古老但強大的函數(shù)能夠讓你知道一個對象的類型是什么,就像這樣:
print(type(0)) # <class 'int'>
print(type('0')) # <class 'str'>
print(type(ObjectCreator)) # <class 'type'>
print(type(ObjectCreator())) # <class '__main__.ObjectCreator'>
這里,type有一種完全不同的能力,它也能動態(tài)的創(chuàng)建類。type可以接受一個類的描述作為參數(shù),然后返回一個類
我知道,根據(jù)傳入?yún)?shù)的不同,同一個函數(shù)擁有兩種完全不同的用法是一件很愚蠢的事情,但這在Python中是為了保持向后兼容性
type可以像這樣工作:
'''
type(name, bases, attrs)
name:類的名稱 bases:父類,用于繼承,元組類型,可為空 attrs:包含屬性名稱和屬性值的字典
'''
比如下面的代碼:
class MyShinyClass(object):
pass
# 可以手動像這樣創(chuàng)建:
MyShinyClass = type('MyShinyClass', (), {})
# 返回類對象
print(MyShinyClass) # <class '__main__.MyShinyClass'>
# 創(chuàng)建該類的實例
print(MyShinyClass()) # <__main__.MyShinyClass object at 0x0000018E2F0FAAA0>
你會發(fā)現(xiàn)我們使用MyShinyClass作為類名,并且也可以把它當(dāng)做一個變量來作為類的引用。類和變量是不同的,這里沒有任何理由把事情弄的復(fù)雜
type接受一個字典來為類定義屬性,因此:
class Foo(object):
flag = True
可以翻譯為:
Foo = type('Foo', (), {'flag': True})
并且可以將Foo當(dāng)成一個普通的類一樣使用:
print(Foo) # <class '__main__.Foo'>
print(Foo.flag) # True
foo = Foo()
print(foo) # <__main__.Foo object at 0x00000203B4A4AA70>
print(foo.flag) # True
當(dāng)然,你可以向這個類繼承:
class FooChild(Foo):
pass
就可以寫成:
FooChild = type('FooChild', (Foo,), {})
print(FooChild) # <class '__main__.FooChild'>
# flag屬性是由繼承而來的
print(FooChild.flag) # True
最終你會希望為你的類增加方法。只需要定義一個有著恰當(dāng)簽名的函數(shù)并將其作為屬性賦值就可以了:
def echo_flag(self):
print(self.flag)
FooChild = type('FooChild', (Foo,), {'echo_flag': echo_flag})
print(hasattr(Foo, 'echo_flag')) # False
print(hasattr(FooChild, 'echo_flag')) # True
child = FooChild()
child.echo_flag() # True
你可以看到,在Python中,類也是對象,你可以動態(tài)的創(chuàng)建類。這就是當(dāng)你使用關(guān)鍵字class時Python在幕后做的事情,而這就是通過元類來實現(xiàn)的
2、什么是元類
元類就是用來創(chuàng)建類的東西。你創(chuàng)建類就是為了創(chuàng)建類的實例對象,不是嗎?但是我們已經(jīng)知道Python中的類也是對象。好吧,元類就是用來創(chuàng)建這些類(對象)的,元類就是類的類,你可以這樣理解:
'''
MyClass = MetaClass()
MyObject = MyClass()
'''
你已經(jīng)看到了type可以讓你像這樣做:
MyClass = type('MyClass', (), {})
這是因為函數(shù)type實際上是一個元類。type就是Python在背后用來創(chuàng)建所有類的元類?,F(xiàn)在你想知道那為什么type會全部采用小寫形式而不是Type呢?好吧,我猜這是為了和str保持一致性,str是用來創(chuàng)建字符串對象的類,而int是用來創(chuàng)建整數(shù)對象的類
type就是創(chuàng)建類對象的類。你可以通過檢查__class__
屬性來看到這一點。Python中所有的東西,注意,我是指所有的東西——都是對象。這包括整數(shù)、字符串、函數(shù)以及類。它們?nèi)慷际菍ο?,而且它們都是從一個類創(chuàng)建而來
age = 18
print(age.__class__) # <class 'int'>
name = 'Tom'
print(name.__class__) # <class 'str'>
def method(): pass
print(method.__class__) # <class 'function'>
class Bar(object): pass
bar = Bar()
print(bar.__class__) # <class '__main__.Bar'>
那么,對于任何一個__class__
的__class__
屬性又是什么呢?
print(age.__class__.__class__) # <class 'type'>
print(name.__class__.__class__) # <class 'type'>
print(method.__class__.__class__) # <class 'type'>
print(bar.__class__.__class__) # <class 'type'>
因此,元類就是創(chuàng)建類這種對象的東西。如果你喜歡的話,可以把元類稱為類工廠(不要和工廠類搞混了),type就是Python的內(nèi)建元類,當(dāng)然了,你也可以創(chuàng)建自己的元類
3、__metaclass__屬性
你可以在寫一個類的時候為其添加__metaclass__
屬性:
class Foo(object):
__metaclass__ = something
如果你這么做了,Python就會用元類來創(chuàng)建類Foo。小心點,這里面有些技巧。你首先寫下class Foo(object)
,但是類對象Foo還沒有在內(nèi)存中創(chuàng)建
Python會在類的定義中尋找__metaclass__
屬性,如果找到了,Python就會用它來創(chuàng)建類Foo,如果沒有找到,就會用內(nèi)建的type來創(chuàng)建這個類
class Foo(Bar):
pass
當(dāng)你寫該代碼時,Python做了如下的操作:
Foo中有__metaclass__
這個屬性嗎?如果是,Python會在內(nèi)存中通過__metaclass__
創(chuàng)建一個名字為Foo的類對象。如果Python沒有找到__metaclass__
,它會繼續(xù)在Bar(父類)中尋找__metaclass__
屬性,并嘗試做和前面同樣的操作。如果Python在任何父類中都找不到__metaclass__
,它就會在模塊層次中去尋找__metaclass__
,并嘗試做同樣的操作,如果還是找不到__metaclass__
,Python就會用內(nèi)置的type來創(chuàng)建這個類對象
現(xiàn)在的問題就是,你可以在__metaclass__
中放置些什么代碼呢?答案就是:可以創(chuàng)建一個類的東西。那么什么可以用來創(chuàng)建一個類呢?type或任何使用到type或子類化type的東西都可以
Python3中的元類:
在Python3中,設(shè)置元類的語法已經(jīng)更改:
class Foo(object, metaclass=something):
pass
即不再使用metaclass屬性,而是在基類列表中使用__metaclass__
關(guān)鍵字參數(shù)。然而,元類的行為基本保持不變
4、自定義元類
元類的主要目的就是為了當(dāng)創(chuàng)建類時能夠自動地改變類。通常,你會為API做這樣的事情,你希望可以創(chuàng)建符合當(dāng)前上下文的類。假想一個很愚蠢的例子,你決定在你的模塊里所有的類的屬性都應(yīng)該是大寫形式
有好幾種方法可以辦到,但其中一種就是通過在模塊級別設(shè)定__metaclass__
。采用這種方法,這個模塊中的所有類都會通過這個元類來創(chuàng)建,我們只需要告訴元類把所有的屬性都改成大寫形式就萬事大吉了
幸運的是,__metaclass__
實際上可以被任意調(diào)用,它并不需要是一個正式的類。所以,我們這里就先以一個簡單的函數(shù)作為例子開始
# 元類會自動將你通常傳給type的參數(shù)作為自己的參數(shù)傳入
def upper_attr(future_class_name, future_class_parents, future_class_attrs):
"""返回一個類對象,將屬性全部轉(zhuǎn)為大寫形式"""
# 選擇所有不以'__'開頭的屬性(私有屬性),將它們轉(zhuǎn)為大寫形式
uppercase_attrs = {
attr if attr.startswith("__") else attr.upper(): v
for attr, v in future_class_attrs.items()
}
# 通過type來做類對象的創(chuàng)建
return type(future_class_name, future_class_parents, uppercase_attrs)
# 這會作用到這個模塊中的所有類
__metaclass__ = upper_attr
# 需要注意的是,全局__metaclass__將不能與object一起工作
class Foo():
# 我們也可以只在這里定義__metaclass__,這樣就只會作用于這個類中
# __metaclass__ = upper_attr
bar = 'bip'
print(hasattr(Foo, 'bar')) # True
print(hasattr(Foo, 'BAR')) # False
foo = Foo()
print(foo.bar) # bip
注意:此處存在問題,結(jié)果與預(yù)期相反,原因未知,有人知道什么原因嗎?
現(xiàn)在,讓我們做完全相同的事情,但對元類使用一個真正的類:
# 請記住,type實際上是一個類,就像str和int一樣,所以,你可以從type繼承
class UpperAttrMetaClass(type):
# __new__是在__init__之前被調(diào)用的特殊方法,是用來創(chuàng)建對象并返回的方法
# 而__init__只是用來將傳入的參數(shù)初始化給對象,你很少用到__new__,除非你希望能夠控制對象的創(chuàng)建
# 這里,創(chuàng)建的對象是類,我們希望能夠自定義它,所以我們這里改寫__new__
# 如果你希望的話,你也可以在__init__中做些事情,還有一些高級的用法會涉及到改寫__call__特殊方法,但是我們這里不用
def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attrs):
uppercase_attrs = {
attr if attr.startswith("__") else attr.upper(): v
for attr, v in future_class_attrs.items()
}
return type(upperattr_metaclass, future_class_name, future_class_parents, uppercase_attrs)
但是,這種方式其實不是OOP。我們直接調(diào)用了type,而且我們沒有改寫父類的__new__
方法?,F(xiàn)在讓我們這樣去處理:
class UpperAttrMetaClass(type):
def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attrs):
uppercase_attrs = {
attr if attr.startswith("__") else attr.upper(): v
for attr, v in future_class_attrs.items()
}
return type.__new__(upperattr_metaclass, future_class_name, future_class_parents, uppercase_attrs)
你可能已經(jīng)注意到了有個額外的參數(shù)upperattr_metaclass,這并沒有什么特別的。類方法的第一個參數(shù)總是表示當(dāng)前的實例,就像在普通的類方法中的self參數(shù)一樣
當(dāng)然了,為了清晰起見,這里的名字我起的比較長。但是就像self一樣,所有的參數(shù)都有它們的傳統(tǒng)名稱。因此,在真實的產(chǎn)品代碼中一個元類應(yīng)該是像這樣的:
class UpperAttrMetaclass(type):
def __new__(cls, clsname, bases, attrs):
uppercase_attrs = {
attr if attr.startswith("__") else attr.upper(): v
for attr, v in attrs.items()
}
return type.__new__(cls, clsname, bases, uppercase_attrs)
如果使用super方法的話,我們還可以使它變得更清晰一些,這會簡化繼承(你可以擁有元類,從元類繼承,從type繼承)
class UpperAttrMetaclass(type):
def __new__(cls, clsname, bases, attrs):
uppercase_attrs = {
attr if attr.startswith("__") else attr.upper(): v
for attr, v in attrs.items()
}
return super(UpperAttrMetaclass, cls).__new__(cls, clsname, bases, uppercase_attrs)
class Foo(object, metaclass=UpperAttrMetaclass):
bar = 'bip'
print(hasattr(Foo, 'bar')) # False
print(hasattr(Foo, 'BAR')) # True
foo = Foo()
print(foo.BAR) # bip
在Python3中,如果你使用關(guān)鍵字參數(shù)進行調(diào)用,如下所示:
class Foo(object, metaclass=MyMetaclass, kwargs=default):
pass
它在元類中可轉(zhuǎn)換為:
class MyMetaclass(type):
def __new__(cls, clsname, bases, dct, kwargs=default):
pass
就是這樣,除此之外,關(guān)于元類真的沒有別的可說的了。使用到元類的代碼比較復(fù)雜,這背后的原因倒并不是因為元類本身,而是因為你通常會使用元類去做一些晦澀的事情,依賴于自省,控制繼承等
確實,用元類來搞些“黑暗魔法”是特別有用的,因而會搞出些復(fù)雜的東西來。但就元類本身而言,它們其實是很簡單的:
- 1)攔截類的創(chuàng)建
- 2)修改類
- 3)返回修改之后的類
5、為什么要用metaclass類而不是函數(shù)
由于__metaclass__
可以接受任何可調(diào)用的對象,那為何還要使用類呢,因為很顯然使用類會更加復(fù)雜??!這樣做有以下幾個原因:
- 1)意圖會更加清晰。當(dāng)你讀到UpperAttrMetaclass(type)時,你知道接下來要發(fā)生什么
- 2)你可以使用OOP編程。元類可以從元類中繼承而來,改寫父類的方法。元類甚至還可以使用元類
- 3)你可以把代碼組織的更好。當(dāng)你使用元類的時候肯定不會是像我上面舉的這種簡單場景,通常都是針對比較復(fù)雜的問題。將多個方法歸總到一個類中會很有幫助,也會使得代碼更容易閱讀
- 4)你可以使用
__new__
、__init__
以及__call__
這樣的特殊方法。它們能幫你處理不同的任務(wù)。就算通常你可以把所有的東西都在__new__
里處理掉,有些人還是覺得用__init__
更舒服些 - 5)哇哦,這東西的名字是metaclass,肯定非善類,我要小心!
6、究竟為什么要使用元類
現(xiàn)在回到我們的主題上來,究竟是為什么你會去使用這樣一種容易出錯且晦澀的特性?好吧,一般來說,你根本就用不上它
Python界的領(lǐng)袖Tim Peters說:
元類就是深度的魔法,99%的用戶應(yīng)該根本不必為此操心。如果你想搞清楚究竟是否需要用到元類,那么你就不需要它。那些實際用到元類的人都非常清楚地知道他們需要做什么,而且根本不需要解釋為什么要用元類
元類的主要用途是創(chuàng)建API。一個典型的例子是Django ORM。它允許你像這樣定義:
class Person(models.Model):
name = models.CharField(max_length=30)
age = models.IntegerField()
但是,如果你這樣做:
person = Person(name='Tom', age='18')
print(person.age)
這并不會返回一個IntegerField對象,而是會返回一個int,甚至可以直接從數(shù)據(jù)庫中取出數(shù)據(jù)。這是有可能的,因為models.Model
定義了__metaclass__
, 并且使用了一些魔法能夠?qū)⒛銊倓偠x的簡單的Person類轉(zhuǎn)變成對數(shù)據(jù)庫的一個復(fù)雜hook。Django框架將這些看起來很復(fù)雜的東西通過暴露出一個簡單的使用元類的API將其化簡,通過這個API重新創(chuàng)建代碼,在背后完成真正的工作
7、結(jié)語
首先,你知道了類其實是能夠創(chuàng)建出類實例的對象。好吧,事實上,類本身也是實例,當(dāng)然,它們是元類的實例
Python中的一切都是對象,它們要么是類的實例,要么是元類的實例,除了type
type實際上是它自己的元類,在純Python環(huán)境中這可不是你能夠做到的,這是通過在實現(xiàn)層面做一些手段實現(xiàn)的
其次,元類是很復(fù)雜的。對于非常簡單的類,你可能不希望通過使用元類來對類做修改。你可以通過其他兩種技術(shù)來修改類:
- monkey patching(猴子補?。?/li>
- class decorators(類裝飾器)
當(dāng)你需要動態(tài)修改類時,99%的時間里你最好使用上面這兩種技術(shù)。當(dāng)然了,其實在99%的時間里你根本就不需要動態(tài)修改類文章來源:http://www.zghlxwxcb.cn/news/detail-715429.html
參考文章:https://stackoverflow.com/questions/100003/what-are-metaclasses-in-python文章來源地址http://www.zghlxwxcb.cn/news/detail-715429.html
到了這里,關(guān)于深入理解Python中的元類的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!