[Python-Dev] Alternative Implementation for PEP 292: Simple String Substitutions

Raymond Hettinger raymond.hettinger at verizon.net
Thu Aug 26 23:38:56 CEST 2004


Before it's too late and the API gets frozen, I would like to propose an
alternate implementation for PEP292 that exposes two functions instead
of two classes.

Current way:    print Template('Turn $direction') %
dict(direction='right')

Proposed:       print dollarsub('Turn $direction',
dict(direction='right'))
 or:            print dollarsub('Turn $direction', direction='right')


My main issue with the current implementation is that we get no leverage
from using a class instead of a function.  Though the API is fairly
simple either way, it is easier to learn and document functions than
classes.  We gain nothing from instantiation -- the underlying data
remains immutable and no additional state is attached.  The only new
behavior is the ability to apply the mod operator.  Why not do it in one
step.

I had thought a possible advantage of classes was that they could be
usefully subclassed.  However, a little dickering around showed that to
do anything remotely interesting (beyond changing the pattern alphabet)
you have to rewrite everything by overriding both the method and the
pattern.  Subclassing gained you nothing, but added a little bit of
complexity. A couple of simple exercises show this clearly: write a
subclass using a different escape character or one using dotted
identifiers for attribute lookup in the local namespace -- either way
subclasses provide no help and only get in the way.

One negative effect of the class implementation is that it inherits from
unicode and always returns a unicode result even if all of the inputs
(mapping values and template) are regular strings.  With a function
implementation, that can be avoided (returning unicode only if one of
the inputs is unicode).

The function approach also makes it possible to have keyword arguments
(see the example above) as well as a mapping.  This isn't a big win, but
it is nice to have and reads well in code that is looping over multiple
substitutions (mailmerge style):
  
   for girl in littleblackbook:
      print dollarsub(loveletter, name=girl[0].title(),
favoritesong=girl[3])

Another minor advantage for a function is that it is easier to lookup in
the reference.  If a reader sees the % operator being applied and looks
it up in the reference, it is going to steer them in the wrong
direction.  This is doubly true if the Template instantiation is remote
from the operator application.


Summary for functions:
* is more appropriate when there is no state
* no unnecessary instantiation
* can be applied in a single step
* a little easier to learn/use/document
* doesn't force result to unicode
* allows keyword arguments
* easy to find in the docs


Raymond




----------- Sample Implementation -------------

def dollarsub(template, mapping=None, **kwds):
    """A function for supporting $-substitutions."""
    typ = type(template)
    if mapping is None:
        mapping = kwds
    def convert(mo):
        escaped, named, braced, bogus = mo.groups()  
        if escaped is not None:
            return '$'
        if bogus is not None:
            raise ValueError('Invalid placeholder at index %d' %
                             mo.start('bogus'))
        val = mapping[named or braced]
        return typ(val)
    return _pattern.sub(convert, template)

def safedollarsub(template, mapping=None, **kwds):
    """A function for $-substitutions.

    This function is 'safe' in the sense that you will never get
KeyErrors if
    there are placeholders missing from the interpolation dictionary.
In that
    case, you will get the original placeholder in the value string.
    """
    typ = type(template)
    if mapping is None:
        mapping = kwds    
    def convert(mo):
        escaped, named, braced, bogus = mo.groups()
        if escaped is not None:
            return '$'
        if bogus is not None:
            raise ValueError('Invalid placeholder at index %d' %
                             mo.start('bogus'))
        if named is not None:
            try:
                return typ(mapping[named])
            except KeyError:
                return '$' + named
        try:
            return typ(mapping[braced])
        except KeyError:
            return '${' + braced + '}'
    return _pattern.sub(convert, template)











More information about the Python-Dev mailing list