主页
avatar

Kared

Python 装饰器解密:深入解析装饰器背后的原理

Python 装饰器以其强大的功能和简洁的实现方式,为编程提供了极大的便利,使得函数行为的扩展变得触手可及。然而,在深入挖掘装饰器的奥秘之前,我们必须先建立对函数和闭包这两大核心概念的坚实理解。

函数:Python 编程的基石

在 Python 的世界里,函数是构建程序的基本模块。它们透过 def 关键字声明,拥有独特的函数名,并可携带一系列可选参数。最终,函数通过 return 关键字来输出它们的计算结果。

1、函数的定义与作用域

在 Python 中,变量的可访问性是由其定义的位置决定的,这就构成了变量的作用域。作用域定义了程序的哪些部分可以访问特定的变量名称。Python 的作用域分为四种类型:

  • L (Local)局部作用域:定义在函数内部的变量。
  • E (Enclosing)外围作用域:位于嵌套函数的外部函数中的变量。
  • G (Global)全局作用域:整个文件范围内的变量。
  • B (Built-in)内建作用域:Python 语言自带的特殊变量。
b_count = int(6.6)  # 内建作用域

g_count = 8  # 全局作用域
def outer():
    e_count = 6  # 外围作用域
    def inner():
        l_count = 3  # 局部作用域

为了深入理解,我们主要关注全局作用域和局部作用域,利用 Python 的 globals() 函数,可以获取一个包含当前全局作用域中所有变量的字典。

2、变量的解析规则与生命周期

尽管函数内部可以访问外部定义的全局变量,但是一旦在函数内部创建了一个同名变量,它就会在当前的作用域内覆盖全局变量。变量的查找遵循 L -> E -> G -> B 的顺序,即从局部作用域开始,逐级向外层作用域查找,直至找到为止。

def g_print():
    # 局部作用域无该变量,输出全局变量
    print(string)

def l_print():
    string = 'This is a dog!'
    # 局部作用域有该变量,输出局部变量
    print(string)

if __name__ == '__main__':
    string = 'This is a cat!'
    g_print()  # 输出:This is a cat!
    l_print()  # 输出:This is a dog!

变量的生命周期受其所在命名空间的影响。一旦函数执行完毕,其内部的局部变量便会消失。

def print_string():
    string = 'This is a dog!'
    print(string)

if __name__ == '__main__':
    print_string()
    # 此处尝试访问 string 将引发错误,因为它在这个作用域内不可见
    print(string)  # 抛出错误:NameError: name 'string' is not defined

3、函数的参数

在 Python 中,函数的参数设计极具灵活性,它们在函数内部表现为局部变量,根据传递和定义方式的不同,可以分为以下几类:

  • 必备参数(Positional Arguments):这些参数是必不可少的,且需要按照函数定义时的顺序准确传递。在函数调用时,每一个必备参数都需要对应一个实际的参数值。
  • 关键字参数(Keyword Arguments):调用函数时,关键字参数允许通过指定参数名来设置参数值,这意味着即使参数顺序改变,只要参数名正确,函数也能接收到正确的参数值。
  • 默认参数(Default Arguments):在函数定义时,可以为参数设置默认值。如果在调用函数时没有传递这些参数,那么将使用其默认值。
  • 不定长参数(Arbitrary Argument Lists):当你想要函数接收任意数量参数时,不定长参数就派上用场。在参数名前加上一个星号 *args 表示接收为元组的形式,而两个星号 **kwargs 表示接收为字典的形式,分别用于未命名和命名参数。

下面是一个展示这四种参数类型的 Python 函数示例:

# 定义一个函数,展示不同类型的参数
def introduce(name, greeting="Hello", *hobbies, **personal_info):
    print(f"{greeting}, my name is {name}.")

    # 打印不定长参数(元组)
    if hobbies:
        print("My hobbies are:", ", ".join(hobbies))

    # 打印关键字不定长参数(字典)
    for key, value in personal_info.items():
        print(f"My {key} is {value}.")

# 调用函数
introduce(
    "Alice",                       # 必备参数
    greeting="Hi",                 # 关键字参数
    "reading", "traveling",        # 不定长参数
    age=29, city="New York"        # 关键字不定长参数
)

在这个例子中,name 是必备参数,greeting 是带有默认值的参数,*hobbies 是不定长参数,而 **personal_info 是关键字不定长参数。当我们调用 introduce 函数时,我们按照这些规则提供了相应的参数。这种参数设计使得 Python 函数非常灵活,能够适应各种不同的调用情境。

4、函数嵌套

Python 支持函数嵌套,即你可以在一个函数内部定义另一个函数。嵌套函数可以访问其外层作用域中的变量,这一点在闭包和装饰器的设计中尤为重要。

def out_function():
    string = 'This is a dog!'
    def in_function():
        print(string)
    return in_function()

if __name__ == '__main__':
    out_function()  # 输出:This is a dog!

在上述示例中,in_function 作为内嵌函数,能够访问其外层函数 out_function 中定义的变量 string。当 out_function 被调用,它会创建并返回 in_function,后者在调用时输出了 string 变量的值。这一过程展示了 Python 中作用域和变量生命周期的精妙互动。

闭包:保持状态的函数

