丝滑的Python装饰器

227 阅读6分钟

语法介绍

在Python中,装饰器是一个非常便利的语法糖,它提供了一种对函数功能增强的方式。
先来看个简单的例子:
decorator是装饰器函数,它接收一个"被装饰函数"(也就是这里的target)作为参数,并返回一个新的函数替换原本的函数,也就是用inner替换target。


def decorator(func):
    def inner():
        print("running inner()")
        func()
    return inner
  
@decorator
def target():
    print("running target()")

target()

以上代码的输出如下:

running inner()
running target()

为什么说它是个语法糖?

因为装饰器并不是什么新语法,它只是一种方便的表达,以下是上述装饰器的等价语法:

target = decorator(target)

当我们想对某个函数做一些非业务逻辑的增强时,装饰器非常方便。只需要定义好装饰器,在函数头打上一个“标签”就能增强函数。下面来看几个例子:

使用场景

函数计时

当我们想要对某个函数做计时时,可以用以下这个装饰器。这种修改是非嵌入式的,以后我们想要用新的计时策略替换也非常方便,直接修改标签即可。

def clock(func):
    def wrapper_function(*args, **kwargs):
        start_time = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start_time
        func_name = func.__name__
        args_str = ", ".join(repr(arg) for arg in args)
        print("[%0.8fs] %s(%s) -> %r." % (elapsed, func_name, args_str, result))
        return result
    return wrapper_function

@clock
def calculate_something(arg1, arg2):
    print("calculating...")
    time.sleep(2)
    print("done!")
    return "something"

