Felipe Muniz
Felipe Muniz

Reputation: 361

Lambda isn't creating an instance method in Python 3.6

I'm trying to create methods whose names are based on a class' instance's specific attribute. My code is similar to this:

class myClass:
    info_to_be_accessed = {
        'infoA': 14,
        'infoB': 21,
        'infoC': False,
        'infoD': 'spam'
    }

    def create_methods(self):
        for info in self.info_to_be_accessed:
            setattr(self, info, lambda self: self.info_to_be_accessed.get(info, None))
        return self

c = myClass().create_methods()
print(c.infoA())

I was expecting c.infoA() to be an instance method, but it turns out as a static method that does not get the self parameter when called. Output raises a TypeError:

Traceback (most recent call last):
  File "test.py", line 15, in <module>
    print(c.infoA())
TypeError: <lambda>() missing 1 required positional argument: 'self'

Expected output:

14

EDIT: Answering @chepner. I'm honestly not sure this is the best way to deal with it, but my basic problem is that I am dealing with data that is outside my database / my own code. I'm retrieving information from a database I have no control of, so it can be analysed by my code. Basically, my real-code class has a pandas dataframe with information about the result of a complex SQL query and the ability to plot a chart about it, but sometimes I want to filter the dataframe in order to plot it, and sometimes I want to plot the whole thing. I'm not really analyzing a dictionary, but instead creating methods to filter this dataframe. Thing is, the (external) object I'm analyzing with this query has fixed values a certain attribute can be, but if this external database changes in the future, I don't want to create extra code just to cover the new value that is added there. Using methods instead of parameters is just more convenient to me when analyzing this data, but this is not a code that goes to production, merely something I use at the CLI. This is why it's convenient to me to have separate methods for each value that can be passed.

Upvotes: 3

Views: 204

Answers (1)

timgeb
timgeb

Reputation: 78770

(Somewhat) replying to wim's comment:

I have not encountered a good use case for this myself, but you can set arbitrary functions as methods of arbitrary instances. Is it good style? Probably there's a better way in most cases. But you can and I'll show how.


You are just setting an attribute which happens to be a function on your instance.

Functions are (non-data) descriptors and you can use their __get__ method to fix the first parameter.

Demo:

>>> class myClass: 
...:     def __repr__(self): # nicer output for demo 
...:         return '<myClass object>' 
...:     def create_method_on_instance(self, name, function): 
...:         setattr(self, name, function.__get__(self))                                                               
>>>                                                                                                                    
>>> m1 = myClass()                                                                                                     
>>> m2 = myClass()                                                                                                     
>>> m1.create_method_on_instance('foo', lambda self, x, y: print(self, x + y))                                           
>>> m2.create_method_on_instance('foo', lambda self, x, y: print(self, x - y))                                           
>>>                                                                                                             
>>> m1.foo(3, 4)                                                                                                       
<myClass object> 7
>>> m2.foo(3, 4)                                                                                                 
<myClass object> -1

I can't reiterate everything from the Descriptor HowTo Functions and Methods section here, but it breaks down like this:

When you access a function attribute on an instance via the dot-notation, the function's __get__ method won't be called.

However, the function's __get__ method is responsible for binding the first argument (usually called self) of the function to the instance in question (thus creating a method).

You can still call __get__ manually if you want to create methods from arbitrary functions on arbitrary instances. This can get a little silly, since you could even bind the first function argument to some instance and then set the method as an attribute of another object ;)

>>> class Unrelated: 
...:     pass 
...:
>>> u = Unrelated()                                                                                                                   
>>> u.foo = (lambda self, x, y: print(self, x+y)).__get__(m1)                                                          
>>> u.foo(3, 4)                                                                                                        
<myClass object> 7

Upvotes: 2

Related Questions