文章目錄
裝飾器的高級應用
上一篇博客裏講到了python裝飾器語法的基本功能,這裏我們談一談裝飾器比較fancy的用法。
類上的裝飾器
裝飾器用來類定義上,有兩種用法。第一種就是直接用在類成員函數的前一行,用來修飾該成員函數。另外還有一類python內置的裝飾器,例如@classmethod, @staticmethod, @property。
@classmethod和@staticmethod用來定義該方法屬於類的命名空間內部,不能被類的實例直接訪問。
@property裝飾器則可以使得成員函數當作屬性一樣被調用。和@property裝飾器非常相關的還有兩個概念,getter和setter等。
首先來看下面的代碼:
import matplotlib.pyplot as plt
import numpy as np
class Circle(object):
def __init__(self, radius):
self._radius = radius
def cylinder_volume(self,height):
"""Calculate volume of cylinder with circle as base"""
return self.area * height
@property
def diameter(self):
return self._radius * 2
@diameter.setter
def diameter(self, new_diameter):
if new_diameter > 0:
self._radius = new_diameter / 2
else:
raise ValueError("Diameter should be positive!")
@property
def radius(self):
return self._radius
@radius.setter
def radius(self,value):
if value > 0 :
self._radius = value
else:
raise ValueError("Radius should be positive!")
@classmethod
def unit_circle(cls):
"""Factory method creating a circle with radius 1"""
returl cls(1)
@staticmethod
def draw(radius):
x = radius*np.sin(np.linspace(0,2*np.pi,100))
y = radius*np.cos(np.linspace(0,2*np.pi,100))
plt.plot(x,y,'r')
plt.axis('equal')
my_circle = Circle(2)
print('radius is {}'.format(my_circle.radius))
print('diameter is {}'.format(my_circle.diameter))
#change the radius into 6
my_circle.radius = 6
print('radius is {}'.format(my_circle.radius))
print('diameter is {}'.format(my_circle.diameter))
#change the diameter into 6
my_circle.diameter = 6
print('radius is {}'.format(my_circle.radius))
print('diameter is {}'.format(my_circle.diameter))
radius is 2
diameter is 4
radius is 6
diameter is 12
radius is 3.0
diameter is 6.0
值得一提的是,Circle類unit_circle()的參數是cls。對於通常的成員函數,第一個參數都是實例的引用self,但是如果這個成員函數是用@classmethod修飾的類方法,第一個參數必須是關鍵字cls。
在Circle類定義中:
- cylinder_volume是常規的成員函數;
- @property裝飾符修飾成員函數diameter()和radius(),會自動生成對應的diameter和radius屬性,當調用獲取相應值時,就調用該函數
- @diameter.setter或者@radius.setter裝飾器,可以用=賦值的方式設置對應屬性的新值。
- @classmethod修飾的unit_circle成員函數是一個類方法,類方法不與具體的類實例相綁定,通常用來作爲類的默認設置(factory methods)
- @staticmethod裝飾器用來修飾類的靜態方法,表明這種方法可以不需要定義實例而直接調用“classname.static_func(paras)”。例如:
class Math:
@staticmethod
def factorial(number):
if number == 0:
return 1
else:
return number * Math.factorial(number - 1)
factorial = Math.factorial(5)
print(factorial)
輸出: 120
我們再看看@statemethod修飾的draw函數:
%matplotlib inline
Circle.draw(3)
作爲靜態方法,draw可以不用定義類的實例,直接用類名.靜態方法()
的方式調用。
另外裝飾器還可以直接裝飾類。例如python3.7中的dataclasses模塊。關於這個模塊,見參考文檔:https://docs.python.org/3/library/dataclasses.html
from dataclasses import dataclass
@dataclass
class PlayingCard:
rank:str
suit:str
PlayingCard = dataclass(PlayingCard)
當用裝飾器修飾一個類的時候,其實就賦予了這個類的定義靈活動態改變的能力,這時候裝飾器的作用有些類似於元類metaclass。
定義針對類的裝飾器與針對函數的裝飾器非常相似,唯一的不同就是類的裝飾器輸入參數應該是類。
import functools
import time
def timer(func):
"""Print the run time of the decorated function."""
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Fininshed {func.__name__} in {run_time:.4f} secs")
return value
return wrapper_timer
@timer
class TimeWaster():
def __init__(self,max_num):
self.max_num = max_num
def waste_time(self,num):
for _ in range(num):
sum([i**3 for i in range(self.max_num)])
tw = TimeWaster(5000)
tw.waste_time(100)
Fininshed TimeWaster in 0.0000 secs
雖然編譯器沒有報錯,但運行結果錯了。下面的例子中,給出了類的裝飾器正確的寫法,可以和上面函數的裝飾器仔細對比一下:
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
first = TheOne()
second = TheOne()
print(f"first id = {id(first)}")
print(f"second_id = {id(second)}")
first id = 4800667280
second_id = 4800667280
nesting decorators 裝飾器的嵌入
裝飾器的嵌入方式,就是在一個函數前用多個裝飾器進行修飾,例如下面的例子就是用了debug和do_twice兩個裝飾器,執行的時候按照裝飾器定義的先後,從外到內逐層嵌入,debug(do_twice(greet(name)))
from decorators import debug, do_twice
@debug
@do_twice
def greet(name):
print(f"Hello {name}")
得到輸出:
greet("Eva")
Calling greet('Eva')
Hello Eva
Hello Eva
'greet' returned None
帶參數的裝飾器
根據需要,可以給裝飾器傳遞參數以控制對修飾的行爲。例如設計一個裝飾器,將函數的輸出重複指定的次數,應該如何實現呢?
import functools
def repeat(num):
def decorator_repeat(func):
@functools.wraps(func)
def wrapper_repeat(*args, **kwargs):
val = 0
for _ in range(num):
val += func(*args, **kwargs)
return val
return wrapper_repeat
return decorator_repeat
@repeat(num=4)
def running(km):
print(f'you have run {km} kilometers!')
return km*70
print("Weight loss daily plans:")
calories = running(10)
print(f'total calorie consumption is: {calories}')
Weight loss daily plans:
you have run 10 kilometers!
you have run 10 kilometers!
you have run 10 kilometers!
you have run 10 kilometers!
total calorie consumption is: 2800
狀態追蹤裝飾器(stateful decorator)
裝飾器模式也被用於追蹤函數的狀態,我們可以用stateful decorator來跟蹤被修飾函數總共執行了多少次。下面的例子中實現了一個“支付-找零”的決策系統,並且設置了3次是支付的最大嘗試次數。我們可以用stateful decorator跟蹤支付函數pay執行的次數,count次數被設置爲裝飾圖的屬性,隨着函數的執行次數逐漸累計,並可以用被修飾函數名pay.count的方式調用。
import functools
def count_calls(func):
@functools.wraps(func)
def wrapper_count(*args, **kwargs):
wrapper_count.count += 1
val = func(*args, **kwargs)
return val
wrapper_count.count = 0
return wrapper_count
@count_calls
def pay(money,price):
print(f"Thanks, you have paid {price} yuan")
return money-price
def payment(money,price):
print(f"You have to pay {price} yuan.")
if pay.count < 3:
change = pay(money, price)
if change >= 0:
print(f'Here is your change, {change} yuan')
else:
print(f'Sorry, your need to pay {abs(change)} yuan more.')
else:
print("Sorry,your payment chance has reached the limit of 3 times!")
print()
payment(2,10)
payment(5,10)
payment(6,10)
payment(8,10)
You have to pay 10 yuan.
Thanks, you have paid 10 yuan
Sorry, your need to pay 8 yuan more.
You have to pay 10 yuan.
Thanks, you have paid 10 yuan
Sorry, your need to pay 5 yuan more.
You have to pay 10 yuan.
Thanks, you have paid 10 yuan
Sorry, your need to pay 4 yuan more.
You have to pay 10 yuan.
Sorry,your payment chance has reached the limit of 3 times!
裝飾器類(classes as decorators)
歸功於python極高的靈活性,我們還可以定義裝飾器類。我們回憶一下類的定義,和前面講過的裝飾器函數,可以肯定的是在裝飾器類的初始化函數__init__()的輸入參數,必須是一個函數名。除此之外,裝飾器類還必須是callable的。
class Counter:
def __init__(self, start=0):
self.count = start
def __call__(self):
self.count += 1
print(f"Current count is {self.count}")
c = Counter()
c()
print(c.count)
c()
print(c.count)
Current count is 1
1
Current count is 2
2
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!")
say_whee()
say_whee()
Call 1 of 'say_whee'
Whee!
Call 2 of 'say_whee'
Whee!
參考:
[1] cls和self的區別:https://medium.com/@gmotzespina/method-types-in-python-2c95d46281cd
[2] Python中property屬性(特性)的理解:https://blog.csdn.net/u014745194/article/details/70432673