Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to store functions as class variables in python?

I am writing a framework, and I want my base class to use different functions for renaming in the child classes. I figured the best way would be to use a class attribute, like in case of A, but I got TypeErrors when running it like in rename_columns(). However it worked with implementation like B

import pandas as pd

class A:
    my_func_mask = str.lower
    foo = 'bar'

    def rename_columns(self, data):
        return data.rename(columns=self.my_func_mask)

class B(A):

    def rename_columns(self, data):
        return data.rename(columns=self.__class__.my_func_mask)

So I experimented with the above a bit, and I get the following:

a = A()
a.foo # Works fine, gives back 'bar'
a.__class__.my_func_mask # Works as expected `a.__class__.my_func_mask is str.lower` is true
a.my_func_mask # throws TypeError: descriptor 'lower' for 'str' objects doesn't apply to 'A' object

My questions would be why can I use regular typed (int, str, etc.) values as class attributes and access them on the instance as well, while I cannot do that for functions? What happens during the attribute lookup in these cases? What is the difference in the attribute resolution process? Actually both foo and my_func_mask is in __class__.__dict__ so I am a bit puzzled. Thanks for the clarifications!

like image 857
Hodossy Szabolcs Avatar asked Jan 07 '19 12:01

Hodossy Szabolcs


3 Answers

You are storing an unbound built-in method on your class, meaning it is a descriptor object. When you then try to access that on self, descriptor binding applies but the __get__ method called to complete the binding tells you that it can't be bound to your custom class instances, because the method would only work on str instances. That's a strict limitation of most methods of built-in types.

You need to store it in a different manner; putting it inside another container, such as a list or dictionary, would avoid binding. Or you could wrap it in a staticmethod descriptor to have it be bound and return the original. Another option is to not store this as a class attribute, and simply create an instance attribute in __init__.

But in this case, I'd not store str.lower as an attribute value, at all. I'd store None and fall back to str.lower when you still encounter None:

return data.rename(columns=self.my_func_mask or str.lower)

Setting my_func_mask to None is a better indicator that a default is going to be used, clearly distinguishable from explicitly setting str.lower as the mask.

like image 194
Martijn Pieters Avatar answered Oct 17 '22 14:10

Martijn Pieters


You need to declare staticmethod.

class A:
    my_func_mask = staticmethod(str.lower)
    foo = 'bar'

>>> A().my_func_mask is str.lower
>>> True
like image 2
wangkai Avatar answered Oct 17 '22 15:10

wangkai


Everything that is placed in the class definition is bound to the class, but you can't bind a built-in to your own class.

Essentially, all code that you place in a class is executed when the class is created. All items in locals() are then bound to your class at the end of the class. That's why this also works to bind a method to your class:

def abc(self):
    print('{} from outside the class'.format(self))

class A:
    f1 = abc
    f2 = lambda self: print('{} from lambda'.format(self))
    def f3(self):
        print('{} from method'.format(self))

To not have the function bound to your class, you have to place it in the __init__ method of your class:

class A:
    def __init__(self):
        self.my_func_mask = str.lower
like image 1
user8181134 Avatar answered Oct 17 '22 15:10

user8181134