Python 装饰器

装饰器的基本语法

装饰器本质上就是“定义一个闭包并用语法糖@简练地调用该闭包”,从而实现把一个方法对象当做参数,传入到另一个方法中,然后返回一个增强功能的新方法对象。

在 Python 中允许在一个方法中嵌套另一个方法,这种特殊的机制就叫做「闭包」,这个内部方法可以保留外部方法的作用域,尽管外部方法不是全局的,内部方法也可以访问到外部方法的参数和变量。

不带参数的装饰器

一个最简单的装饰器

# coding: utf8

import time

def timeit(func):
    def inner():
        start = time.time()
        func()
        end = time.time()
        print 'duration time: %ds' % int(end - start)
    return inner

@timeit
def hello():
    time.sleep(1)
    print 'hello'

上面的例子,我们实现了一个最简单的装饰器,装饰的方法 hello 是没有参数的,如果 hello 需要参数,此时如何装饰器如何实现呢?(例子中的 @wraps 后文会做解释,现在可以忽略)

# coding: utf8

import time
from functools import wraps

def timeit(func):
    @wraps(func)
    def inner(name):  # inner 也需加对应的参数
        start = time.time()
        func(name)
        end = time.time()
        print 'duration time: %ds' % int(end - start)
    return inner

@timeit
def hello(name):  # 加了一个参数
    time.sleep(1)
    print 'hello %s' % name

hello('张三')

由于最终调用的是 inner 方法,被装饰的方法 hello 如果想加参数,那么对应的 inner 也添加相应的参数就可以了。

但是,我们定义的 timeit 是一个通用的装饰器,现在为了适应 hello 的参数,而在 inner 中加了一个参数,那如果要装饰的方法,有 2 个甚至更多参数,怎么办?难道要在 inner 中加继续加参数吗?

这当然是不行的,我们需要一个一劳永逸的方案来解决。我们改造如下:

# coding: utf8

import time
from functools import wraps

def timeit(func):
    @wraps(func)
    def inner(*args, **kwargs):  # 使用 *args, **kwargs 适应所有参数
        start = time.time()
        func(*args, **kwargs)    # 传递参数给真实调用的方法
        end = time.time()
        print 'duration time: %ds' % int(end - start)
    return inner

@timeit
def hello(name):
    time.sleep(1)
    print 'hello %s' % name

@timeit
def say(name, age):
    print 'hello %s %s' % (name, age)

@timeit
def say2(name, age=20):
    print 'hello %s %s' % (name, age)

hello('张三')
say('李四', 25)
say2('王五')

我们把 inner 方法的参数改为了 *args, **kwargs,然后调用真实方法时传入参数 func(*args, **kwargs),这样一来,我们的装饰器就可以装饰有任意参数的方法了,这个装饰器就变得非常通用了。

带参数的装饰器

被装饰的方法有参数,装饰器内部方法使用 *args, **kwargs 来适配。但我们平时也经常看到,有些装饰器也是可以传入参数的,这种如何实现呢?

# coding: utf8

import time
from functools import wraps

def timeit(prefix):  # 装饰器可传入参数
    def decorator(func): # 多一层方法嵌套
        @wraps(func)
        def wrapper(*args, **kwargs):
            start = time.time()
            func(*args, **kwargs)
            end = time.time()
            print '%s: duration time: %ds' % (prefix, int(end - start))
        return wrapper
    return decorator

@timeit('prefix1')
def hello(name):
    time.sleep(1)
    print 'hello %s' % name

实际上,装饰器方法多加一层内部方法就可以了。

我们在 timeit 中定义了 2 个内部方法,然后让 timeit 可以接收参数,返回 decorator 对象,而在 decorator 方法中再返回 wrapper 对象。

通过这种方式,带参数的装饰器由 2 个内部方法嵌套就可以实现了。

类作为装饰器

上面几个例子,都是用方法实现的装饰器,除了用方法实现装饰器,还有没有其他方法实现?

答案是肯定的,我们还可以用类来实现一个装饰器,也可以达到相同的效果。

# coding: utf8

import time
from functools import wraps

class Timeit(object):
    """用类实现装饰器"""
    def __init__(self, prefix):
        self.prefix = prefix

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            start = time.time()
            func(*args, **kwargs)
            end = time.time()
            print '%s: duration time: %ds' % (self.prefix, int(end - start))
        return wrapper

@Timeit('prefix')
def hello():
    time.sleep(1)
    print 'hello'

