Python装饰器:如何通过套层壳提升代码功能?

2026-05-25 15:251阅读0评论SEO资源
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计1991个文字,预计阅读时间需要8分钟。

Python装饰器:如何通过套层壳提升代码功能?

《Python技巧:The Book》第三章Effective Functions学习心得,主要内容涵盖作用域、闭包以及Python装饰器等,重点分析了UnboundLocalError和NameError两种异常的触发条件。

本文记录《Python Tricks: The Book》第三章“Effective Functions”的学习心得,主要内容包括作用域、闭包以及Python装饰器等,重点对UnboundLocalError和NameError两种异常的触发条件进行了分析,并通过示例说明了Python装饰器的基础知识和使用技巧。 Python装饰器:套层壳我变得更强了
  • Python装饰器:套层壳我变得更强了
    • 关于作用域和闭包可以聊点什么?
      • 什么是作用域
      • 什么是闭包
    • 装饰器:套层壳我变得更强了
    • 参考资料

昨天阅读了《Python Tricks: The Book》的第三章“Effective Functions”,这一章节介绍了Python函数的灵活用法,包括lambda函数、装饰器、不定长参数*args和**kwargs等,书中关于闭包的介绍让我回想起了《你不知道的JavaScript-上卷》中的相关内容。本文主要记录自己在学习Python闭包和装饰器过程中的一些心得体会,部分内容直接摘抄自参考资料。

关于作用域和闭包可以聊点什么? 什么是作用域

作用域负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。换句话说,作用域是根据名称查找变量的一套规则。

作用域的以下两点规则需要特别注意:

  • “遮蔽效应”:作用域查找会在找到第一个匹配的标识符时停止,嵌套作用域内部的标识符会遮蔽外部的标识符;

  • 提升:无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理,可以形象地认为变量和函数声明从它们在代码中出现的位置被“移动”到了所在作用域的顶部。

下面通过一个例子进行说明:

level = 3 def upgrade(): """在当前等级的基础上提升一级""" level += 1 def cprint(): print('当前等级:' + '*' * level) upgrade() # UnboundLocalError: local variable 'level' referenced before assignment cprint() # 当前等级:*** print(xyz) # NameError: name 'xyz' is not defined

为什么同样是引用全局变量“level”,执行函数“upgrade”触发了“UnboundLocalError”异常,而执行函数“cprint”就不会呢?这是因为在代码编译的过程中,函数“upgrade”的赋值表达式“level += 1”会被解析为“level = level + 1”,这涉及变量声明和变量赋值两个过程。首先是变量声明,“level”会被声明为局部变量(全局作用域里面的“level”被遮盖了),并且它的声明会被提升到函数作用域的顶部;其次是变量赋值,Python解释器会从函数作用域中查询“level”,并计算表达式“level + 1”的结果,由于此时“level”虽然被声明了,但是还没有被赋值(绑定?),计算失败,触发了“UnboundLocalError”异常。

“UnboundLocalError”异常和“NameError”异常的触发条件是不同的:

  • UnboundLocalError: Raised when a reference is made to a local variable in a function or method, but no value has been bound to that variable.

  • NameError: Raised when a local or global name is not found.

从官方文档给出的描述中可以看到,“UnboundLocalError”异常是在变量被声明了(在作用域中找到了)但是还没有绑定值的时候触发,而“NameError”异常是在作用域中找不到变量的时候触发,两者是有比较明显的区别的。

通过为函数“upgrade”中的变量“level”加上global声明可以规避“UnboundLocalError”异常:

level = 3 def upgrade(): """在当前等级的基础上提升一级""" global level # global声明将“level”标记为全局变量 level += 1 upgrade() # 太棒了,没有触发异常! print(level) # 4

global声明将“level”标记为全局变量,在代码编译过程中不会再声明“level”为函数作用域里面的局部变量了。nonlocal声明具有相似的功能,但使用的场景与global不同,由于篇幅限制,这里不再展开说明。

