☰
装饰器:让代码“保暖”的神奇魔法
2026/10/21 11:34:51 网站建设 项目流程

在讲那个叫做装饰器的东西之前, 我本来是想先举个例子, 虽然这个例子有点不太干净, 甚至可以说是有点脏, 但是它跟我们要聊的这个装饰器的话题, 真的是贴得非常紧。

每个人都有的这种内裤, 它的主要功能是用来遮住难为情的地方。可是到了冬天, 它没有办法替我们挡住寒风并且保存体温。这时候人们想到的一个办法是为这个内裤做个改动。具体来说就是把它弄得又厚又长一些。经过这么一弄之后, 它不仅还保留了遮羞的功能, 同时还能够用来保暖。

但是这里面存在一个问题, 当被我们改成裤子样子的内裤变成长裤的形态时。虽然它还发挥着遮盖的作用, 但是从本质上讲, 它已经不是一条正儿八经的内裤了。

于是, 比较机灵的人就发明出来了长裤, 这长裤是怎么穿的, 是在内裤的外面直接给套上去的, 这么做是不影响内裤原本的功效的, 而且因为裤子上套了裤子, 所以, 内裤还算是内裤, 这下好了, 有了长裤之后, 小孩子也就再也不怕冷了, 而代码里的装饰器它就像咱们这里提到的长裤一样, 也是在不影响原本那部分作用的前提下, 给我们的身子提供了保暖的效果。

在开始谈论所谓的装饰机制之前, 还必须事先搞清楚一个非常重要的事实问题, 在这里面所提及的函数类型, 与我们通常熟知的Java这种编程语言或者是C++这种编程语言当中的相关概念, 存在着比较明显的差异, 在此处语境下定义的函数, 实际上能够被当作一种普通的、没有特殊身份的变量来看待, 并且具备被传递给另外某一个其他函数的能力, 具体的这个现象可以通过如下所示的这个例子来进行观察和理解:

def foo():

print("foo")

def bar(func):

func()

bar(foo)

现在咱们正式回来, 回到咱们的主题上面去。装饰器在根本上来说, 它就是属于一个函数或者是一个类这种东西。它的主要作用是, 能够让别的功能模块或者是别的类对象, 在进入到一个运行状态的时候, 不用进行任何代码层面的改动操作, 就自动把额外的一些功能给加进去。

而且你要知道, 最后从装饰器那边返回回来的值,也是同样地被归类为一个函数或是类的对象这类东西。

大家通常会在存在切面需求这样的场合下面去使用到它, 比方说, 要插入一些日志内容、要做性能方面的测试工作、要进行事务的处理流程、要实现缓存机制的操作、还是说要执行权限校验的一系列步骤等等这类场景里头去应用它。总而言之, 用这种设计方法的话, 算是解决这些问题的非常不错的选择方案了。

一旦我们拥有了装饰器这个玩意儿, 我们就能够把那些跟函数本身的功能没啥直接关系、但是到处都长得一模一样的冗长代码, 统统给抽调出来, 扔到装饰器这个容器里去, 然后接着进行重复利用。

从整体上概括起来的话, 装饰器这个东西所能够实现的核心作用就是给早就已经存在的那些对象或者说是实例, 来强行加上一些原本没有的、额外的功能特性。

咱们来先看一个比较简单的例子, 当然了, 咱们在实际编写代码的时候, 遇到的情况可能会比这一种要复杂许多。

def foo():

print('i am foo')

当前阶段出现了一项新的需求, 这个需求的内容是希望对于函数的执行日志来进行记录操作, 于是基于这一情况, 在代码里面添加了日志相关的代码处理:

def foo():

print('i am foo')

logging.info("foo is running")

如果函数 bar()还有类似的这个需求, 那个叫做 bar2()的函数如果也想要处理同样的问题, 应该怎么办呢? 难道要在 bar()函数里再去写一份代码吗? 这样做的话, 就会产生大量一模一样的、重复的代码内容, 看起来非常累赘。

为了能够避免这种反复书写相同逻辑的情况, 我们可以采取一个办法, 那就是把这个公共的部分单独拿出来, 重新去定义一个新的函数出来。让这一个新的函数专门用来负责完成日志的处理工作, 等它把日志处理完毕了之后, 然后再去接着执行那些真正的属于业务层面的代码即可。

