通过动态设置方法的奇怪行为

strange behavioiur by dynamically setting methods

我发现动态添加方法到 class 时的“奇怪”行为。当我一个一个地(非迭代地)完成时,一切正常 [Sample 1] 但是当我尝试循环任务时,似乎所有方法都指向最后一个的主体添加方法 [示例 2.A]。在进一步尝试 [Sample 2.B] 中,我再次得到相同的“奇怪”结果。

你能帮我找出这种行为的根源是什么以及如何解决它吗?

我正在使用 python 3.9.6

这里是代码示例:

测试函数

def a(): print('a()')
def a1(): print('a1()')
def a2(p='p'): print('a2({})'.format(p))

示例 1: 一种方法(正确行为)

class Test1(object): pass

setattr(Test1, a.__name__, lambda self, *args, **kwargs: a(*args, **kwargs))
setattr(Test1, a1.__name__, lambda self, *args, **kwargs: a1(*args, **kwargs))
setattr(Test1, a2.__name__, lambda self, *args, **kwargs: a2(*args, **kwargs))

Test1().a()
Test1().a1()
Test1().a2()

输出

a()
a1()
a2(p)

示例 2.A:“奇怪”行为 - 迭代方法

class Test2(object): pass

for f in [a, a1, a2]:

    setattr(Test2, f.__name__, lambda self, *args, **kwargs: f(*args, **kwargs))
    # print(f.__name__, hasattr(Test2, f.__name__))   # debug: correct output
    # eval("Test2().{}()".format(f.__name__))   #       debug: correct output


Test2().a()
Test2().a1()
Test2().a2()
print(dir(Test2))    # the methods have right signature but same body!

输出

a2(p)
a2(p)
a2(p)
[..., 'a', 'a1', 'a2']

示例 2.B:“奇怪”行为 - 工厂方法

Test3 = type('Test3', (object,), {f.__name__: lambda self, *args, **kwargs: f(*args, **kwargs) for f in [a, a1, a2]})


Test3().a()
Test3().a1()
Test3().a2()
print(dir(Test3))    # the methods have right signature but same body!

输出

a2(p)
a2(p)
a2(p)
[..., 'a', 'a1', 'a2']

我写了一个更简化的例子来解释这里发生的事情。

funcs = []

for i in range(5):
    print(i + 1)
    funcs.append(lambda: i + 1)

print("exec lambdas")   
for func in funcs:
    print(func())

输出

1
2
3
4
5
exec lambdas
5
5
5
5
5

现在这显示了与您的问题相同的行为。在设置 lambda 时,我们看到 i 的值在增加。然而,当我稍后调用 lambdas 时,它们都是 return 相同的值。

这里的问题是我告诉 lambda,当我打电话给你时,我希望你 return i + 1 的值。然而,当我开始执行 lambdas 函数时,循环已经完成。循环后 i 的值现在是 4。因为所有的 lambda 都说同样的事情,return i + 1 的值和 i 的值现在是 4 ,lambda return 的所有执行都是相同的值,因为它们都引用 i 并且在循环结束时 i 被设置为 4.

你的情况也一样。您正在设置在循环期间评估的属性 f.__name__。然后创建一个 lambda,稍后执行时将计算 f。当你在循环之后执行 lambda 时,f 将指向 a2 所以你所有的属性 a a1 a2 都将在循环之后调用并评估 f 意味着 f 指向它设置的最后一项,即 a2

感谢 Chris Doyle 的考虑,我提出了问题的解决方案(至少在语法方面)。

示例2.A的解决方案:“奇怪”行为 - 迭代方法

class Test2(object): pass

for f in [a, a1, a2]:
   setattr(Test2, f.__name__, \
            eval('lambda self,*args, **kwargs: {}(*args, **kwargs)'.format(f.__name__))) # ok

Test2().a()
Test2().a1()
Test2().a2()

输出

a()
a1()
a2(p)

备注

  1. 重要的是 eval 作用于完整的 lambda 表达式,如果它只是 return 值,eval("f(*args, **kwargs)",那么问题与之前相同
  2. 对于无 eval 的解决方案,传递迭代变量 f 作为匿名函数的键值参数 setattr(Test2, f.__name__, lambda self, f=f, *args, **kwargs: f(*args, **kwargs))

示例2.B的解决方案:“奇怪”行为 - 工厂方法

d = {f.__name__: eval('lambda self, *args, **kwargs: f(*args, **kwargs)', dict(f=f, )) for f in [a, a1, a2]}  # ok

Test3 = type('Test3', (object,), d)

Test3().a()
Test3().a1()
Test3().a2()

输出

a()
a1()
a2(p)

备注

对于无 eval 的解决方案,它与上面

的论证相同

d = {f.__name__: lambda self, f=f, *args, **kwargs: f(*args, **kwargs) for f in [a, a1, a2]}

我想问题的根源是与嵌套范围的某种冲突。如果有人想在这一点上添加一些评论,那么问题就可以认为已经解决了。

我当然也对不同的解决方案持开放态度...也许使用 nonlocal 关键字或更奇特的后缀:)