淺談Python的元編程
相應(yīng)的元編程就是描述代碼本身的代碼,元編程就是關(guān)于創(chuàng)建操作源代碼(比如修改、生成或包裝原來(lái)的代碼)的函數(shù)和類(lèi)。主要技術(shù)是使用裝飾器、元類(lèi)、描述符類(lèi)。
一、裝飾器
裝飾器就是函數(shù)的函數(shù),它接受一個(gè)函數(shù)作為參數(shù)并返回一個(gè)新的函數(shù),在不改變?cè)瓉?lái)函數(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ù)) 只需要在原來(lái)的函數(shù)上面加上 @timeit() 即可為其增加新的功能:
@timeit()
def test_timeit():
time.sleep(1)
test_timeit()
#test_timeit cost 1.0026 seconds
上面的代碼跟下面這樣寫(xiě)的效果是一樣的:
test_timeit = timeit(test_timeit) test_timeit()
二、裝飾器的執(zhí)行順序
當(dāng)有多個(gè)裝飾器的時(shí)候,他們的調(diào)用順序是怎么樣的?
假如有這樣的代碼,請(qǐng)問(wèn)是先打印 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è)問(wèn)題之前,我先給你打個(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ù),也可以定義為類(lèi),只要你確保它實(shí)現(xiàn)了__call__() 和 __get__() 方法。
關(guān)于裝飾器的其他用法,可以參考前文:
- 我是裝飾器
- 再談裝飾器
三、元類(lèi)
Python 中所有類(lèi)(object)的元類(lèi),就是 type 類(lèi),也就是說(shuō) Python 類(lèi)的創(chuàng)建行為由默認(rèn)的 type 類(lèi)控制,打個(gè)比喻,type 類(lèi)是所有類(lèi)的祖先。我們可以通過(guò)編程的方式來(lái)實(shí)現(xiàn)自定義的一些對(duì)象創(chuàng)建行為。
定一個(gè)類(lèi)繼承 type 類(lèi) A,然后讓其他類(lèi)的元類(lèi)指向 A,就可以控制 A 的創(chuàng)建行為。典型的就是使用元類(lèi)實(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!!!")
元類(lèi) Singleton 的__init__和__new__ 方法會(huì)在定義 Spam 的期間被執(zhí)行,而 __call__方法會(huì)在實(shí)例化 Spam 的時(shí)候執(zhí)行。
如果想更好的理解元類(lèi),可以閱讀Python黑魔法之metaclass詳情
四、descriptor 類(lèi)(描述符類(lèi))
descriptor 就是任何一個(gè)定義了 __get__(),__set__()或 __delete__()的對(duì)象,描述器讓對(duì)象能夠自定義屬性查找、存儲(chǔ)和刪除的操作。這里舉官方文檔[1]一個(gè)自定義驗(yàn)證器的例子。
定義驗(yàn)證器類(lèi),它是一個(gè)描述符類(lèi),同時(shí)還是一個(gè)抽象類(lèi):
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 < self.minvalue:
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)證用戶(hù)定義的 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) < self.minsize:
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í)這樣寫(xiě):
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
描述器阻止無(wú)效實(shí)例的創(chuàng)建:
>>> Component('Widget', 'metal', 5) # Blocked: 'Widget' is not all uppercase
Traceback (most recent call last):
...
ValueError: Expected <method '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
五、總結(jié)
關(guān)于 Python 的元編程,總結(jié)如下:
如果希望某些函數(shù)擁有相同的功能,希望不改變?cè)械恼{(diào)用方式、不寫(xiě)重復(fù)代碼、易維護(hù),可以使用裝飾器來(lái)實(shí)現(xiàn)。
如果希望某一些類(lèi)擁有某些相同的特性,或者在類(lèi)定義實(shí)現(xiàn)對(duì)其的控制,我們可以自定義一個(gè)元類(lèi),然后讓它類(lèi)的元類(lèi)指向該類(lèi)。
如果希望實(shí)例的屬性擁有某些共同的特點(diǎn),就可以自定義一個(gè)描述符類(lèi)。
到此這篇關(guān)于淺談Python的元編程的文章就介紹到這了,更多相關(guān)Python的元編程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python實(shí)現(xiàn)兩個(gè)經(jīng)緯度點(diǎn)之間的距離和方位角的方法
今天小編就為大家分享一篇python實(shí)現(xiàn)兩個(gè)經(jīng)緯度點(diǎn)之間的距離和方位角的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-07-07
Pandas格式化DataFrame的浮點(diǎn)數(shù)列的實(shí)現(xiàn)
本文主要介紹了Pandas格式化DataFrame的浮點(diǎn)數(shù)列的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-05-05
Python?查看數(shù)據(jù)類(lèi)型與格式
這篇文章主要介紹了Python?查看數(shù)據(jù)類(lèi)型與格式方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09
Python編程中用close()方法關(guān)閉文件的教程
這篇文章主要介紹了Python編程中用close()方法關(guān)閉文件的教程,是Python編程入門(mén)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-05-05
Python+OpenCV實(shí)現(xiàn)車(chē)牌字符分割和識(shí)別
這篇文章主要為大家詳細(xì)介紹了Python+OpenCV實(shí)現(xiàn)車(chē)牌字符分割和識(shí)別,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03
PyTorch中torch.tensor與torch.Tensor的區(qū)別詳解
這篇文章主要介紹了PyTorch中torch.tensor與torch.Tensor的區(qū)別詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05
python常用數(shù)據(jù)重復(fù)項(xiàng)處理方法
在本篇文章里小編給大家整理的是關(guān)于python常用數(shù)據(jù)重復(fù)項(xiàng)處理方法,需要的朋友們參考下。2019-11-11
python pytest進(jìn)階之conftest.py詳解
這篇文章主要介紹了python pytest進(jìn)階之conftest.py詳解,如果我們?cè)诰帉?xiě)測(cè)試用的時(shí)候,每一個(gè)測(cè)試文件里面的用例都需要先登錄后才能完成后面的操作,那么們?cè)撊绾螌?shí)現(xiàn)呢?這就需要我們掌握conftest.py文件的使用了,需要的朋友可以參考下2019-06-06