在 Python 的编程实践中,闭包(Closure)是一个函数,它能够捕获并保持对其词法作用域中变量的引用。这意味着即使函数在其定义环境之外被调用,它仍然能够访问那些变量。闭包的关键特性包括:

  • 环境捕捉:闭包在被定义时捕获周围的状态,即使外层函数执行完毕,这些状态仍然可用。
  • 封装性:闭包封装了内部变量,防止外部直接访问,实现了数据的隐蔽性和安全性。
  • 持久性:闭包内的变量生命周期超出了它们的作用域,只要闭包还在使用,这些变量就会一直存活。

在 Python 中,闭包的存在可以通过函数对象的 __closure__ 属性来确认,该属性包含了闭包中捕获的变量的细胞(cell)对象,每一个细胞内部存储了闭包中引用的自由变量的一个副本。这里所说的自由变量,是指那些在函数定义中被使用到,但既不是函数的参数也不是局部变量的变量。

为了更深入地理解闭包,我们可以来看一个具体的例子:

def make_multiplier(x):
    def multiplier(n):
        return x * n
    return multiplier

# 创建一个闭包实例,记住 x=3 的环境
times3 = make_multiplier(3)
# 创建另一个闭包实例,记住 x=5 的环境
times5 = make_multiplier(5)

# 利用闭包实例进行计算
print(times3(10))  # 输出结果为 30,因为 3 * 10 = 30
print(times5(10))  # 输出结果为 50,因为 5 * 10 = 50

在上述例子中,make_multiplier 函数返回了一个内嵌的 multiplier 函数,而这个内嵌函数闭包了外部函数的参数 x。即使 make_multiplier 函数的执行已经完成,闭包中的 x 仍然被 multiplier 函数保留和访问。

闭包不仅是 Python 函数式编程的基础,而且是装饰器的核心机制。装饰器使用闭包来扩展和修改函数的行为,它们使得在不修改原始函数的情况下增加功能变得可能,进而提升代码的可复用性和模块化。

装饰器:增强函数功能

在 Python 中,函数装饰器是一种使用闭包概念实现的强大工具,它允许我们在不修改函数内部代码的前提下,增加额外的功能。装饰器本质上是一个接收函数作为参数并返回一个新函数的闭包。后面内容中,我们将一步步深入了解函数装饰器的工作原理和用法。

1、Python 函数装饰器

装饰器是一种特殊的闭包,它接受一个函数作为参数,并返回一个功能增强的函数。看看下面的例子:

def out_function(function):
    def in_function():
        string = function()
        print('string: ', string)
    # 注意应该返回函数本身,而不是函数的调用结果
    return in_function

def function():
    string = 'This is a dog!'
    return string

if __name__ == '__main__':
    decorated_function = out_function(function)
    decorated_function()  # 输出:string:  This is a dog!

2、语法糖:装饰器的简洁方式

Python 允许使用 @ 符号作为装饰器的语法糖,使得装饰器的应用更加简单。我们只需在函数定义之前加上 @ 和装饰器的名称即可。例如:

@out_function
def function():
    string = 'This is a dog!'
    return string

if __name__ == '__main__':
    # 装饰器输出:string:  This is a dog!
    print(function())  # 输出返回值:None

注意,虽然我们添加了装饰器,但是 function() 函数似乎没有返回值。这是因为我们在装饰器内部没有将函数的返回值通过 return 语句传递出来。

3、适配不同参数的装饰器:*args**kwargs

为了让装饰器能够处理带有不同参数的函数,我们需要使用 *args**kwargs 这两个不定长参数,它们可以让装饰器接收任意数量和类型的参数。

import time

def Time(func):
    def wrapper(*args, **kwargs):
        t1 = time.time()
        result = func(*args, **kwargs)
        t2 = time.time()
        print("run time: {:.4f}s".format(t2 - t1))
        return result
    return wrapper

@Time
def count_number(max_number, tag):
    count = 0
    for item in range(max_number):
        if item % tag == 0:
            count += item
    return count

if __name__ == '__main__':
    # 装饰器输出:run time: 0.4188s
    print(count_number(6666666, 2))  # 输出返回值:11111105555556

现在装饰器 Time 已经可以适配任意参数的函数了,*args 表示任何多个无名参数,它是一个元组;**kwargs 表示关键字参数,它是一个字典。在使用时,*args 必须位于 **kwargs 之前。最后,我们通过一个综合示例来演示如何使用这两个参数:

def noName(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f'*args: {args}')
        print(f'**kwargs: {kwargs}')
        return result
    return wrapper

@noName
def function(a, b, c, num, string):
    return f'{a} {b} {c} {num} {string}'

if __name__ == '__main__':
    print(function(1, 2, 3, num=123, string='function'))


""" 输出结果:
*args: (1, 2, 3)
**kwargs: {'num': 123, 'string': 'function'}
1 2 3 123 function
"""

正如我们所见,Python 装饰器不仅是一个优雅的编程工具,它们还为代码的重构和模块化提供了巨大的便利。通过装饰器,我们可以无缝地增加函数功能,而不必更改函数本身的代码。这种能力在维护大型代码库时显得尤为宝贵,因为它允许我们扩展功能而不会引入潜在的新错误。

Python 装饰器 函数 闭包