Search code examples
pythonpython-3.xlambdaenumeratemutability

Why is a stored function in an object property being overwritten (Python)?


The following is a minimum working example of my problem where I have a school object containing an array of fish objects, and I want each fish to have properties based on their index. All I'm doing is setting the properties and then printing them out, both during the init and later once the class is fully instantiated. The problem is that setting these properties doesn't seem to work for functions. Also, it performs differently based on whether or not enumerate is used (which I would like to understand).

In the following code the output reveals that the 'func' property of the fish in my school object is being overwritten, but the 'x' property isn't.

class Fish:
    def __init__(self):
        self.x = -1
        self.func = lambda y: -1


class School:
    def __init__(self):
        self.fishList = [Fish() for i in range(2)]
        for fishIndex, f in enumerate(self.fishList):
            f.x = fishIndex
            f.func = lambda y: fishIndex
        print("No enumerate:")
        for f in self.fishList:
            print(f.x, f.func(0))
        print("Enumerate")
        for fishIndex, f in enumerate(self.fishList):
            print(f.x, f.func(0))

mySchool = School()
print("-----------------------------------------")
print("No enumerate:")
for f in mySchool.fishList:
    print(f.x, f.func(0))
print("Enumerate:")
for fishIndex, f in enumerate(mySchool.fishList):
    print(f.x, f.func(0))


Output:

No enumerate:
0 1
1 1
Enumerate
0 0
1 1
-----------------------------------------
No enumerate:
0 1
1 1
Enumerate:
0 1
1 1

So far I have tracked the issue to originating from the how I store the fish. If I store the fish separately, as opposed to in the array, the functions persist correctly. So this makes me think that this is a mutability/pointers issue. But were that the case I would expect 'x' to not persist correctly either.

Edit: To clarify, in the output the first "Enumerate" before the dashed lines is what I expected for all of the outputs.


Solution

  • In the definition of func, fishIndex is a free variable. That means the name itself, not the current value of fishIndex, is part of the function definition.

    When the function is called, it will use the current value of the local variable fishIndex defined in __init__.

    While inside __init__, that variable changes when you use it as an index variable in the for loop.

    Outside __init__, fishIndex still refers to the variable defined in __init__, even though that function returns. We say that func is a closure over the variables of the scope in which it was defined.

    The fact that you are reusing the name fishIndex in your loop at the global scope does not affect the behavior of func.