1. 程式人生 > 實用技巧 >Python類超程式設計

Python類超程式設計

什麼是類超程式設計

類超程式設計是指動態地建立或定製類,也就是在執行時根據不同的條件生成符合要求的類,一般來說,類超程式設計的主要方式有類工廠函式,類裝飾器和元類。

建立類的另一種方式

通常,我們都是使用 class 關鍵字來宣告一個類,像這樣:

class A:
    name = 'A'

但是,我們還有另外一種方式來生成我們的類,下述程式碼與上面作用相同:

A = type('A', (object,), {'name': 'A'})

一般情況下我們把 type 視作函式,呼叫 type(obj) 來獲取 obj 物件所屬的類。然而,type 是一個類(或者說,元類,後面會介紹),傳入三個引數(類名,父類元組,屬性列表)便可以新建一個類。至於類如何像函式一樣使用,只需要實現 __call__

特殊方法即可。

類工廠函式

在Python中,類是一等物件,因此任何時候都可以使用函式建立類,而無需使用 class 關鍵字。

通常,我們定義一個類需要用到 class 關鍵字,比如一個簡單的 Dog 類:

class Dog:
    def __init__(self, name, age, owner):
        self.name = name
        self.age = age
        self.owner = owner

這樣一個簡單的類,我們將每個欄位的名字都寫了三遍,並且想要獲得友好的字串表示形式還得再次編寫 __str__ 或者 __repr__

方法,那麼有沒有簡單的方法即時建立這樣的簡單類呢?答案是有的。受到標準庫中的類工廠函式——collections.namedtuple的啟發,我們可以實現這樣一個類似的工廠函式來建立簡單類:

Dog = create_class('Dog', 'name age owner')

實現這樣的工廠函式的思路也很簡單,切分出屬性名後呼叫 type 新建類並返回即可:

def create_class(name, fields):

    # 物件的屬性元組
    fields = tuple(fields.replace(',', ' ').split())
    
    def __init__(self, *args, **kwargs):
        # {屬性名:初始化值}
        attrs = dict(zip(self.__slots__, args))
        # 關鍵字引數
        attrs.update(kwargs)
        for name, value in attrs.items():
            # 相當於 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)

利用這樣的類工廠函式可以很方便的創建出類似Dog的簡單類,並且擁有了友好的字串表示形式:

>>> Dog = create_class('Dog', 'name age owner')
>>> dog = Dog('R', 2, 'assassin')
>>> dog
Dog(name=R, age=2, owner=assassin)

類裝飾器

類裝飾器也是函式,與一般的裝飾器不同的是引數為類,用來審查,修改,甚至把被裝飾的類替換成其他類。讓我們寫一個給類新增 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

利用類裝飾器可以對傳入的類做各種修改以達到使用需求。類裝飾器的缺點就是隻對直接依附的類有效,這意味著子類有可能繼承也有可能不繼承被裝飾效果,這取決於裝飾器中所做的改動。

元類

除非開發框架,否則不要編寫元類——然而,為了尋找樂趣,或者練習相關概念,可以這麼做。

——《流暢的Python》

一句話理解,元類就是用於構建類的類。

預設情況下,類都是 type 的例項,也就是說, type 是大多數內建類和自定義類的元類。 type 是一個神奇的存在,它是自身的例項,而在 type 和 object 之間,type 是 object 的子類,object 是 type 的例項。

這些神奇的關係可以不用關注,編寫元類一定要明白的是:所有類都是 type 的例項,但只有元類同時還是 type 的子類,所以元類從 type 繼承了構建類的能力,這就是我們編寫元類的依據,具體來說,元類通過實現 __init____new__ 方法來定製類,他們的區別如下:

__init__ 被稱為構造方法是從其他語言借鑑過來的術語,其實用於構建例項的是 __new__ 這是個特殊處理的類方法,必須返回一個例項,作為第一個引數傳給 __init__ 方法,而 __init__ 禁止返回任何值,所以其實應該是“初始化方法”。從 __new____init__ 並不是必須的,因為 __new__ 方法非常強大,甚至可以返回其他例項,這時候不會呼叫 __init__ 方法。

所以,一般情況下我們想利用元類來對類進行審查,修改屬性時實現 __init__ 方法即可,而如果需要根據已有類構造新類時就需要實現 __new__ 方法。

元類最常用在框架中,例如 ORM 就會用到元類,當我們宣告一個類並使用了框架提供的元類時,元類會做這些事:

  • 讀取使用者類名作為表名

  • 建立屬性名和列名的對映關係

  • __new__ 方法中建立新的類,儲存有表名和屬性與列的對映關係

ORM 元類的編寫比較複雜,我以另外一個例子說明元類的使用方法。在《Python3網路爬蟲開發實戰》一書代理池的例子中,我們需要實現一個爬蟲類來爬取各個代理網站的代理,這個類的結構是這樣的:

class Crawler():
    def get_proxies(self, crawl_func):
        '''執行指定方法來獲取代理'''
        pass
    
    def crawl_1(self):
        '''爬取網站1的資料'''
        pass
    
    def crawl_2(self):
        '''爬取網站2的資料'''
        pass

我們在爬蟲類中定義了一系列針對各個網站的爬取方法,並定義了一個 get 方法來爬取指定的網站,我們希望可以隨時新增可爬取的網站,只需要新增以 crawl_ 開頭的方法。要實現這樣的功能,很明顯這樣是不夠的,因為我們不知道一共有哪些 crawl_ 開頭的爬取方法,如果再用另外的方式手動記錄又很麻煩,並且有忘記更新記錄的隱患存在。學習了元類後,我們可以很輕鬆的在爬蟲類中新增屬性來自動記錄其中的爬取方法,像下面這樣:

class ProxyMetaClass(type):
    '''元類,初始化類時記錄所有以crawl_開頭的方法'''
    
    # 第一個引數為元類的例項,後面三個與 type 用到的三個引數相同
    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


# 爬蟲類,指定元類後會自動呼叫元類進行構建
class Crawler(metaclass=ProxyMetaClass):
    def get_proxies(self, crawl_func):
        '''執行指定方法來獲取代理'''
        pass

    def crawl_1(self):
        '''爬取網站1的資料'''
        pass
    
    def crawl_2(self):
        '''爬取網站2的資料'''
        pass

這樣後面工作的時候就可以呼叫 crawler.crawl_funcs 獲取所有的 func 然後按個呼叫 crawler.get_proxies(func) 進行爬取。

最後,元類功能強大但是難以掌握,類裝飾器能以更簡單的方式解決很多問題,比如上面這個需求,使用類裝飾器也可以很輕鬆的辦到(¬‿¬)。