
腾讯二面:说说看什么是Python装饰器?它和闭包是什么关系?如何保持被装饰函数的元数据?
面试官:说说看什么是Python装饰器?
Python装饰器(decorators)是Python中的一个高级功能,它允许用户在不修改原有函数或方法定义的情况下,给函数或方法添加额外的功能。装饰器本质上是一个高阶函数,它接收一个函数作为参数,并返回一个新的函数或可调用对象。
装饰器的语法使用 @
符号,它放在函数定义之前。当Python解释器遇到这个符号时,它会将紧跟其后的函数名作为参数传递给装饰器函数,并将装饰器函数的返回值(通常是一个新的函数)重新绑定到原函数名上。
装饰器的一个常见用途是添加日志记录、性能计时、事务处理、缓存、权限校验等功能。通过使用装饰器,这些额外的功能可以被干净地封装起来,并且可以在多个函数之间重用,而无需在每个函数内部重复编写相同的代码。
装饰器的一个关键特性是它们能够保持被装饰函数的元数据,如函数名、文档字符串等。这是通过使用 functools.wraps
装饰器来实现的,它会在新函数上复制原函数的这些属性。
下面是一个简单的Python装饰器示例,它用于记录函数的执行时间:
import time import functools def timer_decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): start_time = time.time() result = func(*args, **kwargs) end_time = time.time() print(f"Function {func.__name__} executed in {end_time - start_time:.4f} seconds") return result return wrapper @timer_decorator def example_function(seconds): print(f"Sleeping for {seconds} seconds...") time.sleep(seconds) print("Awake!")
# 使用装饰器 example_function(2)
在这个例子中,timer_decorator是一个装饰器,它接收一个函数func作为参数,并返回一个新的函数wrapper。wrapper函数在调用原函数func之前和之后分别记录了时间,并计算了函数的执行时间。然后,它使用@timer_decorator语法被应用到example_function函数上。当example_function被调用时,它的执行时间会被自动记录下来并打印出来。
面试官:使用装饰器式,如何保持被装饰函数的元数据?
在Python中,当使用装饰器装饰一个函数时,被装饰函数的元数据(如函数名、文档字符串、模块名等)可能会丢失,因为装饰器通常会返回一个新的函数对象,这个新的函数对象会覆盖原始函数对象的元数据。
为了保持被装饰函数的元数据,可以使用以下几种方法:
1. 手动复制元数据
可以在装饰器内部手动将原始函数的元数据复制到新的函数对象上。例如:
def my_decorator(func): def wrapper(*args, **kwargs):
# 在这里添加额外的功能 result = func(*args, **kwargs) return result
# 手动复制元数据 wrapper.__name__ = func.__name__ wrapper.__doc__ = func.__doc__ wrapper.__module__ = func.__module__
# 可以根据需要复制其他元数据 return wrapper
这种方法虽然有效,但比较繁琐,每次创建装饰器时都需要手动复制元数据。
2. 使用 functools.wraps 装饰器
functools.wraps 是Python标准库中的一个装饰器,它用于在定义装饰器时保留被装饰函数的元数据。使用 @wraps
可以简化元数据复制的过程,并使代码更加清晰和可读。
from functools import wraps def my_decorator(func): @wraps(func) def wrapper(*args, **kwargs):
# 在这里添加额外的功能 result = func(*args, **kwargs) return result return wrapper
在这个例子中, @wraps(func) 会告诉Python, wrapper 函数应该拥有 func
(即被装饰的函数)的所有元数据。这样,当使用 my_decorator 装饰一个函数时,该函数的元数据就会被保留下来。
3. 注意事项
-
使用
functools.wraps时,应该将其放在内部函数wrapper的定义之前,以确保在返回wrapper之前就已经复制了元数据。 -
functools.wraps不会复制所有可能的元数据,例如__annotations__(函数注解)等,如果需要保留这些元数据,可以手动进行复制。 -
在使用多个装饰器时,每个装饰器都应该使用
@wraps来保留被装饰函数的元数据,以确保最终的函数对象具有正确的元数据。
为了保持被装饰函数的元数据,推荐使用 functools.wraps 装饰器。它不仅可以简化元数据复制的过程,还可以提高代码的可读性和可维护性。
面试官:Python装饰器可以接收参数吗?
可以的,Python装饰器可以接收参数。实际上,装饰器本身就是一个高阶函数,它接收一个函数作为参数,并返回一个新的函数。但是,装饰器还可以被设计为接收额外的参数,这些参数允许你更灵活地控制装饰器的行为。
要实现一个接收参数的装饰器,你需要将装饰器函数定义为一个接收参数的普通函数,并在这个函数内部返回一个真正的装饰器(即另一个高阶函数)。这个内部的高阶函数将接收被装饰的函数作为参数,并返回一个新的函数,这个新函数包含了装饰器的逻辑。
下面是一个接收参数的装饰器的示例:
from functools import wraps def my_decorator_with_args(arg1, arg2): def actual_decorator(func): @wraps(func) def wrapper(*args, **kwargs):
# 在这里可以使用arg1和arg2 print(f"Decorator arguments: {arg1}, {arg2}") result = func(*args, **kwargs) return result return wrapper return actual_decorator
# 使用装饰器时,需要传递参数 @my_decorator_with_args("Hello", "World") def my_function(): print("Function is being called")
# 调用函数 my_function()
在这个例子中, my_decorator_with_args 是一个接收两个参数 arg1 和 arg2
的装饰器工厂函数。它返回了一个名为 actual_decorator 的真正装饰器,这个装饰器接收一个函数 func
作为参数,并返回一个新的函数 wrapper 。在 wrapper 函数内部,我们可以使用 arg1 和 arg2
这两个参数。
当使用 @my_decorator_with_args("Hello", "World") 语法装饰 my_function 时,实际上是将
my_function 作为参数传递给了 actual_decorator ,并且 "Hello" 和 "World"
被传递给了 my_decorator_with_args 。最终, my_function 被 wrapper 函数所替代,而 wrapper 函数在调用时会打印出装饰器的参数,并调用原始的 my_function 。
面试官:再说说看在Python中,装饰器与闭包有什么关系?
在Python中,可以说装饰器通常是通过闭包来实现的。以下是它们之间关系的详细解释:
闭包
-
定义 :闭包是指在函数内部定义的另一个函数,这个内部函数可以访问外部函数的局部变量,即使外部函数已经返回。换句话说,闭包使得内部函数能够“记住”并访问其外部函数的作用域中的变量。
-
特性 :
* 嵌套函数:闭包必须包含一个内部函数(嵌套函数)。
* 引用外部变量:内部函数必须引用包含它的外部函数中的变量。
* 外部函数返回内部函数:外部函数必须返回这个内部函数。
装饰器
-
定义 :装饰器是一种高级函数,用于在不修改已有函数或方法定义的前提下,动态地添加功能。装饰器本质上是一个高阶函数,它接受一个函数作为输入并返回一个新的函数。
-
实现方式 :装饰器通常使用闭包来保持对被装饰函数的引用,并在内部函数中访问和修改装饰器的参数或状态。通过这种方式,装饰器可以在不改变原函数代码的情况下,为其添加额外的功能。
-
特性 :
* 高阶函数:装饰器是接受一个函数作为参数并返回一个新函数的高阶函数。
* 可组合性:多个装饰器可以通过堆叠的方式组合使用,每个装饰器依次作用于被装饰的函数。
* 保持函数签名:使用 ` functools.wraps ` 装饰器可以保持被装饰函数的原始签名和文档字符串,使得装饰后的函数更具可读性。
装饰器与闭包的关系
-
实现机制 :装饰器通常是通过闭包来实现的。装饰器函数返回的内部函数(即闭包)会捕获并保存外部函数(即装饰器函数)的作用域中的变量,这些变量通常包括被装饰的函数和其他需要的状态信息。
-
功能扩展 :通过闭包,装饰器可以在不改变原函数代码的情况下,为其添加前置、后置或其他额外的功能。这些功能可以在内部函数中实现,并通过闭包的作用域访问被装饰函数的参数和返回值。
-
灵活性 :由于闭包的存在,装饰器可以处理任意数量的位置参数和关键字参数,适应不同类型的函数。这使得装饰器在Python编程中具有极高的灵活性和可重用性。
面试官:Python中有哪些内置的装饰器?
Python 中有多个内置的装饰器,以下是一些常用的内置装饰器:
@lru_cache:
* 来自 ` functools ` 模块。
* 用于缓存函数的结果,以便在后续调用中使用相同参数时,能够直接返回缓存的结果,从而提高性能。
* 特别适用于计算量大或使用相同参数频繁调用的函数。
@total_ordering:
* 同样来自 ` functools ` 模块。
* 根据类中已定义的一个或多个比较方法(如 ` __eq__ ` 和 ` __lt__ ` ),为类自动生成其他缺失的比较方法(如 ` __le__ ` 、 ` __gt__ ` 和 ` __ge__ ` )。
* 这使得类的实例之间可以进行全面的比较。
@property:
* 内置装饰器,无需导入。
* 用于将类的方法伪装成属性。
* 通过 ` @property ` 装饰的方法可以像访问属性一样被调用,同时支持 ` .setter ` 和 ` .deleter ` 方法来定义属性的设置和删除行为。
@classmethod:
* 内置装饰器,无需导入。
* 用于定义类方法,这些方法可以通过类对象或实例对象调用。
* 类方法的第一个参数通常是 ` cls ` ,代表类本身,可以访问类属性和类方法,但不能访问实例属性。
@staticmethod:
* 内置装饰器,无需导入。
* 用于定义静态方法,这些方法既不依赖于类也不依赖于类的实例。
* 静态方法相当于在类的命名空间中的普通函数,调用时无需通过类实例或类本身(尽管它们仍然可以通过这两者调用)。
@dataclass:
* 来自 ` dataclasses ` 模块(Python 3.7+)。
* 用于自动为类生成特殊方法(如 ` __init__ ` 、 ` __repr__ ` 等),从而简化类的定义。
* 与 ` @property ` 结合使用,可以方便地定义数据类及其属性。
@atexit.register:
* 来自 ` atexit ` 模块。
* 用于注册在程序正常终止时调用的函数。
* 这使得开发者可以在程序退出前执行一些清理工作,如关闭文件、释放资源等。
@abstractmethod:
* 来自 ` abc ` 模块。
* 用于声明抽象方法,即必须在子类中实现的方法。
* 含有抽象方法的类不能实例化,且继承该类的子类必须实现所有抽象方法才能实例化。
欢迎在评论区留言表达看法 或 提出你想学习的技术内容 与 面试问题,阿沛会将其作为往后更新的内容。
如果本文对大家有帮助,麻烦大家动动小手点个免费的“赞”或“在看”,大家的鼓励就是阿沛持续更新的动力~

-- 往期精彩回顾 –
美团一面:你能阐述一下CAP理论的基本概念和核心思想?说说它有哪些分布模型以及如何抉择?