0

given the following snippet:

def fun(ret):
    return ret

class A:
    def __init__(self):
        for string in ['a', 'b']:
            setattr(self, string, lambda: fun(string))

>>> a = A()
>>> a.a()
'b'

I want a method a() which returns 'a' and a method b() which returns 'b'. As long as I don't use a lambda expression but setting the attribute to a simple string, the association is correct.

I think my intention is clear? So where am I wrong?

hinerk
  • 43
  • 1
  • 5
  • 1
    See e.g. https://stackoverflow.com/q/7514093/3001761 and the various linked questions. – jonrsharpe May 28 '18 at 16:40
  • What jon said. The link shows how to fix it using a default arg. Another thing you can do to illustrate what's going on is to set `string` after the loop, eg `string='hello'`. BTW, `string` isn't a great variable name choice, since it's the name of a standard module. – PM 2Ring May 28 '18 at 16:48

1 Answers1

1

In Python, a function will lookup non-local names in the scope where it was defined or in the global scope if the name still does not exist there. If the value associated to the name changed, so will the returned value. Note that this is not specific to lambda functions.

A way around this is to create a closure by writing a helper function.

def fun(ret):
    return ret

class A:
    def __init__(self):
        def apply_fun(item):
            return lambda: fun(item)

        for string in ['a', 'b']:
            setattr(self, string, apply_fun(string))

print(A().a())  # 'a'

Alternative solution

In that particular case, using __getattr__ might be more suited as it is intended to dynamically return attributes.

def fun(ret):
    return ret

class A:
    def __getattr__(self, item):
        if item in ['a', 'b']:
            return lambda: fun(item)

print(A().a())  # 'a'
Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73