2

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.

2 Answers2

3

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))
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
1

Functions get their implicit self argument by being descriptors: the lookup x.f constructs and returns a method object that remembers x so as to supply it to f. functools.partial(...) does not return a descriptor, so it doesn't get that special treatment. (It's actually a class, so it "returns" an instance of itself.)

Davis Herring
  • 36,443
  • 4
  • 48
  • 76
  • 1
    Not going to downvote because this istrue , but you need to have a solution to have an answer. Right now you just have a very insightful comment at best. – Mad Physicist Sep 24 '17 at 04:13
  • @MadPhysicist: There are no question marks in the, er, question, but I wrote this to address "I'm trying to understand why the partial version doesn't work.". – Davis Herring Sep 24 '17 at 04:15
  • You are absolutely right. +1. The OP has such a strong implication in their question and I am so tired, I read something very different from the actual question in my mind. Thanks for the catch. – Mad Physicist Sep 24 '17 at 04:21
  • Thanks for the useful documentation. – Diego F. Rodríguez V. Sep 28 '17 at 01:09