讓Python更優雅更易讀(第二集)

2022-09-09 06:06:38

友情連結

讓Python更優雅更易讀(第一集)

1.裝飾器

1.1裝飾器特別適合用來實現以下功能

  1. 執行時校驗:在執行階段進行特定校驗,當校驗通不過時終止執行。 適合原因:裝飾器可以方便地在函數執行前介入,並且可以讀取所有引數輔助校驗。
  2.  注入額外引數:在函數被呼叫時自動注入額外的呼叫引數。適合原因:裝飾器的位置在函數頭部,非常靠近引數被定義的位置,關聯性強。
  3. 快取執行結果:通過呼叫引數等輸入資訊,直接快取函數執行結果。
  4. 註冊函數:將被裝飾函數註冊為某個外部流程的一部分。適合原因:在定義函數時可以直接完成註冊,關聯性強。
  5. 替換為複雜物件:將原函數(方法)替換為更復雜的物件,比如類範例或特殊的描述符物件

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!
"""