[Python-ideas] new format spec for iterable types

Andrew Barnert abarnert at yahoo.com
Thu Sep 10 00:39:45 CEST 2015

On Sep 9, 2015, at 15:03, Wolfgang Maier <wolfgang.maier at biologie.uni-freiburg.de> wrote:
>> On 09.09.2015 23:28, Andrew Barnert via Python-ideas wrote:
>>> On Sep 9, 2015, at 06:41, Wolfgang Maier <wolfgang.maier at biologie.uni-freiburg.de> wrote:
>>> 3)
>>> Finally, the alternative idea of having the new functionality handled by a new !converter, like:
>>> "List: {0!j:,}".format([1.2, 3.4, 5.6])
>>> I considered this idea before posting the original proposal, but, in addition to requiring a change to str.format (which would need to recognize the new token), this approach would need either:
>>> - a new special method (e.g., __join__) to be implemented for every type that should support it, which is worse than for my original proposal or
>>> - the str.format method must react directly to the converter flag, which is then no different to the above solution just that it uses !j instead of *. Personally, I find the * syntax more readable, plus, the !j syntax would then suggest that this is a regular converter (calling a special method of the object) when, in fact, it is not.
>>> Please correct me, if I misunderstood something about this alternative proposal.
>> But the format method already _does_ react directly to the conversion flag. As the docs say, the "type coercion" (call to str, repr, or ascii) happens before formatting, and then the __format__ method is called on the result. A new !j would be a "regular converter"; it just calls a new join function (which returns something whose __format__ method then does the right thing) instead of the str, repr, or ascii functions.
> Ah, I see! Thanks for correcting me here. Somehow, I had the mental picture that the format converters would call the object's __str__ and __repr__ methods directly (and so you'd need an additional __join__ method for the new converter), but that's not the case then.
>> And random's custom converter idea would work similarly, except that presumably his !join would specify a function registered to handle the "join" conversion in some way rather than being hardcoded to a builtin.
> How would such a registration work (sorry, I haven't had the time to search the list for his previous mention of this idea)? A new builtin certainly won't fly.

I believe he posted a more detailed version of the idea on one of the other spinoff threads from the f-string thread, but I don't have a link. But there are lots of possibilities, and if you want to start bikeshedding, it doesn't matter that much what his original color was. For example, here's a complete proposal:

    class MyJoiner:
        def __init__(self, value):
            self.value = value
        def __format__(self, spec):
            return spec.join(map(str, self.value))
    string.register_converter('join', MyJoiner)

That last line adds it to some global table (maybe string._converters, or maybe it's not exposed at the Python level at all; whatever).

In str.format, instead of reading a single character after a !, it reads until colon or end of field; if that's more than a single character, it looks it up in the global table and calls the registered callable. So, in this case, "{spam!join:-}"
would call MyJoiner(spam).__format__('-').

Any more complexity can be added to MyJoiner pretty easily, so this small extension to str.format seems sufficient for anything you might want. For example, if you want a three-part format spec that includes the join string, a format spec to pass to each element, and a format spec to apply to the whole thing:

    def __format__(self, spec):
        joinstr, _, spec = spec.partition(':')
        espec, _, jspec = spec.partition(':')
        bits = (format(e, espec) for e in self.value)
        joined = joinstr.join(bits)
        return format(joined, jspec)

Or maybe it would be better to have a standard way to do multi-part format specs--maybe even passing arguments to a converter rather than cramming them in the spec--but this seems simple and flexible enough.

It might also be worth having multiple converters called in a chain, but I can't think of a use case for that, so I'll ignore it.

Most converters will be classes that just store the constructor argument and use it in __format__, so it seems tedious to repeat that boilerplate for 90% of them, but that's easy to fix with a decorator:

    def simple_converter(func):
        class Converter:
            def __init__(self, value):
                self.value = value
            def __format__(self, spec):
                return func(self.value, spec)

Meanwhile, maybe you want the register function to be a decorator:

    def register_converter(name):
        def decorator(func):
            _global_converter_table[name] = func
            return func
        return decorator

So now, the original example becomes:

    def my_joiner(values, joinstr):
        return joinstr.join(map(str, values))

More information about the Python-ideas mailing list