Search code examples
pythonlambdapython-3.6getattr

__getattr__ returning lambda function requiring one argument does not work


I am in the process of learning Python 3 and just ran into the getattr function. From what I can tell, it is invoked when the attribute call is not found in the class definition as a function or a variable.

In order to understand the behaviour, I wrote the following test class (based on what I've read):

class Test(object):
    def __init__(self, foo, bar):
        self.foo = foo
        self.bar = bar

    def __getattr__(self, itm):
        if itm is 'test':
            return lambda x: "%s%s" % (x.foo, x.bar)
        raise AttributeError(itm)

And I then initate my object and call the non-existent function test which, expectedly, returns the reference to the function:

t = Test("Foo", "Bar")    
print(t.test)
<function Test.__getattr__.<locals>.<lambda> at 0x01A138E8>

However, if I call the function, the result is not the expected "FooBar", but an error:

print(t.test())
TypeError: <lambda>() missing 1 required positional argument: 'x'

In order to get my expected results, I need to call the function with the same object as the first parameter, like this:

print(t.test(t))
FooBar

I find this behaviour rather strange, as when calling p.some_function(), is said to add p as the first argument.

I would be grateful if someone could shine some light over this headache of mine. I am using PyDev in Eclipse.


Solution

  • __getattr__ return values are "raw", they don't behave like class attributes, invoking the descriptor protocol that plain methods involve that causes the creation of bound methods (where self is passed implicitly). To bind the function as a method, you need to perform the binding manually:

    import types
    
    ...
    
    def __getattr__(self, itm):
        if itm is 'test':  # Note: This should really be == 'test', not is 'test'
            # Explicitly bind function to self
            return types.MethodType(lambda x: "%s%s" % (x.foo, x.bar), self)
        raise AttributeError(itm)
    

    types.MethodType is poorly documented (the interactive help is more helpful), but basically, you pass it a user-defined function and an instance of a class and it returns a bound method that, when called, implicitly passes that instance as the first positional argument (the self argument).

    Note that in your specific case, you could just rely on closure scope to make a zero-argument function continue to work:

    def __getattr__(self, itm):
        if itm is 'test':  # Note: This should really be == 'test', not is 'test'
            # No binding, but referring to self captures it in closure scope
            return lambda: "%s%s" % (self.foo, self.bar)
        raise AttributeError(itm)
    

    Now it's not a bound method at all, just a function that happens to have captured self from the scope in which it was defined (the __getattr__ call). Which solution is best depends on your needs; creating a bound method is slightly slower, but gets a true bound method, while relying on closure scope is (trivially, ~10ns out of >400ns) faster, but returns a plain function (which may be a problem if, for example, it's passed as a callback to code that assumes it's a bound method and can have __self__ and __func__ extracted separately for instance).