1. 什么是類元編程
類元編程是指動(dòng)態(tài)地創(chuàng)建或定制類,也就是在運(yùn)行時(shí)根據(jù)不同的條件生成符合要求的類禾进,一般來(lái)說(shuō)豁跑,類元編程的主要方式有類工廠函數(shù),類裝飾器和元類泻云。
2. 創(chuàng)建類的另一種方式
通常艇拍,我們都是使用 class 關(guān)鍵字來(lái)聲明一個(gè)類,像這樣:
class A:
name = 'A'
但是壶愤,我們還有另外一種方式來(lái)生成類淑倾,下述代碼與上面作用相同:
A = type('A', (object,), {'name': 'A'})
一般情況下我們把 type 視作函數(shù)馏鹤,調(diào)用 type(obj)
來(lái)獲取 obj 對(duì)象所屬的類征椒。然而,type 是一個(gè)類(或者說(shuō)湃累,元類勃救,后面會(huì)介紹),傳入三個(gè)參數(shù)(類名治力,父類元組蒙秒,屬性列表)便可以新建一個(gè)類。至于類如何像函數(shù)一樣使用宵统,只需要實(shí)現(xiàn) __call__
特殊方法即可晕讲。
3. 類工廠函數(shù)
在Python中,類是一等對(duì)象,因此任何時(shí)候都可以使用函數(shù)創(chuàng)建類瓢省,而無(wú)需使用 class 關(guān)鍵字弄息。
通常,我們定義一個(gè)類需要用到 class 關(guān)鍵字勤婚,比如一個(gè)簡(jiǎn)單的 Dog 類:
class Dog:
def __init__(self, name, age, owner):
self.name = name
self.age = age
self.owner = owner
這樣一個(gè)簡(jiǎn)單的類摹量,我們將每個(gè)字段的名字都寫(xiě)了三遍,并且想要獲得友好的字符串表示形式還得再次編寫(xiě) __str__
或者 __repr__
方法馒胆,那么有沒(méi)有簡(jiǎn)單的方法即時(shí)創(chuàng)建這樣的簡(jiǎn)單類呢缨称?答案是有的。受到標(biāo)準(zhǔn)庫(kù)中的類工廠函數(shù)——collections.namedtuple的啟發(fā)祝迂,我們可以實(shí)現(xiàn)這樣一個(gè)類似的工廠函數(shù)來(lái)創(chuàng)建簡(jiǎn)單類:
Dog = create_class('Dog', 'name age owner')
實(shí)現(xiàn)這樣的工廠函數(shù)的思路也很簡(jiǎn)單睦尽,切分出屬性名后調(diào)用 type 新建類并返回即可:
def create_class(name, fields):
# 對(duì)象的屬性元組
fields = tuple(fields.replace(',', ' ').split())
def __init__(self, *args, **kwargs):
# {屬性名:初始化值}
attrs = dict(zip(self.__slots__, args))
# 關(guān)鍵字參數(shù)
attrs.update(kwargs)
for name, value in attrs.items():
# 相當(dāng)于 self.name = value
setattr(self, name, value)
def __repr__(self):
values = []
for i in self.__slots__:
# {屬性名=屬性值}
values.append(f'{i}={getattr(self, i)}')
values = ', '.join(values)
return f'{self.__class__.__name__}({values})'
class_attrs = {
'__slots__': fields,
'__init__': __init__,
'__repr__': __repr__
}
return type(name, (object,), class_attrs)
利用這樣的類工廠函數(shù)可以很方便的創(chuàng)建出類似Dog的簡(jiǎn)單類,并且擁有了友好的字符串表示形式:
>>> Dog = create_class('Dog', 'name age owner')
>>> dog = Dog('R', 2, 'assassin')
>>> dog
Dog(name=R, age=2, owner=assassin)
4. 類裝飾器
類裝飾器也是函數(shù)型雳,與一般的裝飾器不同的是參數(shù)為類骂删,用來(lái)審查,修改四啰,甚至把被裝飾的類替換成其他類宁玫。讓我們寫(xiě)一個(gè)給類添加 cls_name 屬性的裝飾器吧:
def add_name(cls):
setattr(cls, 'cls_name', cls.__name__)
return cls
@add_name
class Dog:
def __init__(self, name, age, owner):
self.name = name
self.age = age
self.owner = owner
利用類裝飾器可以對(duì)傳入的類做各種修改以達(dá)到使用需求。類裝飾器的缺點(diǎn)就是只對(duì)直接依附的類有效柑晒,這意味著子類有可能繼承也有可能不繼承被裝飾效果欧瘪,這取決于裝飾器中所做的改動(dòng)。
5. 元類
除非開(kāi)發(fā)框架匙赞,否則不要編寫(xiě)元類——然而佛掖,為了尋找樂(lè)趣,或者練習(xí)相關(guān)概念涌庭,可以這么做芥被。
——《流暢的Python》
一句話理解,元類就是用于構(gòu)建類的類坐榆。
默認(rèn)情況下拴魄,類都是 type 的實(shí)例,也就是說(shuō)席镀, type 是大多數(shù)內(nèi)置類和自定義類的元類匹中。 type 是一個(gè)神奇的存在,它是自身的實(shí)例豪诲,而在 type 和 object 之間顶捷,type 是 object 的子類,object 是 type 的實(shí)例屎篱。
前面這些神奇的關(guān)系可以不用關(guān)注服赎,但是編寫(xiě)元類一定要明白的是:所有類都是 type 的實(shí)例葵蒂,但只有元類同時(shí)還是 type 的子類,所以元類從 type 繼承了構(gòu)建類的能力重虑,這就是我們編寫(xiě)元類的依據(jù)刹勃,具體來(lái)說(shuō),元類通過(guò)實(shí)現(xiàn) __init__
和 __new__
方法來(lái)定制類嚎尤,他們的區(qū)別如下:
__init__
被稱為構(gòu)造方法是從其他語(yǔ)言借鑒過(guò)來(lái)的術(shù)語(yǔ)荔仁,其實(shí)用于構(gòu)建實(shí)例的是__new__
,這是個(gè)特殊處理的類方法芽死,必須返回一個(gè)實(shí)例乏梁,作為第一個(gè)參數(shù)傳給__init__
方法,而__init__
禁止返回任何值关贵,所以其實(shí)應(yīng)該叫“初始化方法”遇骑。從__new__
到__init__
并不是必須的,因?yàn)?__new__
方法非常強(qiáng)大揖曾,甚至可以返回其他實(shí)例落萎,這時(shí)候不會(huì)調(diào)用__init__
方法。——《流暢的Python》
所以炭剪,一般情況下我們想利用元類來(lái)對(duì)類進(jìn)行審查练链,修改屬性時(shí)實(shí)現(xiàn) __init__
方法即可,而如果需要根據(jù)已有類構(gòu)造新類時(shí)就需要實(shí)現(xiàn) __new__
方法奴拦。
元類最常用在框架中媒鼓,例如 ORM 就會(huì)用到元類,當(dāng)我們聲明一個(gè)類并使用了框架提供的元類時(shí)错妖,元類會(huì)做這些事:
讀取用戶類名作為表名
創(chuàng)建屬性名和列名的映射關(guān)系
在
__new__
方法中創(chuàng)建新的類绿鸣,保存有表名和屬性與列的映射關(guān)系
ORM 元類的編寫(xiě)比較復(fù)雜,我以另外一個(gè)例子說(shuō)明元類的使用方法暂氯。在《Python3網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)》一書(shū)代理池的例子中潮模,我們需要實(shí)現(xiàn)一個(gè)爬蟲(chóng)類來(lái)爬取各個(gè)代理網(wǎng)站的代理,這個(gè)類的結(jié)構(gòu)是這樣的:
class Crawler():
def get_proxies(self, crawl_func):
'''執(zhí)行指定方法來(lái)獲取代理'''
pass
def crawl_1(self):
'''爬取網(wǎng)站1的數(shù)據(jù)'''
pass
def crawl_2(self):
'''爬取網(wǎng)站2的數(shù)據(jù)'''
pass
我們?cè)谂老x(chóng)類中定義了一系列針對(duì)各個(gè)網(wǎng)站的爬取方法痴施,并定義了一個(gè) get 方法來(lái)爬取指定的網(wǎng)站擎厢,我們希望可以隨時(shí)添加可爬取的網(wǎng)站,只需要添加以 crawl_ 開(kāi)頭的方法晾剖。要實(shí)現(xiàn)這樣的功能锉矢,很明顯這樣是不夠的,因?yàn)槲覀儾恢酪还灿心男?crawl_ 開(kāi)頭的爬取方法齿尽,如果再用另外的方式手動(dòng)記錄又很麻煩,并且有忘記更新記錄的隱患存在灯节。學(xué)習(xí)了元類后循头,我們可以很輕松的在爬蟲(chóng)類中添加屬性來(lái)自動(dòng)記錄其中的爬取方法绵估,像下面這樣:
class ProxyMetaClass(type):
'''元類,初始化類時(shí)記錄所有以crawl_開(kāi)頭的方法'''
# 第一個(gè)參數(shù)為元類的實(shí)例卡骂,后面三個(gè)與 type 用到的三個(gè)參數(shù)相同
def __init__(cls, name, bases, attrs):
count = 0
crawl_funcs = []
for k, _ in attrs.items():
if 'crawl_' in k:
crawl_funcs.append(k)
count += 1
# 添加屬性
cls.crawl_func_count = count
cls.crawl_funcs = crawl_funcs
# 爬蟲(chóng)類国裳,指定元類后會(huì)自動(dòng)調(diào)用元類進(jìn)行構(gòu)建
class Crawler(metaclass=ProxyMetaClass):
def get_proxies(self, crawl_func):
'''執(zhí)行指定方法來(lái)獲取代理'''
pass
def crawl_1(self):
'''爬取網(wǎng)站1的數(shù)據(jù)'''
pass
def crawl_2(self):
'''爬取網(wǎng)站2的數(shù)據(jù)'''
pass
這樣后面工作的時(shí)候就可以調(diào)用 crawler.crawl_funcs
獲取所有的 func 然后按個(gè)調(diào)用 crawler.get_proxies(func)
進(jìn)行爬取。
最后全跨,元類功能強(qiáng)大但是難以掌握缝左,類裝飾器能以更簡(jiǎn)單的方式解決很多問(wèn)題,比如上面這個(gè)需求浓若,使用類裝飾器也可以很輕松的辦到(???)渺杉。