简单地理解 Python 的装饰器

本文从装饰器的基本原理出发,详细解析了Python装饰器的工作机制,包括装饰器的构造与调用过程、函数版装饰器的实现方式、带参数装饰器的设计方法以及装饰有参数的函数等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Python有大量强大又贴心的特性,如果要列个最受欢迎排行榜,那么装饰器绝对会在其中。


刚接触装饰器,会觉得代码不多却难以理解。其实装饰器的语法本身挺简单的,复杂是因为同时混杂了其它的概念。下面我们一起抛去无关概念,简单地理解下Python的装饰器。


装饰器的原理


在解释器下跑个装饰器的例子,直观地感受一下。


# make_bold就是装饰器,实现方式这里略去

>>> @make_bold

... def get_content():

...     return 'hello world'

...

>>> get_content()

'<b>hello world</b>'


被make_bold装饰的get_content,调用后返回结果会自动被b标签包住。怎么做到的呢,简单4步就能明白了。


1. 函数是对象


我们定义个get_content函数。这时get_content也是个对象,它能做所有对象的操作。


def get_content():

    return 'hello world'


它有id,有type,有值。


>>> id(get_content)

140090200473112

>>> type(get_content)

<class 'function'>

>>> get_content

<function get_content at 0x7f694aa2be18>


跟其他对象一样可以被赋值给其它变量。


>>> func_name = get_content

>>> func_name()

'hello world'


它可以当参数传递,也可以当返回值


>>> def foo(bar):

...     print(bar())

...     return bar

...

>>> func = foo(get_content)

hello world

>>> func()

'hello world'


2. 自定义函数对象


我们可以用class来构造函数对象。有成员函数__call__的就是函数对象了,函数对象被调用时正是调用的__call__。


class FuncObj(object):

    def __init__(self, name):

        print('Initialize')

        self.name= name

 

    def __call__(self):

        print('Hi', self.name)


我们来调用看看。可以看到,函数对象的使用分两步:构造和调用(同学们注意了,这是考点)。


>>> fo = FuncObj('python')

Initialize

>>> fo()

Hi python


3. @是个语法糖


装饰器的@没有做什么特别的事,不用它也可以实现一样的功能,只不过需要更多的代码。


@make_bold

def get_content():

    return 'hello world'

 

# 上面的代码等价于下面的

 

def get_content():

    return 'hello world'

get_content = make_bold(get_content)


make_bold是个函数,要求入参是函数对象,返回值是函数对象。@的语法糖其实是省去了上面最后一行代码,使可读性更好。用了装饰器后,每次调用get_content,真正调用的是make_bold返回的函数对象。


4. 用类实现装饰器


入参是函数对象,返回是函数对象,如果第2步里的类的构造函数改成入参是个函数对象,不就正好符合要求吗?我们来试试实现make_bold。


class make_bold(object):

    def __init__(self, func):

        print('Initialize')

        self.func = func

 

    def __call__(self):

        print('Call')

        return '<b>{}</b>'.format(self.func())


大功告成,看看能不能用。


>>> @make_bold

... def get_content():

...     return 'hello world'

...

Initialize

>>> get_content()

Call

'<b>hello world</b>'


成功实现装饰器!是不是很简单?


这里分析一下之前强调的构造和调用两个过程。我们去掉@语法糖好理解一些。


# 构造,使用装饰器时构造函数对象,调用了__init__

>>> get_content = make_bold(get_content)

Initialize

 

# 调用,实际上直接调用的是make_bold构造出来的函数对象

>>> get_content()

Call

'<b>hello world</b>'


到这里就彻底清楚了,完结撒花,可以关掉网页了~~~(如果只是想知道装饰器原理的话)


函数版装饰器


阅读源码时,经常见到用嵌套函数实现的装饰器,怎么理解?同样仅需4步。


1. def的函数对象初始化


用class实现的函数对象很容易看到什么时候构造的,那def定义的函数对象什么时候构造的呢?


# 这里的全局变量删去了无关的内容

>>> globals()

{}

>>> def func():

...     pass

...

>>> globals()

{'func': <function func at 0x10f5baf28>}


不像一些编译型语言,程序在启动时函数已经构造那好了。上面的例子可以看到,执行到def会才构造出一个函数对象,并赋值给变量make_bold。


这段代码和下面的代码效果是很像的。


class NoName(object):

    def __call__(self):

        pass

 

func = NoName()


2. 嵌套函数


Python的函数可以嵌套定义。


def outer():

    print('Before def:', locals())

    def inner():

        pass

    print('After def:', locals())

    return inner


