65

I'm trying to define some class methods using another more generic class method as follows:

class RGB(object):
    def __init__(self, red, blue, green):
        super(RGB, self).__init__()
        self._red = red
        self._blue = blue
        self._green = green

    def _color(self, type):
        return getattr(self, type)

    red = functools.partial(_color, type='_red')
    blue = functools.partial(_color, type='_blue')
    green = functools.partial(_color, type='_green')

But when i attempt to invoke any of those methods i get:

rgb = RGB(100, 192, 240)
print rgb.red()
TypeError: _color() takes exactly 2 arguments (1 given)

I guess self is not passed to _color since rgb.red(rgb) works.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Arjor
  • 979
  • 1
  • 8
  • 12
  • This is a useful question, and I don't want to detract from the main point, but as a side-issue question on your code I was wondering why you wrote super(RGB, self).__init__(), when RGB inherits from object? – Captain Lepton Mar 20 '19 at 10:23
  • @CaptainLepton It's good practice, in case someday somebody adds a superclass. – michaelb958--GoFundMonica Jun 14 '19 at 02:02
  • @michaelb958--GoFundMonica you meant making `RGB` superclass to a newer written subclass? – deadvoid Mar 16 '21 at 08:54
  • @deadvoid he means if someone edits the line `class RGB(object)` to instead say something like `class RGB(Other class)`, then the `__init__` is still correct. Of course the tradeoff is that `super(RGB, self)` breaks if you instead edit it to `class Other name(object)`. Of course Python 3 fixed that by letting you just call `super()`. – mtraceur May 25 '22 at 17:34
  • @CaptainLepton the biggest reason why it's good practice is that it makes your class actually work right if someone wants to multiply inherit from both your class and another class. See [this answer](https://stackoverflow.com/a/16310777/4372452). – mtraceur May 25 '22 at 17:42
  • Of course, there is a decent argument to be made that Python code is actually better on balance in the cases that warrant multiple inheritance by just naming parent classes directly instead of using `super()` - you can only really reliably use `super()` to call methods *with arguments* if every class in the MRO has the same signature for that method, and any time the signatures are incompatible, it's just *necessary* to invoke them by name. (There is no way to call superclass' methods in a way that passes all arguments through but calls each superclass' method with just its own arguments.) – mtraceur May 25 '22 at 18:11

2 Answers2

76

You are creating partials on the function, not the method. functools.partial() objects are not descriptors, they will not themselves add the self argument and cannot act as methods themselves. You can only wrap bound methods or functions, they don't work at all with unbound methods. This is documented:

partial objects are like function objects in that they are callable, weak referencable, and can have attributes. There are some important differences. For instance, the __name__ and __doc__ attributes are not created automatically. Also, partial objects defined in classes behave like static methods and do not transform into bound methods during instance attribute look-up.

Use propertys instead; these are descriptors:

class RGB(object):
    def __init__(self, red, blue, green):
        super(RGB, self).__init__()
        self._red = red
        self._blue = blue
        self._green = green

    def _color(self, type):
        return getattr(self, type)

    @property
    def red(self): return self._color('_red')
    @property
    def blue(self): return self._color('_blue')
    @property
    def green(self): return self._color('_green')

As of Python 3.4, you can use the new functools.partialmethod() object here; it'll do the right thing when bound to an instance:

class RGB(object):
    def __init__(self, red, blue, green):
        super(RGB, self).__init__()
        self._red = red
        self._blue = blue
        self._green = green

    def _color(self, type):
        return getattr(self, type)

    red = functools.partialmethod(_color, type='_red')
    blue = functools.partialmethod(_color, type='_blue')
    green = functools.partialmethod(_color, type='_green')

but these'd have to be called, whilst the property objects can be used as simple attributes.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 5
    What about `self.red = functools.partial(RGB._color, self, 'red')` in `__init__`? It is Python2 compatible too. – dashesy Apr 04 '16 at 19:35
  • 8
    @dashesy: sure, but that puts those objects on each instance (a memory cost), also making it harder for a subclass to replace them. – Martijn Pieters Apr 04 '16 at 20:31
  • It seems to work if I create a partial from the `_color()` method from an instance of `RGB`? – Victor Cui Nov 28 '22 at 20:55
  • @VictorCui: then you created a partial for a *bound* method, and it'll remain bound to the specific instance. That's not the same thing as creating a partialmethod that then re-binds `self` to the current instance. – Martijn Pieters Nov 28 '22 at 21:44
  • @MartijnPieters is `functools.partialmethod()` creating partial methods that re-binds `self` to the current instance like your code example? – Victor Cui Nov 29 '22 at 01:49
  • 1
    @VictorCui exactly – Martijn Pieters Nov 29 '22 at 07:49
6

The issue with partialmethod is that it is not compatible with inspect.signature, functools.wraps,...

Weirdly enough, if you re-implement functools.partial yourself using the partial documentation implementation example, it will work:

# Implementation from:
# https://docs.python.org/3/library/functools.html#functools.partial
def partial(func, /, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = {**keywords, **fkeywords}
        return func(*args, *fargs, **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc
class RGB(object):
    def __init__(self, red, blue, green):
        super(RGB, self).__init__()
        self._red = red
        self._blue = blue
        self._green = green

    def _color(self, type):
        return getattr(self, type)

    red = partial(_color, type='_red')
    blue = partial(_color, type='_blue')
    green = partial(_color, type='_green')

rgb = RGB(100, 192, 240)
print(rgb.red())  # Print red

The reason is that newfunc is a true function which implement the descriptor protocol with newfunc.__get__. While type(functools.partial) is a custom class with __call__ overwritten. Class won't add the self parameter automatically.

Conchylicultor
  • 4,631
  • 2
  • 37
  • 40
  • lmao nice find, that means functools.partial kinda breaks the zen of python lol. if it just respected ducktyping it would all work fine anyways – Dubslow May 03 '23 at 17:45