說說 Python 的元編程

提到元這個(gè)字,你也許會(huì)想到元數(shù)據(jù),元數(shù)據(jù)就是描述數(shù)據(jù)本身的數(shù)據(jù),元類就是類的類,相應(yīng)的元編程就是描述代碼本身的代碼,元編程就是關(guān)于創(chuàng)建操作源代碼(比如修改、生成或包裝原來的代碼)的函數(shù)和類。主要技術(shù)是使用裝飾器、元類、描述符類。
本文的主要目的是向大家介紹這些元編程技術(shù),并且給出實(shí)例來演示它們是怎樣定制化源代碼的行為。
裝飾器
裝飾器就是函數(shù)的函數(shù),它接受一個(gè)函數(shù)作為參數(shù)并返回一個(gè)新的函數(shù),在不改變?cè)瓉砗瘮?shù)代碼的情況下為其增加新的功能,比如最常用的計(jì)時(shí)裝飾器:
from?functools?import?wraps
def?timeit(logger=None):
????"""
????耗時(shí)統(tǒng)計(jì)裝飾器,單位是秒,保留?4?位小數(shù)
????"""
????def?decorator(func):
????????@wraps(func)
????????def?wrapper(*args,?**kwargs):
????????????start?=?time.time()
????????????result?=?func(*args,?**kwargs)
????????????end?=?time.time()
????????????if?logger:
????????????????logger.info(f"{func.__name__}?cost?{end?-?start?:.4f}?seconds")
????????????else:
????????????????print(f"{func.__name__}?cost?{end?-?start?:.4f}?seconds")
????????????return?result
????????return?wrapper
????return?decorator
(注:比如上面使用 @wraps(func) 注解是很重要的, 它能保留原始函數(shù)的元數(shù)據(jù))
只需要在原來的函數(shù)上面加上 @timeit() 即可為其增加新的功能:
@timeit()
def?test_timeit():
????time.sleep(1)
test_timeit()
#test_timeit?cost?1.0026?seconds
上面的代碼跟下面這樣寫的效果是一樣的:
test_timeit?=?timeit(test_timeit)
test_timeit()
裝飾器的執(zhí)行順序
當(dāng)有多個(gè)裝飾器的時(shí)候,他們的調(diào)用順序是怎么樣的?
假如有這樣的代碼,請(qǐng)問是先打印 Decorator1 還是 Decorator2 ?
from?functools?import?wraps
def?decorator1(func):
????@wraps(func)
????def?wrapper(*args,?**kwargs):
????????print('Decorator?1')
????????return?func(*args,?**kwargs)
????return?wrapper
def?decorator2(func):
????@wraps(func)
????def?wrapper(*args,?**kwargs):
????????print('Decorator?2')
????????return?func(*args,?**kwargs)
????return?wrapper
@decorator1
@decorator2
def?add(x,?y):
????return?x?+?y
add(1,2)
#?Decorator?1
#?Decorator?2
回答這個(gè)問題之前,我先給你打個(gè)形象的比喻,裝飾器就像函數(shù)在穿衣服,離它最近的最先穿,離得遠(yuǎn)的最后穿,上例中 decorator1 是外套,decorator2 是內(nèi)衣。
add?=?decorator1(decorator2(add))
在調(diào)用函數(shù)的時(shí)候,就像脫衣服,先解除最外面的 decorator1,也就是先打印 Decorator1,執(zhí)行到 return func(*args, **kwargs) 的時(shí)候會(huì)去解除 decorator2,然后打印 Decorator2,再次執(zhí)行到 return func(*args, **kwargs) 時(shí)會(huì)真正執(zhí)行 add() 函數(shù)。
需要注意的是打印的位置,如果打印字符串的代碼位于調(diào)用函數(shù)之后,像下面這樣,那輸出的結(jié)果正好相反:
def?decorator1(func):
????@wraps(func)
????def?wrapper(*args,?**kwargs):
????????result?=?func(*args,?**kwargs)
????????print('Decorator?1')
????????return?result
????return?wrapper
def?decorator2(func):
????@wraps(func)
????def?wrapper(*args,?**kwargs):
????????result?=?func(*args,?**kwargs)
????????print('Decorator?2')
????????return?result
????return?wrapper
裝飾器不僅可以定義為函數(shù),也可以定義為類,只要你確保它實(shí)現(xiàn)了__call__() 和 __get__() 方法。
關(guān)于裝飾器的其他用法,可以參考前文:
元類
Python 中所有類(object)的元類,就是 type 類,也就是說 Python 類的創(chuàng)建行為由默認(rèn)的 type 類控制,打個(gè)比喻,type 類是所有類的祖先。我們可以通過編程的方式來實(shí)現(xiàn)自定義的一些對(duì)象創(chuàng)建行為。
定一個(gè)類繼承 type 類 A,然后讓其他類的元類指向 A,就可以控制 A 的創(chuàng)建行為。典型的就是使用元類實(shí)現(xiàn)一個(gè)單例:
class?Singleton(type):
????def?__init__(self,?*args,?**kwargs):
????????self._instance?=?None
????????super().__init__(*args,?**kwargs)
????def?__call__(self,?*args,?**kwargs):
????????if?self._instance?is?None:
????????????self._instance?=?super().__call__(*args,?**kwargs)
????????????return?self._instance
????????else:
????????????return?self._instance
class?Spam(metaclass=Singleton):
????def?__init__(self):
????????print("Spam!!!")
元類 Singleton 的__init__和__new__ 方法會(huì)在定義 Spam 的期間被執(zhí)行,而 __call__方法會(huì)在實(shí)例化 Spam 的時(shí)候執(zhí)行。
如果想更好的理解元類,可以閱讀Python黑魔法之metaclass
descriptor 類(描述符類)
descriptor 就是任何一個(gè)定義了 __get__(),__set__()或 __delete__()的對(duì)象,描述器讓對(duì)象能夠自定義屬性查找、存儲(chǔ)和刪除的操作。這里舉官方文檔[1]一個(gè)自定義驗(yàn)證器的例子。
定義驗(yàn)證器類,它是一個(gè)描述符類,同時(shí)還是一個(gè)抽象類:
from?abc?import?ABC,?abstractmethod
class?Validator(ABC):
????def?__set_name__(self,?owner,?name):
????????self.private_name?=?'_'?+?name
????def?__get__(self,?obj,?objtype=None):
????????return?getattr(obj,?self.private_name)
????def?__set__(self,?obj,?value):
????????self.validate(value)
????????setattr(obj,?self.private_name,?value)
????@abstractmethod
????def?validate(self,?value):
????????pass
自定義驗(yàn)證器需要從 Validator 繼承,并且必須提供 validate() 方法以根據(jù)需要測(cè)試各種約束。
這是三個(gè)實(shí)用的數(shù)據(jù)驗(yàn)證工具:
OneOf 驗(yàn)證值是一組受約束的選項(xiàng)之一。
class?OneOf(Validator):
????def?__init__(self,?*options):
????????self.options?=?set(options)
????def?validate(self,?value):
????????if?value?not?in?self.options:
????????????raise?ValueError(f'Expected?{value!r}?to?be?one?of?{self.options!r}')
Number 驗(yàn)證值是否為 int 或 float。根據(jù)可選參數(shù),它還可以驗(yàn)證值在給定的最小值或最大值之間。
class?Number(Validator):
????def?__init__(self,?minvalue=None,?maxvalue=None):
????????self.minvalue?=?minvalue
????????self.maxvalue?=?maxvalue
????def?validate(self,?value):
????????if?not?isinstance(value,?(int,?float)):
????????????raise?TypeError(f'Expected?{value!r}?to?be?an?int?or?float')
????????if?self.minvalue?is?not?None?and?value?????????????raise?ValueError(
????????????????f'Expected?{value!r}?to?be?at?least?{self.minvalue!r}'
????????????)
????????if?self.maxvalue?is?not?None?and?value?>?self.maxvalue:
????????????raise?ValueError(
????????????????f'Expected?{value!r}?to?be?no?more?than?{self.maxvalue!r}'
????????????)
String 驗(yàn)證值是否為 str。根據(jù)可選參數(shù),它可以驗(yàn)證給定的最小或最大長(zhǎng)度。它還可以驗(yàn)證用戶定義的 predicate。
class?String(Validator):
????def?__init__(self,?minsize=None,?maxsize=None,?predicate=None):
????????self.minsize?=?minsize
????????self.maxsize?=?maxsize
????????self.predicate?=?predicate
????def?validate(self,?value):
????????if?not?isinstance(value,?str):
????????????raise?TypeError(f'Expected?{value!r}?to?be?an?str')
????????if?self.minsize?is?not?None?and?len(value)?????????????raise?ValueError(
????????????????f'Expected?{value!r}?to?be?no?smaller?than?{self.minsize!r}'
????????????)
????????if?self.maxsize?is?not?None?and?len(value)?>?self.maxsize:
????????????raise?ValueError(
????????????????f'Expected?{value!r}?to?be?no?bigger?than?{self.maxsize!r}'
????????????)
????????if?self.predicate?is?not?None?and?not?self.predicate(value):
????????????raise?ValueError(
????????????????f'Expected?{self.predicate}?to?be?true?for?{value!r}'
????????????)
實(shí)際應(yīng)用時(shí)這樣寫:
class?Component:
????name?=?String(minsize=3,?maxsize=10,?predicate=str.isupper)
????kind?=?OneOf('wood',?'metal',?'plastic')
????quantity?=?Number(minvalue=0)
????def?__init__(self,?name,?kind,?quantity):
????????self.name?=?name
????????self.kind?=?kind
????????self.quantity?=?quantity
描述器阻止無效實(shí)例的創(chuàng)建:
>>>?Component('Widget',?'metal',?5)??????#?Blocked:?'Widget'?is?not?all?uppercase
Traceback?(most?recent?call?last):
????...
ValueError:?Expected?'isupper'?of?'str'?objects>?to?be?true?for?'Widget'
>>>?Component('WIDGET',?'metle',?5)??????#?Blocked:?'metle'?is?misspelled
Traceback?(most?recent?call?last):
????...
ValueError:?Expected?'metle'?to?be?one?of?{'metal',?'plastic',?'wood'}
>>>?Component('WIDGET',?'metal',?-5)?????#?Blocked:?-5?is?negative
Traceback?(most?recent?call?last):
????...
ValueError:?Expected?-5?to?be?at?least?0
>>>?Component('WIDGET',?'metal',?'V')????#?Blocked:?'V'?isn't?a?number
Traceback?(most?recent?call?last):
????...
TypeError:?Expected?'V'?to?be?an?int?or?float
>>>?c?=?Component('WIDGET',?'metal',?5)??#?Allowed:??The?inputs?are?valid
最后的話
關(guān)于 Python 的元編程,總結(jié)如下:
如果希望某些函數(shù)擁有相同的功能,希望不改變?cè)械恼{(diào)用方式、不寫重復(fù)代碼、易維護(hù),可以使用裝飾器來實(shí)現(xiàn)。
如果希望某一些類擁有某些相同的特性,或者在類定義實(shí)現(xiàn)對(duì)其的控制,我們可以自定義一個(gè)元類,然后讓它類的元類指向該類。
如果希望實(shí)例的屬性擁有某些共同的特點(diǎn),就可以自定義一個(gè)描述符類。
參考資料
管方文檔: https://docs.python.org/zh-cn/3/howto/descriptor.html

還不過癮?試試它們
▲趣漫畫:Java 對(duì) Python 的滲透能成功嗎?
▲Python 之父爆料:明年至少令 Python 提速 1 倍!
