程序员阿沛
发布于 2026-06-27 / 0 阅读
0
0

腾讯二面说说看什么是Python装饰器它和闭包是什么关系如何保持被装饰函数的元数据

腾讯二面:说说看什么是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 是一个接收两个参数 arg1arg2
的装饰器工厂函数。它返回了一个名为 actual_decorator 的真正装饰器,这个装饰器接收一个函数 func
作为参数,并返回一个新的函数 wrapper 。在 wrapper 函数内部,我们可以使用 arg1arg2
这两个参数。

当使用 @my_decorator_with_args("Hello", "World") 语法装饰 my_function 时,实际上是将
my_function 作为参数传递给了 actual_decorator ,并且 "Hello""World"
被传递给了 my_decorator_with_args 。最终, my_functionwrapper 函数所替代,而 wrapper 函数在调用时会打印出装饰器的参数,并调用原始的 my_function

面试官:再说说看在Python中,装饰器与闭包有什么关系?

在Python中,可以说装饰器通常是通过闭包来实现的。以下是它们之间关系的详细解释:

闭包

  1. 定义 :闭包是指在函数内部定义的另一个函数,这个内部函数可以访问外部函数的局部变量,即使外部函数已经返回。换句话说,闭包使得内部函数能够“记住”并访问其外部函数的作用域中的变量。

  2. 特性 :

* 嵌套函数:闭包必须包含一个内部函数(嵌套函数)。

* 引用外部变量:内部函数必须引用包含它的外部函数中的变量。

* 外部函数返回内部函数:外部函数必须返回这个内部函数。

装饰器

  1. 定义 :装饰器是一种高级函数,用于在不修改已有函数或方法定义的前提下,动态地添加功能。装饰器本质上是一个高阶函数,它接受一个函数作为输入并返回一个新的函数。

  2. 实现方式 :装饰器通常使用闭包来保持对被装饰函数的引用,并在内部函数中访问和修改装饰器的参数或状态。通过这种方式,装饰器可以在不改变原函数代码的情况下,为其添加额外的功能。

  3. 特性 :

* 高阶函数:装饰器是接受一个函数作为参数并返回一个新函数的高阶函数。

* 可组合性:多个装饰器可以通过堆叠的方式组合使用,每个装饰器依次作用于被装饰的函数。

* 保持函数签名:使用 ` functools.wraps ` 装饰器可以保持被装饰函数的原始签名和文档字符串,使得装饰后的函数更具可读性。

装饰器与闭包的关系

  1. 实现机制 :装饰器通常是通过闭包来实现的。装饰器函数返回的内部函数(即闭包)会捕获并保存外部函数(即装饰器函数)的作用域中的变量,这些变量通常包括被装饰的函数和其他需要的状态信息。

  2. 功能扩展 :通过闭包,装饰器可以在不改变原函数代码的情况下,为其添加前置、后置或其他额外的功能。这些功能可以在内部函数中实现,并通过闭包的作用域访问被装饰函数的参数和返回值。

  3. 灵活性 :由于闭包的存在,装饰器可以处理任意数量的位置参数和关键字参数,适应不同类型的函数。这使得装饰器在Python编程中具有极高的灵活性和可重用性。

面试官:Python中有哪些内置的装饰器?

Python 中有多个内置的装饰器,以下是一些常用的内置装饰器:

  1. @lru_cache
* 来自 ` functools ` 模块。

* 用于缓存函数的结果,以便在后续调用中使用相同参数时,能够直接返回缓存的结果,从而提高性能。

* 特别适用于计算量大或使用相同参数频繁调用的函数。
  1. @total_ordering
* 同样来自 ` functools ` 模块。

* 根据类中已定义的一个或多个比较方法(如 ` __eq__ ` 和 ` __lt__ ` ),为类自动生成其他缺失的比较方法(如 ` __le__ ` 、 ` __gt__ ` 和 ` __ge__ ` )。

* 这使得类的实例之间可以进行全面的比较。
  1. @property
* 内置装饰器,无需导入。

* 用于将类的方法伪装成属性。

* 通过 ` @property ` 装饰的方法可以像访问属性一样被调用,同时支持 ` .setter ` 和 ` .deleter ` 方法来定义属性的设置和删除行为。
  1. @classmethod
* 内置装饰器,无需导入。

* 用于定义类方法,这些方法可以通过类对象或实例对象调用。

* 类方法的第一个参数通常是 ` cls ` ,代表类本身,可以访问类属性和类方法,但不能访问实例属性。
  1. @staticmethod
* 内置装饰器,无需导入。

* 用于定义静态方法,这些方法既不依赖于类也不依赖于类的实例。

* 静态方法相当于在类的命名空间中的普通函数,调用时无需通过类实例或类本身(尽管它们仍然可以通过这两者调用)。
  1. @dataclass
* 来自 ` dataclasses ` 模块(Python 3.7+)。

* 用于自动为类生成特殊方法(如 ` __init__ ` 、 ` __repr__ ` 等),从而简化类的定义。

* 与 ` @property ` 结合使用,可以方便地定义数据类及其属性。
  1. @atexit.register
* 来自 ` atexit ` 模块。

* 用于注册在程序正常终止时调用的函数。

* 这使得开发者可以在程序退出前执行一些清理工作,如关闭文件、释放资源等。
  1. @abstractmethod
* 来自 ` abc ` 模块。

* 用于声明抽象方法,即必须在子类中实现的方法。

* 含有抽象方法的类不能实例化,且继承该类的子类必须实现所有抽象方法才能实例化。

欢迎在评论区留言表达看法 或 提出你想学习的技术内容 与 面试问题,阿沛会将其作为往后更新的内容。

如果本文对大家有帮助,麻烦大家动动小手点个免费的“赞”或“在看”,大家的鼓励就是阿沛持续更新的动力~

-- 往期精彩回顾 –

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

美团二面:请你说说看分布式事务中的两阶段提交(2PC)的过程以及可能发生的问题

字节二面:请讲一下程序的内存分区/内存模型?

面试题:什么时候会出现TCP粘包,如何解决TCP粘包

操作系统入门(六) 内存篇之30张图爆肝内存连续分配管理、页式存储、段式存储和段页式存储


评论