Python類元編程

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è)需求浓若,使用類裝飾器也可以很輕松的辦到(???)渺杉。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市挪钓,隨后出現(xiàn)的幾起案子是越,更是在濱河造成了極大的恐慌,老刑警劉巖碌上,帶你破解...
    沈念sama閱讀 221,273評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件倚评,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡馏予,警方通過(guò)查閱死者的電腦和手機(jī)天梧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)霞丧,“玉大人腿倚,你說(shuō)我怎么就攤上這事◎歉荆” “怎么了敷燎?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,709評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)箩言。 經(jīng)常有香客問(wèn)我硬贯,道長(zhǎng),這世上最難降的妖魔是什么陨收? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,520評(píng)論 1 296
  • 正文 為了忘掉前任饭豹,我火速辦了婚禮,結(jié)果婚禮上务漩,老公的妹妹穿的比我還像新娘拄衰。我一直安慰自己,他們只是感情好饵骨,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布翘悉。 她就那樣靜靜地躺著,像睡著了一般居触。 火紅的嫁衣襯著肌膚如雪妖混。 梳的紋絲不亂的頭發(fā)上老赤,一...
    開(kāi)封第一講書(shū)人閱讀 52,158評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音制市,去河邊找鬼抬旺。 笑死,一個(gè)胖子當(dāng)著我的面吹牛祥楣,可吹牛的內(nèi)容都是我干的开财。 我是一名探鬼主播,決...
    沈念sama閱讀 40,755評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼误褪,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼责鳍!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起振坚,我...
    開(kāi)封第一講書(shū)人閱讀 39,660評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤薇搁,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后渡八,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體啃洋,經(jīng)...
    沈念sama閱讀 46,203評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評(píng)論 3 340
  • 正文 我和宋清朗相戀三年屎鳍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宏娄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,427評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡逮壁,死狀恐怖孵坚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情窥淆,我是刑警寧澤卖宠,帶...
    沈念sama閱讀 36,122評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站忧饭,受9級(jí)特大地震影響扛伍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜词裤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評(píng)論 3 333
  • 文/蒙蒙 一刺洒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吼砂,春花似錦逆航、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,272評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春女揭,著一層夾襖步出監(jiān)牢的瞬間蚤假,已是汗流浹背栏饮。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工吧兔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人袍嬉。 一個(gè)月前我還...
    沈念sama閱讀 48,808評(píng)論 3 376
  • 正文 我出身青樓境蔼,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親伺通。 傳聞我的和親對(duì)象是個(gè)殘疾皇子箍土,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容