hello()     # 调用被装饰的方法

用类实现一个装饰器,与方法实现类似,只不过用类利用了 __init__ 和 __call__ 方法,其中 __init__ 定义了装饰器的参数,__call__ 会在调用 Timeit 对象的方法时触发。

你可以这样理解:t = Timeit('prefix') 会调用 __init__,而调用 t(hello) 会调用 __call__(hello)

是不是很巧妙?这些都归功于 Python 的魔法方法。

对类进行装饰

上面的装饰器都是用来装饰一个函数/方法的,那么我们能不能给一个类添加装饰器呢?当然也是可以的!通常情况下,装饰类的装饰器是一个接受类作为参数的函数,并返回一个新的类或修改原始类的函数。这个装饰器函数可以在类定义之前使用@符号应用到类上。

import time


def timer_decorator(cls):
    class TimerClass(cls):
        def __getattribute__(self, name):
            start_time = time.time()
            result = super().__getattribute__(name)
            end_time = time.time()
            execution_time = end_time - start_time
            print(f"Method '{name}' executed in {execution_time} seconds.")
            return result

    return TimerClass


@timer_decorator
class MyClass:
    def my_method(self):
        time.sleep(1)
        print("Executing my_method")


obj = MyClass()
obj.my_method()

# 输出
# Method 'my_method' executed in 2.1457672119140625e-06 seconds.
# Executing my_method

上述示例中,timer_decorator 装饰器接收一个类作为参数,并返回一个继承自原始类的新类 TimerClassTimerClass 中重写了 __getattribute__ 方法,在调用类的方法时,会计算方法的执行时间并进行打印。

functools.wraps

装饰器存在一个缺陷,它虽然能够保持被装饰函数的语法用法不变,但是会改变被装饰方法原有的属性信息(比如名称),这会给我们的代码调试带来不便。

# coding: utf8

@timeit
def hello():
    time.sleep(1)
    print 'hello'

print hello.__name__    # 输出 hello 方法的名字

# Output:
# inner

我们看到,虽然我们调用的是 hello,但是输出 hello 方法的名字却是 inner

理想情况下,我们希望被装饰的方法,除了增加额外的功能之外,方法的属性信息依旧可以保留原来的,否则在使用中,可能存在一些隐患。

如何解决这个问题?

在 Python 内置的 functools 模块中,提供了一个 wraps 方法,专门来解决这个问题。使用方法是在装饰器函数内部,用带参数的装饰器 @wraps(func) 装饰待返回的新方法,其参数 func 就是传入装饰器的原方法(内部逻辑就是把原方法传给装饰器 @wrap 从而把它的属性信息赋予待返回的新方法上)。

# coding: utf8

import time
from functools import wraps

def timeit(func):
    @wraps(func)  # 使用 wraps 装饰内部方法inner
    def inner():
        start = time.time()
        func()
        end = time.time()
        print 'duration time: %ds' % int(end - start)
    return inner

@timeit
def hello():
    time.sleep(1)
    print 'hello'

print hello.__name__    # 输出 hello 方法的名字

# Output:
# hello

使用 functools 模块的 wraps 方法装饰内部方法 inner 后,我们再获取 hello 的属性,都能得到来自原方法的信息了。

分析总结

本质上,装饰器其实就是先定义好一个闭包,然后使用语法糖 @ 来装饰方法,最后达到重新定义方法的作用。也就是说,最终我们执行的,其实是另外一个被添加新功能的方法。具体来说,装饰器的语法逻辑就是把 @decorator 装饰 func 的过程看成“把 func 输入给 decoratordecorator(func)),然后返回包装后的函数 new_func 代替被修饰函数 func,从而在不改变原有代码前提下扩展 func 的功能”。使用时主要就是关注两点:是否能被调用(callable),如何调用

涉及到有参数的装饰器时(如 @decorator(var)),本质上还是类似的,只不过 @ 的优先级要低于 (),所以先进行 decorator(var) 的运算,返回一个 new_decorator(这个过程相当于通过多引入一层函数,来把 var 参数间接传给 new_decorator,实现装饰器自身的参数传递),然后再加上 @,变成 @new_decorator,后面和普通无参数的装饰器的原理就是一样的了。