什么是闭包

A closure remembers the values from its enclosing lexical scope even when the program flow is no longer in that scope.

当函数可以记住并访问所在的词法作用域(定义函数时所在的作用域),即使函数是在词法作用域之外执行,这时就产生了闭包。

通过计算移动平均值的例子说明Python闭包:

def make_averager(): """工厂函数""" series = [] def averager(new_value): """移动平均值计算器""" series.append(new_value) # series是外部作用域中的变量 total = sum(series) return total / len(series) return averager # 返回内部定义的函数averager averager = make_averager() averager(10) # 10 averager(20) # 15 averager(30) # 20

可以看到函数“averager”的定义体中引用了工厂函数“make_averager”的词法作用域中的局部变量“series”,当“averager”被当作对象返回并且在全局作用域中被调用,它仍然能够访问“series”的值,据此计算移动平均值。这就是闭包。

Python装饰器:如何通过套层壳提升代码功能?

Python在函数的“__code__”属性中保存了词法作用域中的局部变量和自由变量(free variable,“series”就是自由变量)的名称,在函数的“__closure__”属性中保存了自由变量的值:

averager.__code__.co_varnames # ('new_value', 'total') averager.__code__.co_freevars # ('series',) averager.__closure__ # (<cell at 0x000002135DE72FD8: list object at 0x000002135D589488>,) averager.__closure__[0].cell_contents # [10, 20, 30] 装饰器:套层壳我变得更强了

装饰器常用于把被装饰的函数(或可调用的对象)替换成其他函数,它的输入参数是一个函数,输出结果也是一个函数。装饰器是实现横切关注点(cross-cutting concerns)的绝佳方案,使用场景包括数据校验(用户登录了吗?用户有权限访问数据吗?)、缓存(functools.lru_cache)、日志打印等。

def uppercase(func): def wrapper(): original_result = func() # 引用了uppercase函数作用域中的变量func modified_result = original_result.upper() return modified_result return wrapper def make_greeting_words(): """来段问候语""" return 'Hello, World!' greet = uppercase(make_greeting_words) # 用uppercase装饰make_greeting_words greet() # 'HELLO, WORLD!',好耶,单词变成大写的了! greet.__name__ # 'wrapper' greet.__doc__ # None

观察以上例子可以发现:

  1. 装饰器的输入是一个函数,输出也是一个函数;
  2. 被装饰的函数的一些元信息(原始函数名、文档字符串)被覆盖了;
  3. 装饰器基于闭包。

Python提供了通过@decorator_name的方式使用装饰器的语法糖。此外,通过使用functools.wraps(func),被装饰的函数的元信息能够得以保留,这有助于代码的调试:

import functools def uppercase(func): @functools.wraps(func) def wrapper(): original_result = func() # 引用了uppercase函数作用域中的变量func modified_result = original_result.upper() return modified_result return wrapper @uppercase def make_greeting_words(): """来段问候语""" return 'Hello, World!' make_greeting_words() # 'HELLO, WORLD!' make_greeting_words.__name__ # 'make_greeting_words' make_greeting_words.__doc__ # '来段问候语'

带参数的装饰器:

import functools def cache(func): """memorization装饰器,用于提高递归效率""" known = dict() @functools.wraps(func) def wrapper(*args): if args not in known: known[args] = func(*args) return known[args] return wrapper @cache def fibonacci(n): """计算Fibonacci数列的第n项""" assert n >= 0, 'n必须大于等于0' return n if n in {0, 1} else fibonacci(n - 1) + fibonacci(n - 2) fibonacci(5) # 5 fibonacci(50) # 12586269025 参考资料

  1. Python Tricks: The Book
  2. 《你不知道的JavaScript-上卷》第一部分“作用域和闭包”
  3. 《流畅的Python》第7章“函数装饰器和闭包”
  4. Python UnboundLocalError和NameError错误根源解析
  5. Built-in Exceptions
  6. 《精通Python设计模式》第5章“修饰器模式”

