Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

python string format suppress/silent keyerror/indexerror [duplicate]

Is there a way to use python string.format such that no exception is thrown when an index is missing, instead an empty string is inserted.

result = "i am an {error} example string {error2}".format(hello=2,error2="success")

here,result should be :

"i am an   example string success"

Right now, python throws a keyerror and stops formatting. Is it possible to change this behavior ?

Thanks

Edit:

There exists Template.safe_substitute (even that leaves the pattern intact instead of inserting an empty string) , but couldn't something similar for string.format

The desired behavior would be similar to string substitution in php.

class Formatter(string.Formatter):
  def get_value(self,key,args,kwargs):
    try:
        if hasattr(key,"__mod__"):
            return args[key]
        else:
            return kwargs[key]
    except:
        return ""

This seems to provide the desired behavior.

like image 366
Code freak Avatar asked Aug 21 '10 04:08

Code freak


2 Answers

str.format() doesn't expect a mapping object. Try this:

from collections import defaultdict

d = defaultdict(str)
d['error2'] = "success"
s = "i am an {0[error]} example string {0[error2]}"
print s.format(d)

You make a defaultdict with a str() factory that returns "". Then you make one key for the defaultdict. In the format string, you access keys of the first object passed. This has the advantage of allowing you to pass other keys and values, as long as your defaultdict is the first argument to format().

Also, see http://bugs.python.org/issue6081

like image 148
Fabian Fagerholm Avatar answered Oct 01 '22 10:10

Fabian Fagerholm


The official solution (Python 3 Docs) for strings in format mappings is to subclass the dict class and to define the magic-method __missing__(). This method is called whenever a key is missing, and what it returns is used for the string formatting instead:

class format_dict(dict):
    def __missing__(self, key):
        return "..."

d = format_dict({"foo": "name"})

print("My %(foo)s is %(bar)s" % d) # "My name is ..."

print("My {foo} is {bar}".format(**d)) # "My name is ..."

Edit: the second print() works in Python 3.5.3, but it does not in e.g. 3.7.2: KeyError: 'bar' is raised and I couldn't find a way to catch it.

After some experiments, I found a difference in Python's behavior. In v3.5.3, the calls are __getitem__(self, "foo") which succeeds and __getitem__(self, "bar") which can not find the key "bar", therefore it calls __missing__(self, "bar") to handle the missing key without throwing a KeyError. In v3.7.2, __getattribute__(self, "keys") is called internally. The built-in keys() method is used to return an iterator over the keys, which yields "foo", __getitem__("foo") succeeds, then the iterator is exhausted. For {bar} from the format string there is no key "bar". __getitem__() and hence __missing_() are not called to handle the situation. Instead, the KeyError is thrown. I don't know how one could catch it, if at all.

In Python 3.2+ you should use format_map() instead (also see Python Bug Tracker - Issue 6081):

from collections import defaultdict
    
d = defaultdict(lambda: "...")
d.update({"foo": "name"})

print("My {foo} is {bar}".format_map(d)) # "My name is ..."

If you want to keep the placeholders, you can do:

class Default(dict):
    def __missing__(self, key): 
        return key.join("{}")
    
d = Default({"foo": "name"})

print("My {foo} is {bar}".format_map(d)) # "My name is {bar}"

As you can see, format_map() does call __missing__().

The following appears to be the most compatible solution as it also works in older Python versions including 2.x (I tested v2.7.15):

class Default(dict):
    def __missing__(self, key):
        return key.join("{}")

d = Default({"foo": "name"})

import string
print(string.Formatter().vformat("My {foo} is {bar}", (), d)) # "My name is {bar}"

To keep placeholders as-is including the format spec (e.g. {bar:<15}) the Formatter needs to be subclassed:

import string

class Unformatted:
    def __init__(self, key):
        self.key = key
    def __format__(self, format_spec):
        return "{{{}{}}}".format(self.key, ":" + format_spec if format_spec else "")

class Formatter(string.Formatter):
    def get_value(self, key, args, kwargs):
        if isinstance(key, int):
            try:
                return args[key]
            except IndexError:
                return Unformatted(key)
        else:
            try:
                return kwargs[key]
            except KeyError:
                return Unformatted(key)


f = Formatter()
s1 = f.vformat("My {0} {1} {foo:<10} is {bar:<15}!", ["real"], {"foo": "name"})
s2 = f.vformat(s1, [None, "actual"], {"bar":"Geraldine"})
print(s1) # "My real {1} name       is {bar:<15}!"
print(s2) # "My real actual name       is Geraldine      !"

Note that the placeholder indices are not changed ({1} remains in the string without a {0}), and in order to substitute {1} you need to pass an array with any odd first element and what you want to substitute the remaining placeholder with as second element (e.g. [None, "actual"]).

You can also call the format() method with positional and named arguments:

s1 = f.format("My {0} {1} {foo:<10} is {bar:<15}!", "real", foo="name")
s2 = f.format(s1, None, "actual", bar="Geraldine")
like image 41
CodeManX Avatar answered Oct 01 '22 11:10

CodeManX