Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can two functions with the same `id` have different attributes?

Why can two functions with the same id value have differing attributes like __doc__ or __name__?

Here's a toy example:

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()

Which prints:

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

I've tried mixing in a call to copy.deepcopy (not sure if the recursive copy is needed for functions or it is overkill) but this doesn't change anything.

like image 305
ely Avatar asked Mar 21 '14 18:03

ely


4 Answers

You are comparing methods, and method objects are created anew each time you access one on an instance or class (via the descriptor protocol).

Once you tested their id() you discard the method again (there are no references to it), so Python is free to reuse the id when you create another method. You want to test the actual functions here, by using m.function_0.__func__ and m.function_1.__func__:

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

Method objects inherit the __doc__ and __name__ attributes from the function that they wrap. The actual underlying functions are really still different objects.

As for the two functions returning 1; both functions use i as a closure; the value for i is looked up when you call the method, not when you created the function. See Local variables in Python nested functions.

The easiest work-around is to add another scope with a factory function:

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)
like image 175
Martijn Pieters Avatar answered Oct 20 '22 23:10

Martijn Pieters


Per your comment on ndpu's answer, here is one way you can create the functions without needing to have an optional argument:

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
like image 42
Rob Watts Avatar answered Oct 21 '22 01:10

Rob Watts


@Martjin Pieters is perfectly correct. To illustrate, try this modification

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)

You see that the fun get's a different id each time, and is different from the final one. It's the method creation logic that send's it pointing to the same location, as that's where the class's code is stored. Also, if you use the my_type as a sort of class, instances created with it have the same memory address for that function

This code gives:
id 4299601152
id 4299601272
4299376112
4299376112

I am function 0
I am function 1
function_0
function_1
1
None
1
None
<main.my_type object at 0x10047c350>
4299376112

like image 31
Simon Avatar answered Oct 21 '22 01:10

Simon


You should save current i to make this:

1 # <--- Why is it bound to the most recent value of that variable?
1

work, for example by setting default value to function argument:

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

or create a closure:

for i in range(2):
    def f(i):
        def fun(self, *args):
            print i
        return fun
    fun = f(i)
# ...
like image 21
ndpu Avatar answered Oct 20 '22 23:10

ndpu