繼續上次的進度:https://www.cnblogs.com/flashBoxer/p/9847521.html 正文: 裝飾類 在類中有兩種不通的方式使用裝飾器,第一個和我們之前做過的函數非常相似:在類的方法上應用。這也是當時引入裝飾器的原因之一一些常用的裝飾器已經內置到python中,像@cl ...
繼續上次的進度:https://www.cnblogs.com/flashBoxer/p/9847521.html
正文:
裝飾類
在類中有兩種不通的方式使用裝飾器,第一個和我們之前做過的函數非常相似:在類的方法上應用。這也是當時引入裝飾器的原因之一
一些常用的裝飾器已經內置到python中,像@classmethod @staticmethod @property。這三個裝飾器我們之前都介紹過,這段就不翻譯了(打字手酸,偷懶下)
下麵的Circle 類使用了@classmethod @staticmethod和@property三個裝飾器
class Circle: def __init__(self, radius): self._radius = radius @property def radius(self): """Get value of radius""" return self._radius @radius.setter def radius(self, value): """Set radius, raise error if negative""" if value >= 0: self._radius = value else: raise ValueError("Radius must be positive") @property def area(self): """Calculate area inside circle""" return self.pi() * self.radius**2 def cylinder_volume(self, height): """Calculate volume of cylinder with circle as base""" return self.area * height @classmethod def unit_circle(cls): """Factory method creating a circle with radius 1""" return cls(1) @staticmethod def pi(): """Value of π, could use math.pi instead though""" return 3.1415926535
在這個類中
.cylinder_volume()是一個常規函數
.radius是一個可變屬性:它可以被設置不同的值.然而通過定義setter方法,我們可以做一些判斷來確保它不會被設置成一個沒有意義的負數,.radius作為屬性訪問,不使用括弧
.area 是一個不可變的屬性:沒有.setter()方法的屬性是無法更改的,即使它被定義為一個方法,它也被作為不需要括弧的屬性來使用。
.unit_circle() 是一個類方法。它不被綁定到Circle的實例上.類方法通常用在工廠模式,用來創建類的特殊實例
.pi() 是一個靜態方法.除了命名空間外它不依賴Circle類。靜態方法可以在實例或類上調用。
Circle類的使用例子:
>>> c = Circle(5) >>> c.radius 5 >>> c.area 78.5398163375 >>> c.radius = 2 >>> c.area 12.566370614 >>> c.area = 100 AttributeError: can't set attribute >>> c.cylinder_volume(height=4) 50.265482456 >>> c.radius = -1 ValueError: Radius must be positive >>> c = Circle.unit_circle() >>> c.radius 1 >>> c.pi() 3.1415926535 >>> Circle.pi() 3.1415926535
讓我們定義一個類,在這個類中,我們會用到前面的@debug和@timer裝飾器:
from decorators import debug, timer class TimeWaster: @debug def __init__(self, max_num): self.max_num = max_num @timer def waste_time(self, num_times): for _ in range(num_times): sum([i**2 for i in range(self.max_num)])
看一下結果:
>>> tw = TimeWaster(1000) Calling __init__(<time_waster.TimeWaster object at 0x7efccce03908>, 1000) '__init__' returned None >>> tw.waste_time(999) Finished 'waste_time' in 0.3376 secs
另外一種方式是在整個類上使用裝飾器.這裡有個Python3.7中的dataclasses方法用例:
from dataclasses import dataclass @dataclass class PlayingCard: rank: str suit: str
語法的類似於函數裝飾器。在上面的例子中,也可以通過PlayingCard = dataclass(PlayingCard)來實現。
類裝飾器的一種簡單用法是作為元類方式的替代.在兩種情況下,你都在動態的改變一個類的定義
類的裝飾器和函數的裝飾器語法接近,不同的是裝飾器需要接收一個類而不是一個函數作為參數.事實上,上面的裝飾器都可以作用於類,但當你這麼用的時候,你可能得不到預期的結果。下麵將@timer裝飾器應用到一個類
from decorators import timer @timer class TimeWaster: def __init__(self, max_num): self.max_num = max_num def waste_time(self, num_times): for _ in range(num_times): sum([i**2 for i in range(self.max_num)])
@timer只是TimeWaster = timer(TimeWaster)的縮寫
在這裡@timer只能顯示類實例化需要的時間
>>> tw = TimeWaster(1000) Finished 'TimeWaster' in 0.0000 secs >>> tw.waste_time(999) >>>
在後面會有一個正確的類裝飾器的示例@singleton。它保證一個類只有一個實例
嵌套的裝飾器
可以將多個裝飾器疊加到一個函數上
from decorators import debug, do_twice @debug @do_twice def greet(name): print(f"Hello {name}")
運行的順序會按照疊加的順序, @debug 調用 @do_twice @do_twice 調用greet(),或者debug(do_twice(greet()))
>>> greet("Eva") Calling greet('Eva') Hello Eva Hello Eva 'greet' returned None
更改@debug和@do_twice的順序:
from decorators import debug, do_twice @do_twice @debug def greet(name): print(f"Hello {name}")
在這種情況下,@do_twice也會被應用到@debug中:
>>> greet("Eva") Calling greet('Eva') Hello Eva 'greet' returned None Calling greet('Eva') Hello Eva 'greet' returned None
帶參數的裝飾器
在需要傳參給你的裝飾器是這個例子會非常有用。例如,@do_twice可以擴展到@repeat(num_times)裝飾器.然後,可以將執行的被裝飾函數的次數作為參數給出。
可以這麼做:
@repeat(num_times=4) def greet(name): print(f"Hello {name}") >>> greet("World") Hello World Hello World Hello World Hello World
考慮下如何實現這個功能
到目前為止,寫在@後面寫的名字引用一個可以被另外一個函數調用的函數對象,需要repeat(num_times=4)來返回一個函數對象,這個對象可以被作為裝飾器,幸運的是,我們已經知道如何返回函數!一般來說,需要以下內容:
def repeat(num_times): def decorator_repeat(func): ... # Create and return a wrapper function return decorator_repeat
通常,裝飾器創建並返回一個內部包裝函數,所以完整地寫出這個例子會給你一個內部函數
def repeat(num_times): def decorator_repeat(func): @functools.wraps(func) def wrapper_repeat(*args, **kwargs): for _ in range(num_times): value = func(*args, **kwargs) return value return wrapper_repeat return decorator_repeat
例子看起來有點亂,但我們只是添加了一個def來接收參數,這個裝飾器語法我們之前處理過多次.讓我們從最裡面的函數開始:
def wrapper_repeat(*args, **kwargs): for _ in range(num_times): value = func(*args, **kwargs) return value
wrapper_repeat()函數接收任意參數,並放回被裝飾函數的值,func(). 這個包裝函數還包括了被裝飾函數num_times的迴圈 ,除了必須要使用外部參數num_times外,和之前看到的裝飾器函數沒有什麼不同,
再走一步,你就會發現裝飾器函數:
def decorator_repeat(func): @functools.wraps(func) def wrapper_repeat(*args, **kwargs): ... return wrapper_repeat
decorator_repeat()和我們之前寫的裝飾器函數非常像,除了他的名字不同,因為我們為最外層的函數保留了基礎名稱repeat(),這個是用戶要調用的函數。
最外層返回裝飾器函數的引用
def repeat(num_times): def decorator_repeat(func): ... return decorator_repeat
在repeat()中有一些細節:
將decorator_repeat()作為一個內部函數意味著repeat()將引用一個函數對象-decotator_repeat.之前,我們用沒有括弧的repeat來引用函數對象.定義帶有參數的裝飾器,就需要添加括弧
num_times參數看起來沒有在repeat()本身中使用,但是通過傳遞num_times,會創建一個閉包,來存儲num_times的值,直到wrapper_repeat()使用它為止。
一切就緒後,讓我們看看結果:
@repeat(num_times=4) def greet(name): print(f"Hello {name}") >>> greet("World") Hello World Hello World Hello World Hello World
這是我們想要的結果
Both Please, But Never Mind the Bread
稍微註意下.你可以把裝飾器同時定義為帶參數或者不帶參數.你可能不需要這樣,但更有靈活性也不錯
前面已經看到,當裝飾器需要參數的時候,需要有一個額外的外部函數,困難在於,代碼需要知道裝飾器是否被調用了,是否有參數
因為只有在沒有參數的情況下調用裝飾器時才會直接傳遞裝飾的函數,這個函數必須是可選參數.意味著裝飾器參數必須要友關鍵字指定,可以使用特殊的*,也就是說,下麵的參數都是關鍵字
def name(_func=None, *, kw1=val1, kw2=val2, ...): # 1 def decorator_name(func): ... # Create and return a wrapper function. if _func is None: return decorator_name # 2 else: return decorator_name(_func) # 3
_func參數是一個標記,提示裝飾器被調用的時候是否有參數
1.如果name調用的時候沒有傳參,被裝飾函數會被作為_func傳入.如果有參數傳入,_func會被置為None,一些關鍵字參數可能已不再是預設值, 參數列表中的*表示其餘參數不能作為位置參數調用。
2.裝飾器可以傳參調用,返回一個裝飾器函數,它可以讀取和返回一個函數
3.裝飾器不可以傳參調用,會只將裝飾器應用到函數上
改造下之前的@repeat裝飾器
def repeat(_func=None, *, num_times=2): def decorator_repeat(func): @functools.wraps(func) def wrapper_repeat(*args, **kwargs): for _ in range(num_times): value = func(*args, **kwargs) return value return wrapper_repeat if _func is None: return decorator_repeat else: return decorator_repeat(_func)
和之前的對比,唯一的變化是在末尾添加了_func參數和if-else。
這些例子表明,@repeat現在可以在有或沒有參數的情況下使用:
@repeat def say_whee(): print("Whee!") @repeat(num_times=3) def greet(name): print(f"Hello {name}")
預設情況num_times的值是2
>>> say_whee() Whee! Whee! >>> greet("Penny") Hello Penny Hello Penny Hello Penny
有狀態的裝飾器
有時候,可以跟蹤狀態的裝飾器也是很有用的.一個簡單的例子,我們會創建一個統計函數調用次數的裝飾器
註意:在教程的前面,我們討論了基於給定參數返回值的純函數.有狀態的裝飾器正好相反,返回值取決於當前狀態以及給定的參數。
在下一節中,您將看到如何使用類來保持狀態。但在簡單的情況下,也可以使用函數屬性:
import functools def count_calls(func): @functools.wraps(func) def wrapper_count_calls(*args, **kwargs): wrapper_count_calls.num_calls += 1 print(f"Call {wrapper_count_calls.num_calls} of {func.__name__!r}") return func(*args, **kwargs) wrapper_count_calls.num_calls = 0 return wrapper_count_calls @count_calls def say_whee(): print("Whee!")
狀態——函數的調用次數——存儲在包裹函數(wrapper_count_calls)的函數屬性.num_calls中。下麵是使用它的效果:
>>> say_whee() Call 1 of 'say_whee' Whee! >>> say_whee() Call 2 of 'say_whee' Whee! >>> say_whee.num_calls 2
類裝飾器
典型的維護狀態的方式是使用類。在本節中,將看到如何重寫@count_calls的例子來實現類裝飾器
回想一下,裝飾器語法@my_decorator只是func = my_decorator(func)一種方便快捷的用法.因此,如果my_decorator是一個類,需要在它的.__init__方法中接收func作為一個參數.而且,這個類需要是可以被調用的,這樣它就可以替代裝飾器函數了
如果需要一個類可以被調用,要實現.__call__方法(看示例:https://www.cnblogs.com/flashBoxer/tag/python/)
class Counter: def __init__(self, start=0): self.count = start def __call__(self): self.count += 1 print(f"Current count is {self.count}")
.__call__方法每次運行都會嘗試調用一個類的實例:
>>> counter = Counter() >>> counter() Current count is 1 >>> counter() Current count is 2 >>> counter.count 2
因此,實現類裝飾器需要實現.__init__和.__call__
import functools class CountCalls: def __init__(self, func): functools.update_wrapper(self, func) self.func = func self.num_calls = 0 def __call__(self, *args, **kwargs): self.num_calls += 1 print(f"Call {self.num_calls} of {self.func.__name__!r}") return self.func(*args, **kwargs) @CountCalls def say_whee(): print("Whee!")
.__init__方法必須可以存儲一個函數的引用和能夠做一些必要的初始化. 調用.__call__方法來替代裝飾器函數.它做的和我們之前的 wrapper()函數基本一樣,註意,這裡使用functools.update_wrapper()函數,而不是@functools.wraps
這個@CountCalls裝飾器的工作原理與前一節相同:
>>> say_whee() Call 1 of 'say_whee' Whee! >>> say_whee() Call 2 of 'say_whee' Whee! >>> say_whee.num_calls 2
更多現實中的例子
我們已經學到了很多(看了下翻譯的行數量,已經1K+了,確實很多),已經學會如何創建各種各樣的裝飾師,把我們的新知識應用到創建更多的示例中,這些示例在現實中可能非常有用。
代碼降速,重新訪問
我們之前實現的@slow_down一直是保持sleep 1秒.現在你知道瞭如何給裝飾器添加參數,因此,讓我們來重寫@slow_down,使用一個可選的rate參數來控制它的sleep時間:
import functools import time def slow_down(_func=None, *, rate=1): """Sleep given amount of seconds before calling the function""" def decorator_slow_down(func): @functools.wraps(func) def wrapper_slow_down(*args, **kwargs): time.sleep(rate) return func(*args, **kwargs) return wrapper_slow_down if _func is None: return decorator_slow_down else: return decorator_slow_down(_func)
我們使用 Both Please, But Never Mind the Bread 這裡的樣例來讓@slow_down有參數和沒有參數時都可調用,countdown()函數現在在每次計數之間休眠2秒:
@slow_down(rate=2) def countdown(from_number): if from_number < 1: print("Liftoff!") else: print(from_number) countdown(from_number - 1
和前面一樣,你最好自己寫寫,跑下看看結果
>>> countdown(3) 3 2 1 Liftoff!
創建單例模式
單例模式是一個只有一個實例的類.在Python經常使用的單例對象包括None,True和False.可以使用is來比較,像我們之前在Both Please的章節中:
if _func is None: return decorator_name else: return decorator_name(_func)
is只對完全相同實例的對象返回True。下麵的@singleton裝飾器將類的第一個實例存儲為屬性,從而將類轉換為單例對象。之後創建實例只是返回已經存儲的實例:
import functools def singleton(cls): """Make a class a Singleton class (only one instance)""" @functools.wraps(cls) def wrapper_singleton(*args, **kwargs): if not wrapper_singleton.instance: wrapper_singleton.instance = cls(*args, **kwargs) return wrapper_singleton.instance wrapper_singleton.instance = None return wrapper_singleton @singleton class TheOne: pass
這個類裝飾器和我們的函數裝飾器基本一樣.唯一不同的地方在於使用cls代替了fun來表示這是一個類裝飾器
看下運行結果:
>>> first_one = TheOne() >>> another_one = TheOne() >>> id(first_one) 140094218762280 >>> id(another_one) 140094218762280 >>> first_one is another_one True
很明顯,first_one確實與另一個實例完全相同。
緩存返回值
裝飾器可以提供很方便的緩存和記憶機制.作為一個例子,我們來看看斐波那契數列的遞歸定義:
from decorators import count_calls @count_calls def fibonacci(num): if num < 2: return num return fibonacci(num - 1) + fibonacci(num - 2)
實現很簡單,性能很糟糕
>>> fibonacci(10) <Lots of output from count_calls> 55 >>> fibonacci.num_calls 177
為了計算第10個斐波那契數,你實際上只需要計算前面的斐波那契數,但是這個實現需要177次計算。更糟糕的是:斐波納契數列(20)需要21891次計算,第30次需要270萬次計算。這是因為代碼一直在重新計算已知的斐波那契數。
通常的解決方案是使用for迴圈和查找表來實現斐波那契數。但是,簡單的計算緩存也可以做到這一點:
import functools from decorators import count_calls def cache(func): """Keep a cache of previous function calls""" @functools.wraps(func) def wrapper_cache(*args, **kwargs): cache_key = args + tuple(kwargs.items()) if cache_key not in wrapper_cache.cache: wrapper_cache.cache[cache_key] = func(*args, **kwargs) return wrapper_cache.cache[cache_key] wrapper_cache.cache = dict() return wrapper_cache @cache @count_calls def fibonacci(num): if num < 2: return num return fibonacci(num - 1) + fibonacci(num - 2)
緩存作為查找表工作,所以現在fibonacci()只執行一次計算:
>>> fibonacci(10) Call 1 of 'fibonacci' ... Call 11 of 'fibonacci' 55 >>> fibonacci(8) 21
註意,在對fibonacci(8)的最後調用中,沒有進行新的計算,因為fibonacci(10)已經計算了第8個fibonacci數。
在標準庫中,提供了@functools.lru_cache。
這個裝飾器比上面的例子要具備更多特性.我們應該使用@functools.lru_cache來代替我們自己寫的緩存裝飾器
import functools @functools.lru_cache(maxsize=4) def fibonacci(num): print(f"Calculating fibonacci({num})") if num < 2: return num return fibonacci(num - 1) + fibonacci(num - 2)
maxsize參數指定緩存了多少次調用。預設值是128,但是可以指定maxsize=None來緩存所有函數調用。但是,請註意,如果正在緩存許多很大的對象,這可能會導致記憶體問題。
可以使用.cache_info()方法查看緩存的執行情況,併在需要時進行調優。在我們的示例中,我們設定一個小maxsize來查看從緩存中刪除元素的效果:
>>> fibonacci(10) Calculating fibonacci(10) Calculating fibonacci(9) Calculating fibonacci(8) Calculating fibonacci(7) Calculating fibonacci(6) Calculating fibonacci(5) Calculating fibonacci(4) Calculating fibonacci(3) Calculating fibonacci(2) Calculating fibonacci(1) Calculating fibonacci(0) 55 >>> fibonacci(8) 21 >>> fibonacci(5) Calculating fibonacci(5) Calculating fibonacci(4) Calculating fibonacci(3) Calculating fibonacci(2) Calculating fibonacci(1) Calculating fibonacci(0) 5 >>> fibonacci(8) Calculating fibonacci(8) Calculating fibonacci(7) Calculating fibonacci(6) 21 >>> fibonacci(5) 5 >>> fibonacci.cache_info() CacheInfo(hits=17, misses=20, maxsize=4, currsize=4)
添加單元信息
下麵的示例與前面的Registering Plugins示例有點類似,因為它不會真正改變被裝飾函數的行為。相反,它只是將unit添加為函數屬性:
def set_unit(unit): """Register a unit on a function""" def decorator_set_unit(func): func.unit = unit return func return decorator_set_unit
下麵的示例根據圓柱體的半徑和高度(以釐米為單位)來計算體積:
import math @set_unit("cm^3") def volume(radius, height): return math.pi * radius**2 * height 這個.unit函數屬性是可以訪問的: >>> volume(3, 5) 141.3716694115407 >>> volume.unit 'cm^3'
註意,可以使用函數註釋實現類似的功能:
import math def volume(radius, height) -> "cm^3": return math.pi * radius**2 * height
但是,由於註釋用於類型提示,因此很難將註釋和靜態類型檢查相結合。
在連接到一個能夠在單位間轉換的庫,單位可以變得更加強大和有趣.pip install pint, 您可以將體積轉換為立方英寸或加侖:
>>> import pint >>> ureg = pint.UnitRegistry() >>> vol = volume(3, 5) * ureg(volume.unit) >>> vol <Quantity(141.3716694115407, 'centimeter ** 3')> >>> vol.to("cubic inches") <Quantity(8.627028576414954, 'inch ** 3')> >>> vol.to("gallons").m # Magnitude 0.0373464440537444
你還可以修改裝飾器來直接返回一個Pint數量.數量是通過與單位相乘得到的,在pint中,units必須只能在UnitRegistry中查詢.這裡註冊用來存儲函數屬性來避免命名空間混亂
def use_unit(unit): """Have a function return a Quantity with given unit""" use_unit.ureg = pint.UnitRegistry() def decorator_use_unit(func): @functools.wraps(func) def wrapper_use_unit(*args, **kwargs): value = func(*args, **kwargs) return value * use_unit.ureg(unit) return wrapper_use_unit return decorator_use_unit @use_unit("meters per second") def average_speed(distance, duration): return distance / duration
使用@use_unit裝飾器,轉換單位實際上是很容易
>>> bolt = average_speed(100, 9.58) >>> bolt <Quantity(10.438413361169102, 'meter / second')> >>> bolt.to("km per hour") <Quantity(37.578288100208766, 'kilometer / hour')> >>> bolt.to("mph").m # Magnitude 23.350065679064745
驗證JSON
讓我們看最後一個用例。快速看下Flask路由的管理程式:
@app.route("/grade", methods=["POST"]) def update_grade(): json_data = request.get_json() if "student_id" not in json_data: abort(400) # Update database return "success!"
這裡我們確保key student_id是請求的一部分.雖然驗證有效,但它實際上並不屬於函數本身.另外,可能還有其他使用相同驗證的路由。因此,讓我們Don't repeat yourself,來使用裝飾器抽象出任何不必要的邏輯,下麵的@validate_json裝飾器會完成這個工作:
from flask import Flask, request, abort import functools app = Flask(__name__) def validate_json(*expected_args): # 1 def decorator_validate_json(func): @functools.wraps(func) def wrapper_validate_json(*args, **kwargs): json_object = request.get_json() for expected_arg in expected_args: # 2 if expected_arg not in json_object: abort(400) return func(*args, **kwargs) return wrapper_validate_json return decorator_validate_json
在上面的代碼中,裝飾器採用了一個可變長度列表作為參數,這樣我們就可以傳遞儘可能多的字元串參數,每個參數都代表一個用於驗證JSON數據的鍵:
1.json的keys列表作為參數傳遞給裝飾器
2.包裹函數驗證JSON數據中出現的每個預期鍵
然後,路由管理程式可以關註其真正的業務級別——因為它可以安全地假設JSON數據是有效的:
@app.route("/grade", methods=["POST"]) @validate_json("student_id") def update_grade(): json_data = request.get_json() # Update database. return "success!"
結束語:翻譯就到這裡吧,這篇文章的作者對裝飾器的理解很是深入,文章很長,翻起來確實花了不少時間。文中如果有翻譯不穩妥的地方,請留言給我。最後老鐵們如果覺得對理解python的裝飾器有幫助,右下角點個贊吧,結尾附上原文地址:https://realpython.com/primer-on-python-decorators/