eyeinthebrick
eyeinthebrick

Reputation: 548

How to store function in class attribute?

In my code I have a class, where one method is responsible for filtering some data. To allow customization for descendants I would like to define filtering function as a class attribute as per below:

def my_filter_func(x):
    return x % 2 == 0

class FilterClass(object):

    filter_func = my_filter_func

    def filter_data(self, data):
        return filter(self.filter_func, data)

class FilterClassDescendant(FilterClass):

    filter_func = my_filter_func2

However, such code leads to TypeError, as filter_func receives "self" as first argument. What is a pythonic way to handle such use cases? Perhaps, I should define my "filter_func" as a regular class method?

Upvotes: 4

Views: 4289

Answers (2)

Luis Masuelli
Luis Masuelli

Reputation: 12343

Python has a lot of magic within. One of those magics has something to do with transforming functions into UnboundMethod objects (when assigned to the class, and not to an class' instance).

When you assign a function (And I'm not sure whether it applies to any callable or just functions), Python converts it to an UnboundMethod object (i.e. an object which can be called using an instance or not).

Under normal conditions, you can call your UnboundMethod as normal:

def myfunction(a, b):
    return a + b

class A(object):
    a = myfunction

A.a(1, 2)
#prints 3

This will not fail. However, there's a distinct case when you try to call it from an instance:

A().a(1, 2)

This will fail since when an instance gets (say, internal getattr) an attribute which is an UnboundMethod, it returns a copy of such method with the im_self member populated (im_self and im_func are members of UnboundMethod). The function you intended to call, is in the im_func member. When you call this method, you're actually calling im_func with, additionally, the value in im_self. So, the function needs an additional parameter (the first one, which will stand for self).

To avoid this magic, Python has two possible decorators:

  • If you want to pass the function as-is, you must use @staticmethod. In this case, you will have the function not converted to UnboundMethod. However, you will not be able to access the calling class, except as a global reference.
  • If you want to have the same, but be able to access the current class (disregarding whether the function it is called from an instance or from a class), then your function should have another first argument (INSTEAD of self: cls) which is a reference to the class, and the decorator to use is @classmethod.

Examples:

class A(object):
    a = staticmethod(lambda a, b: a + b)

A.a(1, 2)
A().a(1, 2)

Both will work.

Another example:

def add_print(cls, a, b):
    print cls.__name__
    return a + b

class A(object):
    ap = classmethod(add_print)

class B(A):
    pass

A.ap(1, 2)
B.ap(1, 2)
A().ap(1, 2)
B().ap(1, 2)

Check this by yourseld and enjoy the magic.

Upvotes: 3

wim
wim

Reputation: 363043

You could just add it as a plain old attribute?

def my_filter_func(x):
    return x % 2 == 0

class FilterClass(object):

    def __init__(self):
        self.filter_func = my_filter_func

    def filter_data(self, data):
        return filter(self.filter_func, data)

Alternatively, force it to be a staticmethod:

def my_filter_func(x):
    return x % 2 == 0

class FilterClass(object):

    filter_func = staticmethod(my_filter_func)

    def filter_data(self, data):
        return filter(self.filter_func, data)

Upvotes: 6

Related Questions