calculate_something(1, 2# calculating...
# done!
# [2.00450460s] calculate_something(1, 2) -> 'something'.

优化后的计时

假设系统运行一段时间后,发现记录时延的日志太多了,我们想要针对不同的函数设置不同的记录阈值,只有耗时超过阈值时记录。除了更换新的装饰器,我们还可以用参数化装饰器实现定制化计时。

# 参数化装饰器。记得用法即可,不必纠结语法。
def param_clock(elapse_threshold):
    def decorate(func):
        def wrapper_function(*args):
            start_time = time.perf_counter()
            result = func(*args)
            elapsed = time.perf_counter() - start_time
            func_name = func.__name__
            args_str = ", ".join(repr(arg) for arg in args)
            if elapsed > elapse_threshold:
                print("SLOW! [%0.8fs] %s(%s) -> %r." % (elapsed, func_name, args_str, result))
            return result
        return wrapper_function
    return decorate

# 超时阈值为2s
@param_clock(2)
def calculate_something_slow(arg1, arg2):
    print("calculating something slowly...")
    time.sleep(5)
    print("done!")
    return "something"

# 超时阈值为1s
@param_clock(1)
def calculate_something_fast(arg1, arg2):
    print("calculating something fast...")
    print("done!")
    return "something"

calculate_something_slow(1, 2)
# calculating something slowly...
# done!
# SLOW! [5.00498800s] calculate_something_slow(1, 2) -> 'something'.

calculate_something_fast(1, 2)
# calculating something fast...
# done!

鉴权

还有一个常见的需求是对用户请求做鉴权,例如:阻止普通用户访问后台,这时就可以用装饰器实现灵活的权限控制。
P.S. 这里只是举个简单的例子,实际的鉴权系统是需要根据业务需求做设计的。

def authorization_required(allowed_identity):
    def decorate(func):
        def wrapper_function(request, *args):
            if not hasattr(request, "user") or not hasattr(request, "identity"):
                raise Exception("Authentication required, please login first.")
            if request.identity not in allowed_identity:
                raise Exception("Permission denied: please contact administrator for help.")
            print("%s is allowed to call %s." % (request.user, func.__name__))
        return wrapper_function
    return decorate


class HttpRequest(object):
    user = ""
    identity = ""

    def __init__(self, user, identity):
        self.user = user
        self.identity = identity

@clock
@authorization_required(["admin_user"])
def access_monitor_page(request):
    print("<h1>Limited Information<h2>")

request1 = HttpRequest("test_user1", "normal_user")
access_monitor_page(request1)
# Exception: Permission denied: please contact administrator for help.

request2 = HttpRequest("test_user2", "admin_user")
access_monitor_page(request2)
# test_user2 is allowed to call access_monitor_page.
# <h1>Limited Information<h2>
# [0.00000580s] wrapper_function(<__main__.HttpRequest object at 0x0000023E1A03EA30>) -> None.

上述例子体现了装饰器的三个优点:

  1. 当我们想要去掉功能增强时,直接删掉装饰器“标签”即可,不需要直接修改被装饰的函数。
  2. 而当我们需要修改某个功能增强的细节时,也只需要修改装饰器“标签”中的参数。
  3. 同一装饰器可以在不同函数间复用,不同装饰器也可以在同一个函数叠加。举例来讲,当我的函数既需要计时又需要鉴权时,我们直接将两个标签叠加在一个函数上即可,各位看官可自行尝试。

当然还有一些其他例子,例如统一审计、统一报错等,本文将不再就此展开。本文的重点还是展现Python装饰器的灵活性和便利性,以及如何利用它实现耦合度更低的逻辑

由浅入深:关于AOP、代理模式、Python装饰器和装饰器模式

犹记得学习Python装饰器时回想起了一些相似的概念,它们分别是:AOP、代理模式和装饰器模式。这里记录一下我个人的理解与思考。本文默认读者已了解上述几个概念,将不再赘述。

鉴于水平有限,此处仅代表本人个人观点,日后如有更深理解将不断完善本文。此处拓展阅读仅做抛砖引玉,如有不同观点欢迎各位看官理性讨论。

AOP面向切面编程

AOP(面向切面编程)是一种编程思想,并不与特定的语言或设计模式绑定。AOP是对OOP(面向对象编程)的补充,通过在横切关注点(Aspect)加入切面(Point Cut)为核心逻辑添加额外的行为。横切关注点可以简单理解为方法调用的前后。

AOP的具体实现方式各种各样,设计模式层面有装饰器模式、(动/静态)代理模式。实践层面上,Java还可以通过字节码生成的方式实现。总之,这些千变万化的方式只是“术”,AOP的思想才是核心。

装饰器模式 v.s. 代理模式

装饰器模式和代理模式的形式上非常相似,我的理解是:不必纠结两者结构上的差异,两种模式之所以不同主要是逻辑上的关注点不同。装饰器模式的逻辑重点在于:为对象增加额外的功能。代理模式的重点在于:由代理对象控制对原对象的引用,控制原对象的访问。

设计模式属于“术”层面的知识,它帮助我们拄着“拐杖“前进,当我们理解了如何合理设计时,我们完全可以跑开拐杖奔跑前进。因此在学习时可以思考不同设计模式的侧重点,而在实践中,探究采用了什么模式并不太重要,设计出结构良好的系统才是重点。

Python装饰器 v.s. 装饰器模式

Gamma等人写的《设计模式:可复用面向对象软件的基础》中是这样概述装饰器模式的:“动态地给一个对象添加一些额外的职责”。这里职责可以理解为增加对象的功能。且由于具体类和装饰类都实现了同样的接口,因此装饰类具有透明性,可以递归嵌套多个装饰器,添加任意多个功能。以下为装饰器模式的UML类图:

cd3ad4e3ec4ecaede9ae6404bf87fcf.jpg Python装饰器也主要利用了这个思想,但是在实现层面上一些语言通常采用“类”作为装饰的基本单元,而Python装饰器则针对函数做功能增强,且由于装饰器返回的函数接受同样的参数,Python装饰器装饰的函数同样有透明性,从而可以添加任意多的行为。

参考资料:

  • 《流畅的Python》 — [巴西] Luciano Ramalho
  • 《设计模式的艺术》 — 刘伟