Python Question

Can't use partial as __str__

I ran into this problem while trying to write a pretty print procedure for a program in which I use several named tuples containing floating point pairs.

from collections import namedtuple
Position = namedtuple('Position', 'x y')
Vector = namedtuple('Vector', 'x y')
Size = namedtuple('Size', 'width height')


I want to format the floating point numbers when printed because the result of:

import math
print(Position(math.pi, math.pi), Vector(math.pi, math.pi), Size(math.pi, math.pi))


Is too long:

Position(x=3.141592653589793, y=3.141592653589793) Vector(x=3.141592653589793, y=3.141592653589793) Size(width=3.141592653589793, height=3.141592653589793)


So I created a function to print the named tuples:

def pretty_float_pair(name, labels, obj):
"""
If labels = ('a', 'b') and object = (1.2345, 1.2345) returns:
'name(a=1.23, b=1.23)'
"""
return '{}({}={:.2f}, {}={:.2f})'.format(name, labels[0], obj[0], labels[1], obj[1])


The name and labels should be fixed for every type and only the obj argument varies so I thought I could use functools partial.

from functools import partial
Position.__str__ = partial(pretty_float_pair, 'Position', ('x', 'y'))
Vector.__str__ = partial(pretty_float_pair, 'Vector', ('x', 'y'))
Size.__str__ = partial(pretty_float_pair, 'Size', ('width', 'height'))
print(Position(math.pi, math.pi), Vector(math.pi, math.pi), Size(math.pi, math.pi))


But this throws a
TypeError: pretty_float_pair() missing 1 required positional argument: 'obj'.


Surprisingly if I use lambda to create the functions it works.

Position.__str__ = lambda x: pretty_float_pair('Position', ('x', 'y'), x)
Vector.__str__ = lambda x: pretty_float_pair('Vector', ('x', 'y'), x)
Size.__str__ = lambda x: pretty_float_pair('Size', ('width', 'height'), x)
print(Position(math.pi, math.pi), Vector(math.pi, math.pi), Size(math.pi, math.pi))


Prints what I wanted:

Position(x=3.14, y=3.14) Vector(x=3.14, y=3.14) Size(width=3.14, height=3.14)


I'm trying to understand why the partial version doesn't work.

Answer Source

functools.partial returns a non-descriptor callable, roughly equivalent to an unbound method. This means that it is not being passed a self parameter, which is consistent with the error you are seeing.

Since a lambda behaves just like a regular function defined with def, it is in fact a descriptor. The __get__ method of the lambda returns a bound version that passes in the instance as x.

To get a partial function that behaves more like a method, use functools.partialmethod instead. You will have to move obj to the beginning of your argument list so it can receive self when the method is bound.

Here is your example:

from functools import partialmethod

def pretty_float_pair(obj, name, labels):
    """
    If labels = ('a', 'b') and object = (1.2345, 1.2345), returns:
        name(a=1.23, b=1.23)
    """
    return '{}({}={:.2f}, {}={:.2f})'.format(name, labels[0], obj[0], labels[1], obj[1])

Position.__str__ = partialmethod(pretty_float_pair, 'Position', ('x', 'y'))
Vector.__str__ = partialmethod(pretty_float_pair, 'Vector', ('x', 'y'))
Size.__str__ = partialmethod(pretty_float_pair, 'Size', ('width', 'height'))

print(Position(math.pi, math.pi), Vector(math.pi, math.pi), Size(math.pi, math.pi))