为什么具有相同ID的两个函数可以具有不同的属性?

16

为什么两个具有相同id值的函数可以具有不同的属性,例如__doc____name__

这是一个玩具示例:

some_dict = {}
for i in range(2):
    def fun(self, *args):
        print i
    fun.__doc__ = "I am function {}".format(i)
    fun.__name__ = "function_{}".format(i)
    some_dict["function_{}".format(i)] = fun

my_type = type("my_type", (object,), some_dict)
m = my_type()

print id(m.function_0)
print id(m.function_1)
print m.function_0.__doc__
print m.function_1.__doc__
print m.function_0.__name__
print m.function_1.__name__
print m.function_0()
print m.function_1()

这将打印出:

57386560
57386560
I am function 0
I am function 1
function_0
function_1
1 # <--- Why is it bound to the most recent value of that variable?
1

我尝试过在调用copy.deepcopy时进行混合(不确定函数是否需要递归复制,或者这是否过度),但这并没有改变任何东西。


“为什么它绑定到该变量的最新值?”因为涉及闭包和延迟存储的 i - Hyperboreus
为什么依赖于 ifun.__doc__ 没有发生相同的事情呢? - ely
2
因为 format 立即被求值,而 fun 的主体部分却不会。 - Hyperboreus
你真的想要每个帖子只有一个问题。这里有一个部分重复的内容,关于Python嵌套函数中的局部变量 - Martijn Pieters
4个回答

18

您正在比较不同的方法,每次在实例或类上访问方法对象时都会重新创建(通过描述符协议)。

一旦测试了它们的id(),您就可以丢弃该方法了(没有对它的引用),因此当您创建另一个方法时,Python可以自由地重用id。 在这里,您想通过使用m.function_0.__func__m.function_1.__func__来测试实际函数:

>>> id(m.function_0.__func__)
4321897240
>>> id(m.function_1.__func__)
4321906032

方法对象从其包装的函数继承__doc____name__属性。实际的底层函数仍然是不同的对象。

至于两个返回1的函数; 两个函数都使用i作为闭包; 当您调用方法时查找i的值,而不是创建函数时。请参见Python嵌套函数中的局部变量

最简单的解决方法是添加另一个作用域和一个工厂函数:

some_dict = {}
for i in range(2):
    def create_fun(i):
        def fun(self, *args):
            print i
        fun.__doc__ = "I am function {}".format(i)
        fun.__name__ = "function_{}".format(i)
        return fun
    some_dict["function_{}".format(i)] = create_fun(i)

在最后一条评论中,除了将变量放在函数签名中之外,是否有一种方法可以将i的值记忆到函数中,以便它查找创建时存在的值。我认为这是我真正要问的问题,但在阅读您对我的第一层困惑的答案之后,我才意识到这一点。 - ely
1
@EMS:请查看链接的帖子;我在那里向您介绍了几个选项。如果这是您真正的问题,那么它就是链接帖子的重复。 - Martijn Pieters
所有这些函数似乎都要求将要绑定的变量作为参数传递。然而,我正在尝试创建一些具有相同主体且函数签名大部分相同的函数,但是参数签名将会不同。 - ely
@EMS:第一个选项创建了一个新的作用域;每次调用该作用域时,闭包都会绑定在该作用域中。例如,生成的函数对象在签名中没有 i - Martijn Pieters
感谢您提供的所有澄清。链接的答案非常详尽,价值连城。 - ely
显示剩余2条评论

3
根据您对ndpu答案的评论,这里有一种无需使用可选参数即可创建函数的方法:
for i in range(2):
    def funGenerator(i):
        def fun1(self, *args):
            print i
        return fun1
    fun = funGenerator(i)
    fun.__doc__ = "I am function {}".format(i)
    fun.__name__ = "function_{}".format(i)
    some_dict["function_{}".format(i)] = fun

@EMS:这里使用了重复帖子中的第二个选项;一个创建新作用域的工厂函数。 - Martijn Pieters
我正在思考这种方法的一个问题:如果 self 事先不知道怎么办?在这个例子中,函数调用 funGenerator(i) 如何工作?它应该期望 self 是第一个参数,对吧?如何解决这个问题? - ely
1
@EMS 好眼力。事实上,除了 i 之外,我并不需要给 funGenerator 传递任何参数。我已经更新了我的答案来反映这一点。 - Rob Watts

2
您需要保存当前的 i 以实现此操作:
1 # <--- Why is it bound to the most recent value of that variable?
1

举例来说,通过为函数参数设置默认值来工作:

for i in range(2):
    def fun(self, i=i, *args):
        print i
# ...

或者创建一个闭包:

for i in range(2):
    def f(i):
        def fun(self, *args):
            print i
        return fun
    fun = f(i)
# ...

@EMS 你可以创建一个函数,该函数创建并返回另一个函数。 - Rob Watts
@EMS,就像Rob所说的那样。只要打破闭包即可,无论如何。 - Hyperboreus

2

@Martjin Pieters是完全正确的。为了说明,尝试进行以下修改

some_dict = {}

for i in range(2):
    def fun(self, *args):
        print i

    fun.__doc__ = "I am function {}".format(i)
    fun.__name__ = "function_{}".format(i)
    some_dict["function_{}".format(i)] = fun
    print "id",id(fun)

my_type = type("my_type", (object,), some_dict)
m = my_type()

print id(m.function_0)
print id(m.function_1)
print m.function_0.__doc__
print m.function_1.__doc__
print m.function_0.__name__
print m.function_1.__name__
print m.function_0()
print m.function_1()

c = my_type()
print c
print id(c.function_0)

你会发现每次fun函数都有不同的id,并且与最终的id不同。这是由于方法创建逻辑将其指向了相同的位置,因为那里存储了类的代码。此外,如果你使用my_type作为一种类,使用它创建的实例对于该函数具有相同的内存地址。

这段代码给出了以下结果:
id 4299601152
id 4299601272
4299376112
4299376112

我是函数0
我是函数1
function_0
function_1
1

1

<main.my_type object at 0x10047c350>
4299376112


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接