至于类装饰器,其实本质也是一样的,因为类和函数其实都可以实现 Obj(var) 的调用,所以理论上都可以写成 @Obj 语法作为装饰器使用。只不过对于类装饰器 @Cls 来说,装饰时实际调用的是它的 __init__() 函数,返回的是它的一个实例。需要注意的是,我们装饰的功能不是写在 __init__() 里面的,它只是一个“装饰入口”,具体装饰的功能是写在 __call__() 函数里面的。原因也比较简单,上面说了,@Cls 装饰一个函数 func 时返回的其实是一个类的实例 Cls_fun,不再是一个函数了。但是我们肯定还是希望它能保持和原来函数相同的调用方法和使用规则,即可以使用 Cls_fun(*args,**kwargs) 完成调用,而这种语法在类里面其实就是通过定义 __call__() 函数来实现的。简单来说,对于一个类的实例 CLS 而言,CLS(*args, **kwargs) 就代表着调用 CLS.__call__(*args, **kwargs),所以实际的装饰功能需要写到 __call__() 函数里面

更进一步,既然函数和类都可以通过相似的规则和语句实现调用,那么函数可以被装饰,类当然也可以被装饰,而且和装饰函数时一样,可以用函数或者用类实现做为装饰器去装饰它。

综上来看,其实只要理解了装饰器的基本逻辑,和函数及类的调用规则,装饰器就比较好理解了,应用中有可以灵活选择合适的装饰器设计方法。

常用的 Python 自带装饰器

类相关的装饰器

一些用来对类里面的方法进行装饰的装饰器。

普通的类方法一般称为 Instance methods(实例方法),它的第一个参数是 self,代表实例自身。这种方法必须通过实例进行调用,不能直接用类名进行调用,不然会报错 TypeError: x() missing 1 required positional argument: 'self'。利用这种方法可以访问和修改实例数据。

  • @property 内置装饰器

把一个类方法“伪装”成其同名属性,以支持属性访问,它返回的是一个 property 对象。(注意,直接用 @property 装饰的方法只能用来访问实例的属性,无法修改和删除实例的属性。若要实现,请看后文)

import math
class Circle:
    def __init__(self,radius): #圆的半径radius
        self.radius=radius

    @property
    def area(self):
        return math.pi * self.radius**2 #计算面积

    @property
    def perimeter(self):
        return 2*math.pi*self.radius #计算周长

# 我们可以通过实例访问到类中属性circle=Circle(10)
print(circle.radius)



# 通过@property装饰后的方法也可以像访问数据属性一样去访问area,会触发一个函

数的执行,动态计算出一个值;
print(circle.area) 
print(circle.perimeter)

一个 property 对象还具有 getter、setter、deleter 三种方法,可进一步作为装饰器对被装饰方法进行多次装饰,从而实现“伪装”属性的访问、修改和删除功能;setter 用于设置属性值,可以加入对输入属性值的检查;deleter 用于删除属性值。(getter 用于获取属性信息,但是实际使用中,最初的 @property 装饰器本身就是实现这个功能的,所以一般 getter 用不到,只是为了 OOP 设计的完整行引入了它)。

class C:
   def __init__(self):
        self._x = None

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
	    if value<100: # 加入属性值的检查
	        self._x = value
	    else:
		    raise ValueError('value should be less than 100')

    @x.deleter
    def x(self):
        del self._x

#实例化类
c = C()
# 为属性进行赋值
c.x=100
# 输出属性值
print(c.x)
# 删除属性
del c.x
  • @classmethod 内置装饰器
     
     被装饰的方法的第一个参数是表示类自身的 cls 参数(而非通常方法里的 self 参数),方法内部通过该 cls 参数可以访问类的属性,调用类的方法,实例化类对象等。被装饰的方法在外部可以直接通过类名进行调用,不需要实例化。(实例化之后也可以调用,但是无法操作和实例相关的内容,因为代表实例自身的参数 self 没有被传入)。需要注意的是, cls 参数始终表示当前的类,所以在一个子类里面,cls 代表的是子类,而非父类,所以即使子类里面没有重写那个被装饰的方法,它本质上也“被重写了”,里面的 cls 表示的类变成了子类。
class A():
    number = 10
    @classmethod
    def get_a(cls):     #cls 接收的是当前类,类在使用时会将自身传入到类方法的第一个参数
        print('这是类本身:',cls)# 如果子类调用,则传入的是子类
        print('这是类属性:',cls.number)

class B(A):
    number = 20
    pass

# 调用类方法 不需要实例化可以执行调用类方法
A.get_a()
B.get_a()
  • @staticmethod 内置装饰器

