帶引數的全型別 Python 裝飾器
這篇短文中顯示的程式碼取自我的小型開源專案按合同設計,它提供了一個型別化的裝飾器。裝飾器是一個非常有用的概念,你肯定會在網上找到很多關於它們的介紹。簡單說,它們允許在每次呼叫裝飾函式時(之前和之後)執行程式碼。通過這種方式,你可以修改函式引數或返回值、測量執行時間、新增日誌記錄、執行執行時型別檢查等等。請注意,裝飾器也可以為類編寫,提供另一種超程式設計方法(例如在 attrs 包中完成)
在最簡單的形式中,裝飾器的定義類似於以下程式碼:
def my_first_decorator(func): def wrapped(*args, **kwargs): # do something before result = func(*args, **kwargs) # do something after return result return wrapped @my_first_decorator def func(a): return a
如上程式碼,因為當定義了被包裝的巢狀函式時,它的周圍變數可以在函式內訪問並儲存在記憶體中,只要該函式在某處使用(這在函數語言程式設計語言中稱為閉包)。
很簡單, 但是這有一些缺點。最大的問題是修飾函式會丟失它的之前的函式名字(你可以用inspect.signature看到這個),它的文件字串,甚至它的名字, 這些是原始碼文件工具(例如 sphinx)的問題,但可以使用標準庫中的 functools.wraps 裝飾器輕鬆解決:
from functools import wraps from typing import Any, Callable, TypeVar, ParamSpec P = ParamSpec("P") # 需要python >= 3.10 R = TypeVar("R") def my_second_decorator(func: Callable[P, R]) -> Callable[P, R]: @wraps(func) def wrapped(*args: Any, **kwargs: Any) -> R: # do something before result = func(*args, **kwargs) # do something after return result return wrapped @my_second_decorator def func2(a: int) -> int: """Does nothing""" return a print(func2.__name__) # 'func2' print(func2.__doc__) # 'Does nothing'
在這個例子中,我已經添加了型別註釋,註釋和型別提示是對 Python 所做的最重要的補充。更好的可讀性、IDE 中的程式碼完成以及更大程式碼庫的可維護性只是其中的幾個例子。上面的程式碼應該已經涵蓋了大多數用例,但無法引數化裝飾器。考慮編寫一個裝飾器來記錄函式的執行時間,但前提是它超過了一定的秒數。這個數量應該可以為每個裝飾函式單獨配置。如果沒有指定,則應使用預設值,並且應使用不帶括號的裝飾器,以便更易於使用:
@time(threshold=2) def func1(a): ... # No paranthesis when using default threshold @time def func2(b): ...
如果你可以在第二種情況下使用括號,或者根本不提供引數的預設值,那麼這個祕訣就足夠了:
from functools import wraps from typing import Any, Callable, TypeVar, ParamSpec P = ParamSpec("P") # 需要python >= 3.10 R = TypeVar("R") def my_third_decorator(threshold: int = 1) -> Callable[[Callable[P, R]], Callable[P, R]]: def decorator(func: Callable[P, R]) -> Callable[P, R]: @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> R: # do something before you can use `threshold` result = func(*args, **kwargs) # do something after return result return wrapper return decorator @my_third_decorator(threshold=2) def func3a(a: int) -> None: ... # works @my_third_decorator() def func3b(a: int) -> None: ... # Does not work! @my_third_decorator def func3c(a: int) -> None: ...
為了涵蓋第三種情況,有一些包,即 wraps 和 decorator,它們實際上可以做的不僅僅是新增可選引數。雖然質量非常高,但它們引入了相當多的額外複雜性。使用 wrapt-decorated 函式,在遠端叢集上執行函式時,我進一步遇到了序列化問題。據我所知,兩者都沒有完全鍵入,因此靜態型別檢查器/ linter(例如 mypy)在嚴格模式下失敗。
當我在自己的包上工作並決定編寫自己的解決方案時,必須解決這些問題。它變成了一種可以輕鬆重用但很難轉換為庫的模式。
它使用標準庫的過載裝飾器。這樣,可以指定相同的裝飾器與我們的無引數一起使用。除此之外,它是上面兩個片段的組合。這種方法的一個缺點是所有引數都需要作為關鍵字引數給出(這畢竟增加了可讀性)
from typing import Callable, TypeVar, ParamSpec from functools import partial, wraps P = ParamSpec("P") # requires python >= 3.10 R = TypeVar("R @overload def typed_decorator(func: Callable[P, R]) -> Callable[P, R]: ... @overload def typed_decorator(*, first: str = "x", second: bool = True) -> Callable[[Callable[P, R]], Callable[P, R]]: ... def typed_decorator( func: Optional[Callable[P, R]] = None, *, first: str = "x", second: bool = True ) -> Union[Callable[[Callable[P, R]], Callable[P, R]], Callable[P, R]]: """ Describe what the decorator is supposed to do! Parameters ---------- first : str, optional First argument, by default "x". This is a keyword-only argument! second : bool, optional Second argument, by default True. This is a keyword-only argument! """ def wrapper(func: Callable[P, R], *args: Any, **kw: Any) -> R: """The actual logic""" # Do something with first and second and produce a `result` of type `R` return result # Without arguments `func` is passed directly to the decorator if func is not None: if not callable(func): raise TypeError("Not a callable. Did you use a non-keyword argument?") return wraps(func)(partial(wrapper, func)) # With arguments, we need to return a function that accepts the function def decorator(func: Callable[P, R]) -> Callable[P, R]: return wraps(func)(partial(wrapper, func)) return decorator
稍後,我們可以分別使用我們的不帶引數的裝飾器
@typed_decorator def spam(a: int) -> int: return a @typed_decorator(first = "y def eggs(a: int) -> int: return a
這種模式肯定有一些開銷,但收益大於成本。
原文: http://lemonfold.io/posts/2022/dbc/typed_decorator/
- Spring中實現非同步呼叫的方式有哪些?
- 帶引數的全型別 Python 裝飾器
- 整理了幾個Python正則表示式,拿走就能用!
- SOLID:開閉原則Go程式碼實戰
- React中如何引入CSS呢
- 一個新視角:前端框架們都卷錯方向了?
- 編碼中的Adapter,不僅是一種設計模式,更是一種架構理念與解決方案
- 手寫程式語言-遞迴函式是如何實現的?
- 一文搞懂模糊匹配:定義、過程與技術
- 新來個阿里 P7,僅花 2 小時,做出一個多執行緒永動任務,看完直接跪了
- Puzzlescript,一種開發H5益智遊戲的引擎
- @Autowired和@Resource到底什麼區別,你明白了嗎?
- CSS transition 小技巧!如何保留 hover 的狀態?
- React如此受歡迎離不開這4個主要原則
- LeCun再炮轟Marcus: 他是心理學家,不是搞AI的
- Java保證執行緒安全的方式有哪些?
- 19個殺手級 JavaScript 單行程式碼,讓你看起來像專業人士
- Python 的"self"引數是什麼?
- 別整一坨 CSS 程式碼了,試試這幾個實用函式
- 再有人問你什麼是MVCC,就把這篇文章發給他!