本文共计1991个文字,预计阅读时间需要8分钟。

Python装饰器:如何通过套层壳提升代码功能?

《Python技巧:The Book》第三章Effective Functions学习心得,主要内容涵盖作用域、闭包以及Python装饰器等,重点分析了UnboundLocalError和NameError两种异常的触发条件。

本文记录《Python Tricks: The Book》第三章“Effective Functions”的学习心得,主要内容包括作用域、闭包以及Python装饰器等,重点对UnboundLocalError和NameError两种异常的触发条件进行了分析,并通过示例说明了Python装饰器的基础知识和使用技巧。 Python装饰器:套层壳我变得更强了
  • Python装饰器:套层壳我变得更强了
    • 关于作用域和闭包可以聊点什么?
      • 什么是作用域
      • 什么是闭包
    • 装饰器:套层壳我变得更强了
    • 参考资料

昨天阅读了《Python Tricks: The Book》的第三章“Effective Functions”,这一章节介绍了Python函数的灵活用法,包括lambda函数、装饰器、不定长参数*args和**kwargs等,书中关于闭包的介绍让我回想起了《你不知道的JavaScript-上卷》中的相关内容。本文主要记录自己在学习Python闭包和装饰器过程中的一些心得体会,部分内容直接摘抄自参考资料。

关于作用域和闭包可以聊点什么? 什么是作用域

作用域负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。换句话说,作用域是根据名称查找变量的一套规则。

作用域的以下两点规则需要特别注意:

  • “遮蔽效应”:作用域查找会在找到第一个匹配的标识符时停止,嵌套作用域内部的标识符会遮蔽外部的标识符;

  • 提升:无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理,可以形象地认为变量和函数声明从它们在代码中出现的位置被“移动”到了所在作用域的顶部。

下面通过一个例子进行说明:

level = 3 def upgrade(): """在当前等级的基础上提升一级""" level += 1 def cprint(): print('当前等级:' + '*' * level) upgrade() # UnboundLocalError: local variable 'level' referenced before assignment cprint() # 当前等级:*** print(xyz) # NameError: name 'xyz' is not defined

为什么同样是引用全局变量“level”,执行函数“upgrade”触发了“UnboundLocalError”异常,而执行函数“cprint”就不会呢?这是因为在代码编译的过程中,函数“upgrade”的赋值表达式“level += 1”会被解析为“level = level + 1”,这涉及变量声明和变量赋值两个过程。首先是变量声明,“level”会被声明为局部变量(全局作用域里面的“level”被遮盖了),并且它的声明会被提升到函数作用域的顶部;其次是变量赋值,Python解释器会从函数作用域中查询“level”,并计算表达式“level + 1”的结果,由于此时“level”虽然被声明了,但是还没有被赋值(绑定?),计算失败,触发了“UnboundLocalError”异常。

“UnboundLocalError”异常和“NameError”异常的触发条件是不同的:

  • UnboundLocalError: Raised when a reference is made to a local variable in a function or method, but no value has been bound to that variable.

  • NameError: Raised when a local or global name is not found.

从官方文档给出的描述中可以看到,“UnboundLocalError”异常是在变量被声明了(在作用域中找到了)但是还没有绑定值的时候触发,而“NameError”异常是在作用域中找不到变量的时候触发,两者是有比较明显的区别的。

通过为函数“upgrade”中的变量“level”加上global声明可以规避“UnboundLocalError”异常:

level = 3 def upgrade(): """在当前等级的基础上提升一级""" global level # global声明将“level”标记为全局变量 level += 1 upgrade() # 太棒了,没有触发异常! print(level) # 4

global声明将“level”标记为全局变量,在代码编译过程中不会再声明“level”为函数作用域里面的局部变量了。nonlocal声明具有相似的功能,但使用的场景与global不同,由于篇幅限制,这里不再展开说明。

什么是闭包

