thomast
thomast

Reputation: 152

How to define methods dynamically inside a Python class?

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?

Upvotes: 3

Views: 1201

Answers (2)

cards
cards

Reputation: 4965

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), ...)!!

Upvotes: 0

Tim Roberts
Tim Roberts

Reputation: 54688

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)

Upvotes: 6

Related Questions