2

I want to define many methods in my class TestClass. I want to call them by their name TestClass().method_1 ... TestClass().method_n.

I do not want to call them indirectly for example through an intermediate method like TestClass().use_method('method_1', params) to keep consistency with other parts of the code.

I want to define dynamically my numerous methods, but I do not understand why this minimal example does not work:

class TestClass:
    def __init__(self):
        method_names = [
            'method_1',
            'method_2']
        
        for method_name in method_names:
            def _f():
                print(method_name)
            # set the method as attribute
            # (that is OK for me that it will not be
            #   a bound method)
            setattr(
                self,
                method_name,
                _f)
            del _f

if __name__ == '__main__':
    T = TestClass()

    T.method_1()
    T.method_2()
    
    print(T.method_1)
    print(T.method_2)

Output is:

function_2
function_2
<function TestClass.__init__.<locals>._f at 0x0000022ED8F46430>
<function TestClass.__init__.<locals>._f at 0x0000022EDADCE4C0>

while I was expecting

function_1
function_2

I tried to put some copy.deepcopy at many places but it does not help.

Trying to narrow it down with a more minimal example, I am again surprised by the result:

class TestClass:
    def __init__(self):
        variable = 1

        def _f():
            print(variable)
        
        del variable

        setattr(
            self,
            'the_method',
            _f)
        del _f
        
        variable = 2

if __name__ == '__main__':
    T = TestClass()
    T.the_method()

Output is 2 while I was expecting 1.

Any hint about what is happening?

----- Edited to give solution thanks to the accepted answer from Tim Roberts (and thanks cards for noticing the type(self) instead of self-----

Minimal example:

class TestClass:
    def __init__(self):
        variable = 1

        def _f(captured_variable=variable):
            print(captured_variable)
            print(variable)
        
        del variable

        setattr(
            type(self),
            'the_method',
            _f)
        del _f
        
        variable = 2

if __name__ == '__main__':
    T = TestClass()
    T.the_method()

Output is:

1
2

And original question about dynamically defined methods:

class TestClass:
    def __init__(self):
        method_names = [
            'method_1',
            'method_2']
        
        for method_name in method_names:
            def _f(captured_method=method_name):

                print(captured_method)
                print(method_name)
                
            # set the method as attribute
            setattr(
                type(self),
                method_name,
                _f)
            del _f

if __name__ == '__main__':
    T = TestClass()

    T.method_1()
    T.method_2()
    
    print(T.method_1)
    print(T.method_2)

Output is:

method_1
method_2
method_2
method_2
<function TestClass.__init__.<locals>._f at 0x000001D1CF9D6430>
<function TestClass.__init__.<locals>._f at 0x000001D1D187E4C0>

thomast
  • 122
  • 2
  • 8
  • 1
    `for` loops don't have a separate scope for each iteration. So all the methods are in the same scope, and reference the same value fo `method_name`. – Barmar Oct 21 '21 at 20:35
  • 1
    [Similar question](https://stackoverflow.com/questions/7546285/creating-lambda-inside-a-loop), though I wouldn't mark it as a dup. The reflection trickery threw me for a loop looking at this at first, so I think this question has value on its own. – Silvio Mayolo Oct 21 '21 at 20:38
  • Thanks @SilvioMayolo. Following your link and others finally led me to [that comment](https://stackoverflow.com/questions/2295290/what-do-lambda-function-closures-capture/2295372#comment42120781_2295372) pointing to the [official FAQ describing the issue of defining lambdas in a loop](https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result) which explains well the scope issue encountered here – thomast Oct 22 '21 at 17:19

2 Answers2

5

This is one of the classic Python stumbles. Your get the value of the variable, and the variable ends up with the final value.

You can do what you want by "capturing" the value of the variable as a default:

        for method_name in method_names:
            def _f(method=method_name):
                print(method)
Tim Roberts
  • 48,973
  • 4
  • 21
  • 30
0

To make clearer how to fix the problem with the scope, you could pass the method as a key parameter of the lambda function directly and a possibility would be:

class TestClass:
    def __init__(self, *methods):
        self.methods = methods

    def add_methods(self):
        for method in self.methods:
            setattr(type(self), method.__name__, lambda self, m=method: m())

def method_1(): return '1'
def method_2(): return '2'

t = TestClass(method_1, method_2)

t.add_methods()
print(t.method_1())
print(t.method_2())

Output

1
2

I modify a bit the original code but notice setattr(type(self), ...)!!

cards
  • 3,936
  • 1
  • 7
  • 25