python装饰器:高级特性

本文探讨Python装饰器的高级用法,包括类装饰器的两种用法,装饰器嵌套,带参数的装饰器,以及状态追踪装饰器。通过示例代码展示@classmethod、@staticmethod、@property及其相关使用,以及如何创建带参数和状态的装饰器。同时介绍装饰器类的概念,强调装饰器类必须可调用并接收一个函数作为输入。

装饰器的高级应用

上一篇博客里讲到了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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值