inner是在outer内定义的,所以算outer的局部变量。执行到def inner时函数对象才创建,因此每次调用outer都会创建一个新的inner。下面可以看出,每次返回的inner是不同的。


>>> outer()

Before def: {}

After def: {'inner': <function outer.<locals>.inner at 0x7f0b18fa0048>}

<function outer.<locals>.inner at 0x7f0b18fa0048>

>>> outer()

Before def: {}

After def: {'inner': <function outer.<locals>.inner at 0x7f0b18fa00d0>}

<function outer.<locals>.inner at 0x7f0b18fa00d0>


3. 闭包


嵌套函数有什么特别之处?因为有闭包。


def outer():

    msg = 'hello world'

    def inner():

        print(msg)

    return inner


下面的试验表明,inner可以访问到outer的局部变量msg。


>>> func = outer()

>>> func()

hello world


闭包有2个特点


  1. inner能访问outer及其祖先函数的命名空间内的变量(局部变量,函数参数)。

  2. 调用outer已经返回了,但是它的命名空间被返回的inner对象引用,所以还不会被回收。


这部分想深入可以去了解Python的LEGB规则。


4. 用函数实现装饰器


装饰器要求入参是函数对象,返回值是函数对象,嵌套函数完全能胜任。


def make_bold(func):

    print('Initialize')

    def wrapper():

        print('Call')

        return '<b>{}</b>'.format(func())

    return wrapper


用法跟类实现的装饰器一样。可以去掉@语法糖分析下构造和调用的时机。


>>> @make_bold

... def get_content():

...     return 'hello world'

...

Initialize

>>> get_content()

Call

'<b>hello world</b>'


因为返回的wrapper还在引用着,所以存在于make_bold命名空间的func不会消失。make_bold可以装饰多个函数,wrapper不会调用混淆,因为每次调用make_bold,都会有创建新的命名空间和新的wrapper。


到此函数实现装饰器也理清楚了,完结撒花,可以关掉网页了~~~(后面是使用装饰的常见问题)


常见问题


1. 怎么实现带参数的装饰器?


带参数的装饰器,有时会异常的好用。我们看个例子。


>>> @make_header(2)

... def get_content():

...     return 'hello world'

...

>>> get_content()

'<h2>hello world</h2>'


怎么做到的呢?其实这跟装饰器语法没什么关系。去掉@语法糖会变得很容易理解。


@make_header(2)

def get_content():

    return 'hello world'

 

# 等价于

 

def get_content():

    return 'hello world'

unnamed_decorator = make_header(2)

get_content = unnamed_decorator(get_content)


上面代码中的unnamed_decorator才是真正的装饰器,make_header是个普通的函数,它的返回值是装饰器。


来看一下实现的代码。


def make_header(level):

    print('Create decorator')

 

    # 这部分跟通常的装饰器一样,只是wrapper通过闭包访问了变量level

    def decorator(func):

        print('Initialize')

        def wrapper():

            print('Call')

            return '<h{0}>{1}</h{0}>'.format(level, func())

        return wrapper

 

    # make_header返回装饰器

    return decorator


看了实现代码,装饰器的构造和调用的时序已经很清楚了。


>>> @make_header(2)

... def get_content():

...     return 'hello world'

...

Create decorator

Initialize

>>> get_content()

Call

'<h2>hello world</h2>'


2. 如何装饰有参数的函数?


为了有条理地理解装饰器,之前例子里的被装饰函数有意设计成无参的。我们来看个例子。


@make_bold

def get_login_tip(name):

    return 'Welcome back, {}'.format(name)


最直接的想法是把get_login_tip的参数透传下去。


class make_bold(object):

    def __init__(self, func):

        self.func = func

 

    def __call__(self, name):

        return '<b>{}</b>'.format(self.func(name))


如果被装饰的函数参数是明确固定的,这么写是没有问题的。但是make_bold明显不是这种场景。它既需要装饰没有参数的get_content,又需要装饰有参数的get_login_tip。这时候就需要可变参数了。


class make_bold(object):

    def __init__(self, func):

        self.func = func

    def __call__(self, *args, **kwargs):

        return '<b>{}</b>'.format(self.func(*args, **kwargs))


当装饰器不关心被装饰函数的参数,或是被装饰函数的参数多种多样的时候,可变参数非常合适。可变参数不属于装饰器的语法内容,这里就不深入探讨了。


3. 一个函数能否被多个装饰器装饰?


下面这么写合法吗?


@make_italic

