Search code examples
pythonpython-3.xpython-3.5class-methodfunctools

Combination of functools.partialmethod and classmethod


I would like to use functools.partialmethod on a classmethod. However the behavior I find is not what I would expect (and like to have). Here is an example:

class A(object):
    @classmethod
    def h(cls, x, y):
        print(cls, x, y)

class B(A):
    h = functools.partialmethod(A.h, "fixed")

When I do

>>> b = B()
>>> b.h(3)

I get an error:

...
TypeError: h() takes 3 positional arguments but 4 were given

This is consistent with

>>> b.h()
<class '__main__.A'> <__main__.B object at 0x1034739e8> fixed

However, I would expect (and like to have) the following behavior:

>>> b.h(4)
<class '__main__.B'> fixed 4

I think that functools.partialmethod treats B.h as a normal instance method and passes the actual instance as first argument automatically. But this behavior renders functools.partialmethod useless for freezing arguments in classmethods of inheriting classes.


Solution

  • Without going into too much detail, the partial object doesn't mix well with the descriptor protocol that @classmethod utilizes to create a class instance. The simple fix is to just define your overridden method in the usual fashion:

    class B(A):
        @classmethod
        def h(cls, y):
            return A.h("fixed", y)
    

    It might be possible to do what you want with some invocation of partial, but I was unable to find it. Here are some of my attempts, and why they failed.

    A.h invokes the __get__ method of the function object, returning a function where the first argument is already bound to the calling class. partial then applies that function to "fixed", but then the resulting callable still has a __get__ method that tries to insert the calling class into the resulting call. You might try to work around that by defining h to actually be a static method:

    class B(A):
        h = staticmethod(partial(A.h, "fixed"))
    
    >>> B.h(4)
    <class '__main__.A'> fixed 4
    

    But as you can see, you already froze the class argument when you call partial. Another attempt is to avoid the descriptor protocol by accessing the argument directly:

    class B(A):
        h = staticmethod(partial(A.__dict__["h"], "fixed"))
    

    but classmethod objects aren't actually callable; only the return value of their __get__ methods is, so the call to partial fails.