def use_logging(func):

logging.warn("%s is running" % func.__name__)

func()

def foo():

print('i am foo')

use_logging(foo)

从逻辑方面来看, 这样做完全没有问题, 功能也得到了实现。但是, 我们在调用的过程中, 不再是直接调用真正的业务逻辑foo函数了, 而是改用了其他的函数。

这就造成了原有的代码结构被破坏的情况。眼下, 我们不得不每一次都把原本的那个foo函数作为参数传递到新的函数里面去。那么, 是否存在一种更好的方式呢? 答案是存在的, 这种方式就是装饰器。

简单装饰器

def use_logging(func):

def wrapper():

logging.warn("%s is running" % func.__name__)

return func() # 把 foo 当做参数传递进来时,执行func()就相当于执行foo()

return wrapper

def foo():

print('i am foo')

foo = use_logging(foo) # 因为装饰器 use_logging(foo) 返回的时函数对象 wrapper,这条语句相当于 foo = wrapper

foo() # 执行foo()就相当于执行 wrapper()

这实际上就是一个装饰器, 而装饰器本质上是一个普通的函数, 这个函数把执行真正的业务逻辑的函数 func 包裹在内部, 从表面看上去就像是 foo 被装饰了一样。

该装饰器返回的结果同样也是一个函数, 这个函数的名字并没有在当前描述中出现明确命名。在此例中, 当函数进入和执行退出的时候, 这一过程通常被称为一个横切面, 这种特定的编程方式正式的名称被称为面向切面的编程。

@ 语法糖

当你接触并了解相关的内容已经有一段时间以后, 你肯定对@那个符号不会感到陌生。实际上, @符号就是一种装饰器的语法糖形式, 它被放置在函数开始进行定义的那个位置, 通过这样的方式, 我们就可以免去在最后一步再进行重新赋值这个操作。

def use_logging(func):

def wrapper():

logging.warn("%s is running" % func.__name__)

return func()

return wrapper

@use_logging

def foo():

print("i am foo")

foo()

根据上面所描述的情况,如果存在 @ 这个符号的话, 那么我们就可以把 foo = (foo)这一句代码给省略掉, 直接使用 foo() 来进行调用, 这样就能获取得到我们想要得到的结果。

你们是否已经看得到了呢, 函数 foo() 本身是完全不需要的去进行任何修改的, 它仅仅是在那里被定义的地方额外地带上了一个装饰器罢了, 而在后续的调用环节当中, 它依旧保持和从以前没有任何区别的状态, 当我们面对其他一些具备相似特征的函数时, 我们完全有能力继续去借助调用装饰器的这种方式来对相应的函数实施修饰操作, 而彻底无需再重复地进行函数的修改行为或是去额外增加新的封装过程, 透过上述这种做法, 我们可以切实地实现程序可重复利用性方面的提升, 同时还能够有效地让程序的可读性得到进一步的增强。

装饰器之所以在使用上能够方便到如此程度, 这完全归因于相关函数具备这样的特性, 即它们可以像普通的对象那样被处理, 不仅能够作为参数传递给其他的函数, 还被允许赋值给别的变量, 也能够作为返回值来使用, 并且还可以被定义在另外的一个函数里面。

*args、**

或许有人会提起疑问, 说要是咱们的业务逻辑函数 foo 需要带上某些参数该当如何处置呢。好比说这一种情况:

def foo(name):

print("i am %s" % name)

我们可以在, 对函数进行定义, 这个步骤里面的时候, 为参数进行指定。

def wrapper(name):

logging.warn("%s is running" % func.__name__)

return func(name)

return wrapper

这样一来, 在 foo 这个函数内部, 就可以把需要定义好的参数给它定义下来。这时候, 又会有人提出疑问, 说如果 foo 这个函数要接收两个参数的时候怎么办? 要是接收三个参数的时候也同样的情况该怎么办?

更加极端的情况是,使用者可能会往里面传入非常多数量的参数。当装饰器本身并不清楚 foo 这个函数究竟拥有多少个参数的时候, 我们可以使用 *args 这个东西去进行替代。

def wrapper(*args):

logging.warn("%s is running" % func.__name__)

return func(*args)

return wrapper

