友情链接
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, insecond2.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数据模型与描述符
数据模型有关的方法,基本都以双下划线开头和结尾,它们通常被称为魔法方法
例如:我们打印对象的时候输出的是<类名+内存地址>
1 2 3 4 5 6 | class Person: def __init__( self , name): self .name = name print (Person( "yetangjian" )) #<__main__.Person object at 0x000001BA41805FD0> |
__str__就是Python数据模型里最基础的一部分。当对象需要当作字符串使用时,我们可以用__str__方法来定义对象的字符串化结果
注:除了print()以外,str()与.format()函数同样也会触发__str__方法
1 2 3 4 5 6 7 8 9 | 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方法。针对字符串类型会自动给变量加上引号,省去了手动添加的麻烦。
1 2 3 | name = 'yetangjian' age = 18 print (f "{name!r},{age!r}" ) #'yetangjian',18 |
同样我们实现的方法与str方法类似,我们依旧使用上述的例子
1 2 3 4 5 6 7 8 9 | class Person: ... def __repr__( self ): return f "{self.name!r},{self.age!r}" p = Person( "yetangjian" , 80 ) print ( repr (p)) #'yetangjian',80 |
02.__format__
定义对象在字符串格式化时的行为
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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比较运算符重载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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会帮你自动补全剩下的所有方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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做校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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__
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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) |
1 2 3 4 5 6 7 8 | """ 通过类来访问,所以instance为None,返回描述符本身 __get__,None, <__main__.Info object at 0x0000000001D644F0> 通过实例来访问 __get__,<__main__.Foo object at 0x00000000026149D0>, None """ |
__set__
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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__方法不一样
使用描述符实现校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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中就会存在互相覆盖,值冲突的情况
1 2 3 4 5 6 7 8 9 10 11 | 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! """ |