讓Python更優雅更易讀(第二集)
友情鏈接
1.裝飾器
1.1裝飾器特別適合用來實現以下功能
- 運行時校驗:在執行階段進行特定校驗,當校驗通不過時終止執行。 適合原因:裝飾器可以方便地在函數執行前介入,並且可以讀取所有參數輔助校驗。
- 注入額外參數:在函數被調用時自動注入額外的調用參數。適合原因:裝飾器的位置在函數頭部,非常靠近參數被定義的位置,關聯性強。
- 快取執行結果:通過調用參數等輸入資訊,直接快取函數執行結果。
- 註冊函數:將被裝飾函數註冊為某個外部流程的一部分。適合原因:在定義函數時可以直接完成註冊,關聯性強。
- 替換為複雜對象:將原函數(方法)替換為更複雜的對象,比如類實例或特殊的描述符對象
1.2裝飾器簡單實現
import time def cal_time(func): def wrapper(*args,**kwargs): t1=time.time() result=func(*args,**kwargs) t2=time.time() print(f"{func.__name__} running time: {t2-t1} secs.") return result return wrapper
cal_time裝飾器接收待裝飾函數func作為唯一的位置參數,並在函數內定義了一個新函數:wrapper。
@cal_time def second2(): time.sleep(2) second2()#second2 running time: 2.0001144409179688 secs.
一個無參數裝飾器,實現起來較為簡單。假如你想實現一個接收參數的裝飾器,程式碼會更複雜一些。
import time def cal_time(print_args=False): def decorator(func): def wrapper(*args,**kwargs): t1=time.time() result=func(*args,**kwargs) t2=time.time() if print_args: print(f'args: {args},kwargs:{kwargs}') print(f"{func.__name__} running time: {t2-t1} secs.") return result return wrapper return decorator @cal_time(print_args=True) def second2(): time.sleep(2) second2() #args: (),kwargs:{} #second2 running time: 2.0001144409179688 secs.
#先進行一次調用,傳入裝飾器參數,獲得第一層內嵌函數 #進行第二次調用,獲取第二層內嵌函數wrapper _decorator = cal_time(print_args=True) sleepTime = _decorator(second2)
1.3使用functools.wraps()修飾包裝函數
def calls_counter(func): """裝飾器:記錄函數被調用多少次""" counter = 0 def decorated(*args, **kwargs): nonlocal counter counter +=1 return func(*args,**kwargs) def print_counter(): print(f'counter:{counter}') #給函數增加額外函數,列印統計函數被調用的次數 decorated.print_counter = print_counter return decorated @cal_time() @calls_counter def second2(): time.sleep(2)
這是一個記錄函數被調用多少次的裝飾器
我們發現當我們同時使用上述兩個裝飾器的時候報錯了
Traceback (most recent call last): File "F:/pythonProject1/AutomaticTesting/single.py", line 33, in <module> second2.print_counter() AttributeError: 'function' object has no attribute 'print_counter'
首先,由calls_counter對函數進行包裝,此時的second2變成了新的包裝函數,包含print_counter屬性
使用cal_time包裝後,second2變成了cal_time提供的包裝函數,原包裝函數額外的print_counter屬性被自然地丟掉了
要解決上述問題只要引入裝飾器wraps就可以了
import time from functools import wraps def cal_time(print_args=False): def decorator(func): @wraps(func) def wrapper(*args,**kwargs): ... def calls_counter(func): """裝飾器:記錄函數被調用多少次""" counter = 0 @wraps(func) def decorated(*args, **kwargs): ... @cal_time() @calls_counter def second2(): time.sleep(2) # second2() second2.print_counter() #second2 running time: 2.0001144409179688 secs. #counter:1
1.4可選參數的裝飾器
以上數的cal_time為例
有了參數以後我們不僅在裝飾器使用時候@必須帶上()
def cal_time(func=None,*,print_args=False): def decorator(_func): @wraps(_func) def wrapper(*args,**kwargs): t1=time.time() result=func(*args,**kwargs) t2=time.time() if print_args: print(f'args: {args},kwargs:{kwargs}') print(f"{_func.__name__} running time: {t2-t1} secs.") return result return wrapper if func is None: return decorator else: return decorator(func)
@cal_time
@calls_counter
def second2():
time.sleep(2)
這時候調用就不需要()了
1.5用類來實現裝飾器(函數替換)
能否用裝飾器形式使用只有一個判斷標準,就是是否是可調用的對象
如果一個類實現了__call__魔法方法,那麼他的實例就是可調用對象
現在我們把計時裝飾器改寫
import time from functools import wraps class cal_time: """裝飾器:記錄函數用時""" def __init__(self,print_arg=False): self.print_arg = print_arg def __call__(self, func): @wraps(func) def wrapper(*args,**kwargs): t1=time.time() result=func(*args,**kwargs) t2=time.time() if self.print_arg: print(f'args: {args},kwargs:{kwargs}') print(f"{func.__name__} running time: {t2-t1} secs.") return result return wrapper
2數據模型與描述符
數據模型有關的方法,基本都以雙下劃線開頭和結尾,它們通常被稱為魔法方法
例如:我們列印對象的時候輸出的是<類名+記憶體地址>
class Person: def __init__(self, name): self.name = name print(Person("yetangjian"))#<__main__.Person object at 0x000001BA41805FD0>
__str__就是Python數據模型里最基礎的一部分。當對象需要當作字元串使用時,我們可以用__str__方法來定義對象的字元串化結果
註:除了print()以外,str()與.format()函數同樣也會觸發__str__方法
class Person: ... def __str__(self): return self.name print(Person("yetangjian")) #yetangjian print(f'l am {Person("yetangjian")}') #l am yetangjian
常見魔法方法
01. __repr__
在如下的例子中,使用了一個{name!r}這樣的語法
變數名後的!r表示優先使用repr方法,再使用str方法。針對字元串類型會自動給變數加上引號,省去了手動添加的麻煩。
name='yetangjian' age = 18 print(f"{name!r},{age!r}")#'yetangjian',18
同樣我們實現的方法與str方法類似,我們依舊使用上述的例子
class Person: ... def __repr__(self): return f"{self.name!r},{self.age!r}" p=Person("yetangjian",80) print(repr(p))#'yetangjian',80
02.__format__
定義對象在字元串格式化時的行為
class Person:
...
def __format__(self, format_spec):
if format_spec == "all":
return f"{self.name!r},{self.age!r}"
else:
return f"{self.name!r}"
p=Person("yetangjian",80)
print(f"all:{p:all}") #all:'yetangjian',80
print("only name:{p:simple}".format(p=p)) #only name:'yetangjian'
模板語法不僅適用於format,同樣適用於f-string
03比較運算符重載
class Num: def __init__(self,number): self.n = number #等於 def __eq__(self, other): if isinstance(other,self.__class__): return other.n == self.n return False #不等於 def __ne__(self, other): return not (self == other) def __lt__(self, other): if isinstance(other,self.__class__): return self.n < other.n #不支援某種運算,可以返回NotImplemented return NotImplemented #小於等於 def __le__(self, other): return self.__lt__(other) or self.__eq__(other) num1 = Num(5) num2 = Num(10) print(num1 <= num2) #True
但是我們會發現重載這些運算符號程式碼量實在太大,而且較為重複。下面推薦一個工具,簡化這個工作量
@total_ordering
使用functools下的這個裝飾器,我們只需要實現__eq__方法,__lt__、__le__、__gt__、__ge__四個方法里隨意挑一個實現即可,@total_ordering會幫你自動補全剩下的所有方法
from functools import total_ordering @total_ordering class Num: def __init__(self,number): self.n = number #等於 def __eq__(self, other): if isinstance(other,self.__class__): return other.n == self.n return False def __lt__(self, other): if isinstance(other,self.__class__): return self.n < other.n #不支援某種運算,可以返回NotImplemented return NotImplemented num1 = Num(5) num2 = Num(10) print(num1 <= num2) #True
描述符
使用property做校驗
class Count: def __init__(self,c): self.__math = c @property def math(self): return self.__math @math.setter def math(self,v): if v > 50: raise ValueError("數字大於100") self.__math = v c = Count(5) c.math = 40 print(c.math) #40
描述符(descriptor)是Python對象模型里的一種特殊協議,它主要和4個魔法方法有關: __get__、__set__、__delete__和__set_name__
任何一個實現了__get__、__set__或__delete__的類,都可以稱為描述符類,它的實例則叫作描述符對象
__get__
class Info: def __get__(self, instance, owner=None): """ __get__方法存在兩個參數 instance:當通過實例來訪問描述符屬性,該參數為實例對象; 如果通過類訪問,則為None owner:描述符對象所綁定的類 """ print(f'__get__,{instance},{owner}') if not instance: return self class Foo: #要使用一個描述符,最常見的方式是把它的實例對象設置為其他類(常被稱為owner類)的屬性 bar = Info() print(Foo.bar) print(Foo().bar)
""" 通過類來訪問,所以instance為None,返回描述符本身 __get__,None,<class '__main__.Foo'> <__main__.Info object at 0x0000000001D644F0> 通過實例來訪問 __get__,<__main__.Foo object at 0x00000000026149D0>,<class '__main__.Foo'> None """
__set__
class Info: ...... def __set__(self, instance, value): """ __set__方法存在兩個參數 instance:屬性當前綁定的實例對象 value:待設置的屬性值 """ print(f'__set__,{instance},{value}') Foo().bar = 10#__set__,<__main__.Foo object at 0x0000000001DE49D0>,10
描述符的__set__僅對實例起作用,對類不起作用。這和__get__方法不一樣
使用描述符實現校驗
class IntegerField: """整型欄位,只允許一定範圍內的整型值 :param min_value: 允許的最小值 :param max_value: 允許的最大值 """ def __init__(self, min_value, max_value): self.min_value = min_value self.max_value = max_value def __get__(self, instance,owner=None): # 當不是通過實例訪問時,直接返回描述符對象 if not instance: return self # 返回保存在實例字典里的值 return instance.__dict__['_integer_field'] def __set__(self, instance, value): # 校驗後將值保存在實例字典里 value = self._validate_value(value) instance.__dict__['_integer_field'] = value def _validate_value(self, value): """校驗值是否為符合要求的整數""" try: value = int(value) except (TypeError, ValueError): raise ValueError('value is not a valid integer!') if not (self.min_value <= value <= self.max_value): raise ValueError(f'value must between {self.min_value} and {self.max_value}!') return value
因為每個描述符對象都是owner類的屬性,而不是類實例的屬性,所以我們用的都是instance.dict而不是用self.dict。如果把值都存入self中就會存在互相覆蓋,值衝突的情況
class Person: age = IntegerField(min_value=10,max_value=100) def __init__(self,age): self.age = age p = Person(110) """ raise ValueError(f'value must between {self.min_value} and {self.max_value}!') ValueError: value must between 10 and 100! """