Search code examples
python-3.xdynamic

strange behavioiur by dynamically setting methods


I found out a "strange" behavior of when adding methods dynamically to a class. When I do it one by one (non iteratively) everything works fine [Sample 1] but when I try to loop the task then it seems the all the methods points to the body of the last added method [Sample 2.A]. In a further attempt [Sample 2.B] I end up again to the same "strange" result.

Could you help me find out what is the origin of such behavior and how to fix it?

I am using python 3.9.6

Here the code samples:

Test functions

def a(): print('a()')
def a1(): print('a1()')
def a2(p='p'): print('a2({})'.format(p))

Sample 1: one by one approach (correct behavior)

class Test1(object): pass

setattr(Test1, a.__name__, lambda self, *args, **kwargs: a(*args, **kwargs))
setattr(Test1, a1.__name__, lambda self, *args, **kwargs: a1(*args, **kwargs))
setattr(Test1, a2.__name__, lambda self, *args, **kwargs: a2(*args, **kwargs))

Test1().a()
Test1().a1()
Test1().a2()

Output

a()
a1()
a2(p)

Sample 2.A: "strange" behavior - iterative approach

class Test2(object): pass

for f in [a, a1, a2]:

    setattr(Test2, f.__name__, lambda self, *args, **kwargs: f(*args, **kwargs))
    # print(f.__name__, hasattr(Test2, f.__name__))   # debug: correct output
    # eval("Test2().{}()".format(f.__name__))   #       debug: correct output


Test2().a()
Test2().a1()
Test2().a2()
print(dir(Test2))    # the methods have right signature but same body!

Output

a2(p)
a2(p)
a2(p)
[..., 'a', 'a1', 'a2']

Sample 2.B: "strange" behavior - factory approach

Test3 = type('Test3', (object,), {f.__name__: lambda self, *args, **kwargs: f(*args, **kwargs) for f in [a, a1, a2]})


Test3().a()
Test3().a1()
Test3().a2()
print(dir(Test3))    # the methods have right signature but same body!

Output

a2(p)
a2(p)
a2(p)
[..., 'a', 'a1', 'a2']

Solution

  • UPDATE

    using an auxiliary function for the function-method binding is enough to avoid scoping problem.

    class TestClass:
        pass
    
    
    def func2method(func):
        # auxiliary function: needed to avoid for-loop scope problems
        return lambda self, *args, **kwargs: func(*args, **kwargs) # <- the extra parameter is needed for the binding with the class
    
    # binding step
    for f in (a, a1, a2):
        setattr(TestClass, f.__name__, func2method(f))
    
    
    t = TestClass()
    t.a()        # a()
    t.a1()       # a1()
    t.a2()       # a2(p)
    t.a()        # a()
    print(t.a)   # <bound method func2method.<locals>.<lambda> of <__main__.TestClass object at 0x7ff4df787a30>>
    

    Thanks to the considerations of Chris Doyle I come out with the solution of the problems (at least the syntactical side).

    Solution of Sample 2.A: "strange" behavior - iterative approach

    class Test2(object): pass
    
    for f in [a, a1, a2]:
       setattr(Test2, f.__name__, \
                eval('lambda self,*args, **kwargs: {}(*args, **kwargs)'.format(f.__name__))) # ok
    
    Test2().a()
    Test2().a1()
    Test2().a2()
    

    Output

    a()
    a1()
    a2(p)
    

    Remarks

    1. it is important that eval acts on the full lambda expression, if it were only the return value, eval("f(*args, **kwargs)", then same problem as before
    2. for an eval-free solution, pass the the iteration variable, f, as a key-value parameter of the anonymous function setattr(Test2, f.__name__, lambda self, f=f, *args, **kwargs: f(*args, **kwargs))

    Solution of Sample 2.B: "strange" behavior - factory approach

    d = {f.__name__: eval('lambda self, *args, **kwargs: f(*args, **kwargs)', dict(f=f, )) for f in [a, a1, a2]}  # ok
    
    Test3 = type('Test3', (object,), d)
    
    Test3().a()
    Test3().a1()
    Test3().a2()
    

    Output

    a()
    a1()
    a2(p)
    

    Remarks

    for an eval-free solution it holds the same argumentation as above with

    d = {f.__name__: lambda self, f=f, *args, **kwargs: f(*args, **kwargs) for f in [a, a1, a2]}

    I guess the origin of the problem is some kind of conflict with the nested scopes. If someone want to add some remarks on this point then the question can be considered solved.

    I am also open to different solution of course... maybe using nonlocal keyword or even more exotic suffs:)