Advanced formatting with argument passing I want to customise how my objects are printed using the .format() function.

I have two problems to make that happen in clear and concise way. But since the second one stems from the first one I'm presenting both here.

Formatting array

Passing options to .format() in array seems to me to be awfully messy if I want to match default behaviour with __repr__:

import binascii


def format_data(data, formatstr):
    if 'x' in formatstr:
        return binascii.b2a_hex(data).decode('ascii')
    else:
        return repr(data)


class InsideA(object):
    def __repr__(self):
        return "InsideA()"

    def __format__(self, _):
        return "InsideA()"


class InsideB(object):
    def __init__(self, data):
        self.data = data

    def __repr__(self):
        data = format_data(self.data, 'x')
        return "InsideB(data={0})".format(data)

    def __format__(self, formatstr):
        data = format_data(self.data, formatstr)
        return "InsideB(data={0})".format(data)


if __name__ == "__main__":
    a = InsideA()
    b = InsideB(bytearray(b'test\xff\xff'))

    array = [a, b]

    print("what I want:")
    print(array)
    print("")

    print("doesn't work:")
    try:
        print("{0:x}".format(array))
    except Exception as e:  # catch just to continue execution
        print(e)
    print("")

    print("Leaves ' around elements:")
    print(["{0:x}".format(elem) for elem in array])
    print("")

    print("Works but seems overly complex:")
    print("[" + ", ".join("{0:x}".format(elem) for elem in array) + "]")


Produces:



What I want:


[InsideA(), InsideB(data=74657374ffff)]



Doesn't work:


non-empty format string passed to object.__format__



Leaves ' around elements:


['InsideA()', 'InsideB(data=74657374ffff)']



Works but seems overly complex:


[InsideA(), InsideB(data=74657374ffff)]



Formatting with non-uniform objects

The problem only gets compounded if the array elements don't always implement the format function, requiring use of try except clauses:



from __future__ import print_function
import binascii


def format_data(data, formatstr):
    if 'x' in formatstr:
        return binascii.b2a_hex(data).decode('ascii')
    else:
        return repr(data)


class InsideA(object):
    def __repr__(self):
        return "InsideA()"


class InsideB(object):
    def __init__(self, data):
        self.data = data

    def __repr__(self):
        data = format_data(self.data, 'x')
        return "InsideB(data={0})".format(data)

    def __format__(self, formatstr):
        data = format_data(self.data, formatstr)
        return "InsideB(data={0})".format(data)

def format_array(array, formatstr):
    if array is None:
        return "None"
    else:
        str_array = []
        for elem in array:
            try:
                str_array += ["{0:{1}}".format(elem, formatstr)]
            except (TypeError, ValueError):  # Py2 and Py3 compat
                str_array += [repr(elem)]
        return "[" + ", ".join(str_array) + "]"

if __name__ == "__main__":
    a = InsideA()
    b = InsideB(bytearray(b'test\xff\xff'))

    array = [a, b]

    print("Automatic:")
    print(array)
    print("")


    print("With format:")
    print(format_array(array, "x"))


Prints:



Automatic:


[InsideA(), InsideB(data=74657374ffff)]



With format:


[InsideA(), InsideB(data=74657374ffff)]



This code in format_array() seems to me to be too complex for what it does. Do I don't know of some syntax that makes this a breeze or is it the best Python can do?

(Yes, I know that I can implement __repr__ as just a call to format(), leaving that in for readability of example.)
list always uses repr on it's elements.
When you str or repr it.
And internally list is doing roughly the equivalent of what you are doing in format_array.

The only way to make this work is to use something like format_array,
or subclass list.
But they will both be doing the same thing, just depends on how much sugar you want.



Your code is nice, readable and follows PEP8.
As this is tending towards example code, it's not in it's native environment, it's hard to comment distinctively on it.

But format_data can be merged with the class.

class InsideB(object):
    def __init__(self, data):
        self.data = data

    def __repr__(self):
        data = self._data(True)
        return "InsideB(data={0})".format(data)

    def __format__(self, formatstr):
        data = self._data('x' in formatstr)
        return "InsideB(data={0})".format(data)

    def _data(self, as_hex):
        if as_hex:
            return binascii.b2a_hex(self.data).decode('ascii')
        else:
            return repr(self.data)


You can then add an option to use 'hex' or repr as the output type to the class.
With the option to reverting to the original after a use.
Obviously it depends on your usage.

class InsideB(object):
    def __init__(self, data, as_hex=False, revert_hex=False):
        self.data = data
        self._as_hex = as_hex
        self.output_type = as_hex
        self._revert_hex = revert_hex

    def _data(self, as_hex):
        if as_hex or self.output_type:
            data = binascii.b2a_hex(self.data).decode('ascii')
        else:
            data = repr(self.data)

        if self._revert_hex:
            self.output_type = self._as_hex

        return data


Then you would need to make a function that maps over all the items in the list,
changes output_type to what you want, and you'll be in the same position as you are now.
But with a bit more sugar.



If you choose to subclass list, and to use str in __str__,
then you'll get output which is just wrong.

&gt;&gt;&gt; print('{!s}, {!s}'.format('a', 'b'))
a, b
&gt;&gt;&gt; print('{!r}, {!r}'.format('a', 'b'))
'a', 'b'




No matter how I look at it you wont be able to change the output 'nicely'.
Unless you think something more robust to map(lambda i: i.output_type = True, array) is nice.

Bottom line you have to manually implement this, as repr is hard-coded.