Perl风格函数模板转Python时循环变量值捕获异常的解决方案咨询
你遇到的这个问题是Python闭包的经典陷阱——延迟绑定:循环里定义的函数并不会在定义时就捕获当前循环变量的具体值,而是会在函数被调用时才去查找变量的当前状态。等你调用red()、green()这些函数时,循环早就跑完了,name变量已经固定为最后一个值black,所以所有生成的函数都会用这个值。
下面给你两种直接可行的修改方案,适配你的代码场景:
方案1:用函数默认参数捕获当前值(最简便)
函数的默认参数是在定义时求值的,所以我们可以把当前循环的name值作为默认参数绑定到函数里。修改你代码中循环定义函数的部分:
for name in _colors(): # 把函数定义移到循环内部,并添加默认参数 def _f10(*_args, current_name=name): return f"<FONT COLOR='{current_name}'>{perllib.LIST_SEPARATOR.join(map(_str,_args))}</FONT>" globals()[name] = _f10
这里的current_name=name会在每次循环时,把当前name的快照保存为函数的默认参数。当调用函数时,如果没有传入current_name,就会用这个绑定好的初始值,完美解决延迟绑定的问题。
方案2:使用functools.partial绑定参数
如果你更倾向于用函数式的方式,可以借助functools.partial来预先绑定参数值:
首先导入functools:
import functools
然后修改循环部分:
def _f10(current_name, *_args): return f"<FONT COLOR='{current_name}'>{perllib.LIST_SEPARATOR.join(map(_str,_args))}</FONT>" for name in _colors(): # 预先绑定current_name为当前循环的name值 globals()[name] = functools.partial(_f10, name)
functools.partial会创建一个新的函数,把指定的参数预先绑定好,这样每个生成的函数都会持有自己对应的name值。
为什么原来的nonlocal不行?
你注释掉的nonlocal name语法错误,是因为nonlocal只能用于嵌套函数中引用外层(非全局)作用域的变量,而你的循环是在全局作用域里,name是全局变量,这里应该用global——但即使改成global name,也解决不了问题,因为global只是让函数引用全局的name变量,还是会受延迟绑定影响,调用时依然取最后一个值。
修改后的完整代码示例
#!/usr/bin/env python3 # Generated by "pythonizer -v0 test_function_templates.pl" v0.978 run by snoopyjc on Thu May 19 10:49:12 2022 # Implied pythonizer options: -m # test function templates per the perlref documentation import builtins, perllib, sys _str = lambda s: "" if s is None else str(s) perllib.init_package("main") # SKIPPED: use Carp::Assert; def _colors(*_args): return "red blue green yellow orange purple white black".split() _args = perllib.Array() builtins.__PACKAGE__ = "main" # 核心修改部分:把函数定义放到循环内,用默认参数捕获当前name值 for name in _colors(): def _f10(*_args, current_name=name): return f"<FONT COLOR='{current_name}'>{perllib.LIST_SEPARATOR.join(map(_str,_args))}</FONT>" globals()[name] = _f10 print(red("careful")) assert _str(red("careful")) == "<FONT COLOR='red'>careful</FONT>" assert _str(green("light")) == "<FONT COLOR='green'>light</FONT>" perllib.perl_print(f"{sys.argv[0]} - test passed!")
运行这个修改后的代码,print(red("careful"))就会输出预期的<FONT COLOR='red'>careful</FONT>,所有断言也能通过了。
内容的提问来源于stack exchange,提问作者snoopyjc