这样的话, 不管 foo 这个函数一共定义了多少个参数, 用户都能够把这些参数原原本本地传入到 func 里面去。这样做的好处是, 完全不会影响 foo 本身业务逻辑的实现情况。

这个时候, 可能还会有一部分读者发出疑问, 比如问, 倘若 foo 这个函数的内部同时也定义了一些关键字参数, 这种情况又该如何处理呢? 例如如下所示的代码情况:

def foo(name, age=None, height=None):

print("I am %s, age %s, height %s" % (name, age, height))

在这个时候, 你能够执行一个动作, 这个动作的具体内容是将那个叫做“函数”的东西设置为关键字函数的状态。

def wrapper(*args, **kwargs):

# args是一个数组,kwargs一个字典

logging.warn("%s is running" % func.__name__)

return func(*args, **kwargs)

return wrapper

带参数的装饰器

装饰器具有更大的灵活性, 比如带参数的装饰器, 在之前的那个装饰器调用里面, 这个装饰器接收的只有一个参数, 就是去执行具体业务的函数 foo , 语法方面允许我们在调用的时候去提供另外其他的一些参数, 像是 @(a) 这样的一种形式, 这就给写装饰器和用装饰器提供了更多的灵活操作空间。

比如说, 咱们是能够在装饰器这个地方把日志的那个等级给确定下来的。这是为啥? 因为不同的业务函数, 可能需要的那种日志级别, 是不一样不一样的。

def use_logging(level):

def decorator(func):

def wrapper(*args, **kwargs):

if level == "warn":

logging.warn("%s is running" % func.__name__)

elif level == "info":

logging.info("%s is running" % func.__name__)

return func(*args)

return wrapper

return decorator

@use_logging(level="warn")

def foo(name='foo'):

print("i am %s" % name)

foo()

上面提到的是允许带参数的装饰器, 它实际上是对原有装饰器的一个函数进行封装, 然后返回一个新的装饰器, 我们可以将它理解为一个含有参数的闭包, 当使用 @(level="warn") 调用时, 能够发现这一层的封装, 并把参数传递到装饰器的环境中。

@(level="warn") 等价于 @

类装饰器

事实确实是这样, 装饰器这个东西, 它不仅仅可以是函数, 还有可能是类。跟函数装饰器进行对比来看, 类装饰器拥有灵活度很大、内聚程度很高、封装性很好这样的优点。如果要使用类装饰器, 主要是依靠类的里面的方法来操作。当人们采用 @ 的形式把装饰器附加到函数上面的时候, 就会去调用这个类里面的对应方法。

class Foo(object):

def __init__(self, func):

self._func = func

def __call__(self):

print ('class decorator runing')

self._func()

print ('class decorator ending')

@Foo

def bar():

print ('bar')

bar()

functools.wraps

通过使用装饰器, 代码的复用程度得到了非常大的提升, 不过这个做法存在一个不足之处, 就是原函数的那些元信息消失了, 比方说函数的名称、参数列表等等细节都不见了, 接下来先看具体的例子:

# 装饰器

def logged(func):

def with_logging(*args, **kwargs):

print func.__name__ # 输出 'with_logging'

print func.__doc__ # 输出 None

return func(*args, **kwargs)

return with_logging

# 函数

@logged

def f(x):

"""does some math"""

return x + x * x

logged(f)

不难发现, 函数 f 被取代了, 当然它的元信息也变成了函数的相关信息。好在我们有.wraps, wraps本身也是一个装饰器, 它能把原函数的元信息拷贝到装饰器里面的 func 函数中, 使得装饰器里面的 func 函数也具有与原函数 foo 完全一样的元信息。

from functools import wraps

def logged(func):

@wraps(func)

def with_logging(*args, **kwargs):

print func.__name__ # 输出 'f'

print func.__doc__ # 输出 'does some math'

return func(*args, **kwargs)

return with_logging

@logged

def f(x):

"""does some math"""

return x + x * x

装饰器顺序

还有一种情况是, 一个函数能够在定义的时候, 一次性就挂上好多个装饰器, 比方说:

@a @b @c def f (): pass

它的执行顺序是从内部向外部进行的, 也就是说, 最先被调用的是位于最里面的那个装饰器, 而最后被调用的则是处于最外层的那个装饰器, 它在效果上是等效于。

f = a(b(c(f))) fqas 234***

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询