设置一个类的方法为静态方法,静态方法不需要传递隐性的第一参数 self(相当于隐藏了,不是没有,详细区别见下文)。静态方法的本质类型就是一个函数,它可以直接通过类进行调用,也可以通过实例进行调用。

@staticmethod 装饰的方法和普通的不带 self 方法的区别:前者相当于是隐藏了 self 参数,调用时输入(类名调用)或者不输入(实例调用)都行;后者相当于直接声明了没有 self 参数,所以它只能通过类调用,不能通过实例调用,(因为实例调用时会把自身作为 self 参数输入)。

class A():
	@staticmethod
	def static_func():
		print('static func')
	def func_wo_self():
		print('func without self')

# 类调用
A.static_func() # 输出 static func
A.func_wo_self() # 输出 func without self

# 实例调用
AIns = A()
AIns.static_func() # 输出 static func
AIns.func_wo_self() # 报错 TypeError: func_wo_self() takes 0 positional arguments but 1 was given

@staticmethod 装饰的方法和 @classmethod 装饰的方法的区别:前者是独立于类的一个单独函数,只是寄存在一个类名下(逻辑上而非语法上的聚合),它需要通过类名去访问类的属性和方法或者创建实例(这样的坏处是可移植性不好,如果后面类名修改了,那方法里面引用的类名也得修改);后者将类自身作为 cls 参数传入被装饰的方法,可以直接通过该参数去访问类内属性和方法或者创建实例。

class A():
	a=2
	@classmethod
	def cls_func(cls):
		print(cls.a)
	@staticmethod
	def sta_func():
		print(A.a)

		
A.cls_func() # 2
A.sta_func() # 2
  • @abstractmethod abc 内置模块中的装饰器

用于创建抽象类的常用装饰器,使用方法如下:

  • 待创建的 class 继承 abc.ABC
  • 给需要抽象的实例方法添加装饰器 @abc.abstractmethod

完成这两步后, 这个 class 就变成了抽象类, 不能被直接实例化, 要想使用抽象类, 必须继承该类并实现该类的所有抽象方法。

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def info(self):
        print("Animal")

class Bird(Animal):
    # 实现抽象方法
    def info(self):
        # 调用基类方法(即便是抽象方法)
        super().info()
        print("Bird")

animal = Animal() # 直接实例化会报错:TypeError: Can’t instantiate abstract class Animal with abstract methods info

# 实例化定义了抽象方法之后的子类才行
bird = Bird()
bird.info() 
# 输出
# Animal
# Bird

注意,如果抽象方法不仅是实例方法, 而且还是类方法或静态方法, 则应分别使用@abstractclassmethod 和 @abstractstaticmethod

functools 内置库中的装饰器

[[Lib-函数工具 functools 内置库]]

实用的自定义的装饰器

比如记录日志、记录执行耗时、本地缓存、路由映射等功能。

  • 记录调用日志
import logging
from functools import wraps

def logging(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # 记录调用日志
        logging.info('call method: %s %s %s', func.func_name, args, kwargs)
        return func(*args, **kwargs)
    return wrapper
  • 记录方法执行耗时
from functools import wraps

def timeit(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        duration = int(time.time() - start) # 统计耗时
        print 'method: %s, time: %s' % (func.func_name, duration)
        return result
    return wrapper
  • 记录方法执行次数
from functools import wraps

def counter(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.count = wrapper.count + 1   # 累计执行次数
        print 'method: %s, count: %s' % (func.func_name, wrapper.count)
        return func(*args, **kwargs)
    wrapper.count = 0
    return wrapper
  • 本地缓存
from functools import wraps

def localcache(func):
    cached = {}
    miss = object()
    @wraps(func)
    def wrapper(*args):
        result = cached.get(args, miss)
        if result is miss:
            result = func(*args)
            cached[args] = result
        return result
    return wrapper
  • 路由映射
class Router(object):

    def __init__(self):
        self.url_map = {}

    def register(self, url):
        def wrapper(func):
            self.url_map[url] = func
        return wrapper

    def call(self, url):
        func = self.url_map.get(url)
        if not func:
            raise ValueError('No url function: %s', url)
        return func()

router = Router()

@router.register('/page1')
def page1():
    return 'this is page1'

@router.register('/page2')
def page2():
    return 'this is page2'

print router.call('/page1')
print router.call('/page2')

除此之外,装饰器还能用在权限校验、上下文处理等场景中。你可以根据自己的业务场景,开发对应的装饰器。

Reference

posted @ 2023-08-08 18:44  凌晗  阅读(70)  评论(0编辑  收藏  举报