A collection of computer, gaming and general nerdy things.

Monday, September 29, 2014

str.format for fun and profit

One common thing I've used a few times is using str.format to create formatting metapatterns -- essentially a format pattern that becomes a format pattern when formatted. I'm not necessarily advocating for using this everywhere, but it's handy sometimes.

Let me provide two examples:

Arbitrary Padding

Part of a Python learning group gives homework, one of the "assignments" was to pad an input to an arbitrary amount with an arbitrary character. The "cannonical" example given was to pad a given number to five characters with zeroes. Formatting ahoy!

Keep in mind how closures work: When we call padder we're actually creating an "instance" of pad where the pattern is bound at the time of creation. The way this metapattern works is that {{}} will actually output {} when passed through formatting, so assuming defaults, the metapattern ends up looking like: {:0>5}.

In [8]:
def padder(padwith=0, direction='>', length=5):
    pattern = "{{:{}{}{}}}".format(padwith, direction, length)
    def pad(input):
        return pattern.format(input)
    return pad

p = padder()
g = padder(padwith='*', direction='^', length='7')

print(p(11))
print(g('g'))
00011
***g***

Generic repr

A more useful example is creating a generic __repr__ Mixin:

In [4]:
class ReprMixin:
    '''Provides a string representible form for objects.'''

    # Most models will have both an id and name fields
    # This can be overwritten if needed
    __repr_fields__ = ['id', 'name']

    def __repr__(self):
        fields =  {f:getattr(self, f, '<BLANK>') for f in self.__repr_fields__}
        # constructs a dictionary compatible pattern for formatting
        # {{{0}}} becomes {id} for example
        pattern = ['{0}={{{0}}}'.format(f) for f in self.__repr_fields__]
        pattern = ' '.join(pattern)
        pattern = pattern.format(**fields)
        return '<{} {}>'.format(self.__class__.__name__, pattern)

I've been using this in OpenWebAmp's (nee FlaskAmp) models for easy recognition of model instances in the command line shell. Basically, all of my main objects have a id and name fields, but some don't and I need more information from others. Creating a generic __repr__ method isn't very intuitive given how much attributes in objects can vary. Essentially what this does is look for __repr_fields__ in an object (using sane defaults if not present) and then iterates through that creating a list of patterns like ['id={id}', 'name={name}'] and then joins them into a single string, then formats it and then formats that into another pattern. What pops out the other end is something like this: <Artist id=1 name=The Foo Bars>.

But you can also override __repr_fields__ in a class to pull more or different information out of it.

In [7]:
class Artist(ReprMixin):
    def __init__(self, id, name):
        self.id = id
        self.name = name

class Member(ReprMixin):
    def __init__(self, id, name):
        self.id = id
        self.name = name

class Tag(ReprMixin):
    def __init__(self, id, name):
        self.id = id
        self.name = name

class MemberTaggedArtist(ReprMixin):
    __repr_fields__ = ['id', 'member', 'tag', 'artist']
    # pretend there's relational bindings here
    def __init__(self, id, member, tag, artist):
        self.id = id
        self.artist = artist
        self.member = member
        self.tag = tag

mta = MemberTaggedArtist(
    id=4, 
    member=Member(id=1, name='anr'), 
    tag=Tag(id=1, name='Rock'), 
    artist=Artist(id=1, name='The Foo Bars')
    )

repr(mta)
Out[7]:
'<MemberTaggedArtist id=4 member=<Member id=1 name=anr> tag=<Tag id=1 name=Rock> artist=<Artist id=1 name=The Foo Bars>>'

So metapatterns. They make your patterns a little unfun to read, but they're really useful. Don't stick 'em everywhere because they're awful to look at and terrible to debug if you screw something up.

No comments:

Post a Comment