@make_bold

def get_content():

    return 'hello world'


合法。上面的的代码和下面等价,留意一下装饰的顺序。


def get_content():

    return 'hello world'

get_content = make_bold(get_content) # 先装饰离函数定义近的

get_content = make_italic(get_content)


4. functools.wraps有什么用?


Python的装饰器倍感贴心的地方是对调用方透明。调用方完全不知道也不需要知道调用的函数被装饰了。这样我们就能在调用方的代码完全不改动的前提下,给函数patch功能。


为了对调用方透明,装饰器返回的对象要伪装成被装饰的函数。伪装得越像,对调用方来说差异越小。有时光伪装函数名和参数是不够的,因为Python的函数对象有一些元信息调用方可能读取了。为了连这些元信息也伪装上,functools.wraps出场了。它能用于把被调用函数的__module__,__name__,__qualname__,__doc__,__annotations__赋值给装饰器返回的函数对象。


import functools

 

def make_bold(func):

    @functools.wraps(func)

    def wrapper(*args, **kwargs):

        return '<b>{}</b>'.format(func(*args, **kwargs))

    return wrapper


对比一下效果。


>>> @make_bold

... def get_content():

...     '''Return page content'''

...     return 'hello world'

 

# 不用functools.wraps的结果

>>> get_content.__name__

'wrapper'

>>> get_content.__doc__

>>>

 

# 用functools.wraps的结果

>>> get_content.__name__

'get_content'

>>> get_content.__doc__

'Return page content'


实现装饰器时往往不知道调用方会怎么用,所以养成好习惯加上functools.wraps吧。



### Python装饰器的基本概念和用法 装饰器是一种用于修改函数或方法行为的高级工具。它允许在不改变原始函数代码的情况下,为其添加新的功能[^2]。装饰器本质上是一个高阶函数,接收一个函数作为参数,并返回一个新的函数。 #### 装饰器的基本结构 装饰器通常由一个嵌套函数组成,其中外部函数接收被装饰的函数作为参数,内部函数则执行额外的操作并调用原始函数。以下是一个简单装饰器示例: ```python def my_decorator(func): def wrapper(*args, **kwargs): print("执行前操作") result = func(*args, **kwargs) # 调用原始函数 print("执行后操作") return result return wrapper ``` #### 使用装饰器 通过 `@` 符号可以将装饰器应用到函数上。以下是如何使用上述装饰器的例子: ```python @my_decorator def say_hello(name): print(f"Hello, {name}!") say_hello("Alice") # 输出结果包括装饰器的行为 ``` 输出结果为: ``` 执行前操作 Hello, Alice! 执行后操作 ``` #### 带参数的装饰器 如果需要为装饰器本身传递参数,可以再增加一层嵌套函数。以下是一个带参数的装饰器示例: ```python def repeat(n): # 外层函数接收装饰器参数 def decorator(func): # 中间层接收被装饰的函数 def wrapper(*args, **kwargs): # 内层函数执行实际逻辑 for _ in range(n): func(*args, **kwargs) return wrapper return decorator ``` 使用带参数的装饰器时,需要先调用外层函数: ```python @repeat(3) def greet(name): print(f"Hi, {name}!") greet("Bob") # 输出 "Hi, Bob!" 三次 ``` #### 类中的装饰器 装饰器也可以应用于类的方法中。以下是一个类中使用装饰器的示例[^4]: ```python class MyClass: def __init__(self, name): self.name = name def newfunc(origin): def wrapper(self, *args, **kwargs): print("方法执行前操作") result = origin(self, *args, **kwargs) print("方法执行后操作") return result return wrapper @newfunc def say_hello(self): print(f"Hello from {self.name}!") obj = MyClass("Python") obj.say_hello() ``` 输出结果为: ``` 方法执行前操作 Hello from Python! 方法执行后操作 ``` #### 装饰器的工作原理 装饰器的工作原理是通过函数包装器的方式,在不修改原函数代码的前提下,为其添加额外的功能[^1]。当使用 `@decorator` 语法时,实际上是将函数作为参数传递给装饰器,并用装饰器返回的新函数替代原函数。 #### 使用 `functools.wraps` 为了保持被装饰函数的元信息(如名称、文档字符串等),可以使用 `functools.wraps`: ```python from functools import wraps def my_decorator(func): @wraps(func) def wrapper(*args, **kwargs): print("执行前操作") result = func(*args, **kwargs) print("执行后操作") return result return wrapper ``` 这确保了装饰后的函数保留了原始函数的名称和文档字符串。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值