A closure remembers the values from its enclosing lexical scope even when the program flow is no longer in that scope.

当函数可以记住并访问所在的词法作用域(定义函数时所在的作用域),即使函数是在词法作用域之外执行,这时就产生了闭包。

通过计算移动平均值的例子说明Python闭包:

def make_averager(): """工厂函数""" series = [] def averager(new_value): """移动平均值计算器""" series.append(new_value) # series是外部作用域中的变量 total = sum(series) return total / len(series) return averager # 返回内部定义的函数averager averager = make_averager() averager(10) # 10 averager(20) # 15 averager(30) # 20

可以看到函数“averager”的定义体中引用了工厂函数“make_averager”的词法作用域中的局部变量“series”,当“averager”被当作对象返回并且在全局作用域中被调用,它仍然能够访问“series”的值,据此计算移动平均值。这就是闭包。

Python装饰器:如何通过套层壳提升代码功能?

Python在函数的“__code__”属性中保存了词法作用域中的局部变量和自由变量(free variable,“series”就是自由变量)的名称,在函数的“__closure__”属性中保存了自由变量的值:

averager.__code__.co_varnames # ('new_value', 'total') averager.__code__.co_freevars # ('series',) averager.__closure__ # (<cell at 0x000002135DE72FD8: list object at 0x000002135D589488>,) averager.__closure__[0].cell_contents # [10, 20, 30] 装饰器:套层壳我变得更强了

装饰器常用于把被装饰的函数(或可调用的对象)替换成其他函数,它的输入参数是一个函数,输出结果也是一个函数。装饰器是实现横切关注点(cross-cutting concerns)的绝佳方案,使用场景包括数据校验(用户登录了吗?用户有权限访问数据吗?)、缓存(functools.lru_cache)、日志打印等。

def uppercase(func): def wrapper(): original_result = func() # 引用了uppercase函数作用域中的变量func modified_result = original_result.upper() return modified_result return wrapper def make_greeting_words(): """来段问候语""" return 'Hello, World!' greet = uppercase(make_greeting_words) # 用uppercase装饰make_greeting_words greet() # 'HELLO, WORLD!',好耶,单词变成大写的了! greet.__name__ # 'wrapper' greet.__doc__ # None

观察以上例子可以发现:

  1. 装饰器的输入是一个函数,输出也是一个函数;
  2. 被装饰的函数的一些元信息(原始函数名、文档字符串)被覆盖了;
  3. 装饰器基于闭包。

Python提供了通过@decorator_name的方式使用装饰器的语法糖。此外,通过使用functools.wraps(func),被装饰的函数的元信息能够得以保留,这有助于代码的调试:

import functools def uppercase(func): @functools.wraps(func) def wrapper(): original_result = func() # 引用了uppercase函数作用域中的变量func modified_result = original_result.upper() return modified_result return wrapper @uppercase def make_greeting_words(): """来段问候语""" return 'Hello, World!' make_greeting_words() # 'HELLO, WORLD!' make_greeting_words.__name__ # 'make_greeting_words' make_greeting_words.__doc__ # '来段问候语'

带参数的装饰器:

import functools def cache(func): """memorization装饰器,用于提高递归效率""" known = dict() @functools.wraps(func) def wrapper(*args): if args not in known: known[args] = func(*args) return known[args] return wrapper @cache def fibonacci(n): """计算Fibonacci数列的第n项""" assert n >= 0, 'n必须大于等于0' return n if n in {0, 1} else fibonacci(n - 1) + fibonacci(n - 2) fibonacci(5) # 5 fibonacci(50) # 12586269025 参考资料

  1. Python Tricks: The Book
  2. 《你不知道的JavaScript-上卷》第一部分“作用域和闭包”
  3. 《流畅的Python》第7章“函数装饰器和闭包”
  4. Python UnboundLocalError和NameError错误根源解析
  5. Built-in Exceptions
  6. 《精通Python设计模式》第5章“修饰器模式”