Can I use a dynamic mapping to unpack keyword argu

2019-04-07 17:17发布

问题:

Long story short, I want to call format with arbitrarily named arguments, which will preform a lookup.

'{Thing1} and {other_thing}'.format(**my_mapping)

I've tried implementing my_mapping like this:

class Mapping(object):
  def __getitem__(self, key):
    return 'Proxied: %s' % key
my_mapping = Mapping()

Which works as expected when calling my_mapping['anything']. But when passed to format() as shown above I get:

TypeError: format() argument after ** must be a mapping, not Mapping

I tried subclassing dict instead of object, but now calling format() as shown raises KeyError. I even implemented __contains__ as return True, but still KeyError.

So it seems that ** is not just calling __getitem__ on the object passed in. Does anyone know how to get around this?

回答1:

In Python 2 you can do this using string.Formatter class.

>>> class Mapping(object):
...     def __getitem__(self, key):
...         return 'Proxied: %s' % key
...
>>> my_mapping = Mapping()
>>> from string import Formatter
>>> Formatter().vformat('{Thing1} and {other_thing}', (), my_mapping)
'Proxied: Thing1 and Proxied: other_thing'
>>>

vformat takes 3 args: the format string, sequence of positional fields and mapping of keyword fields. Since positional fields weren't needed, I used an empty tuple ().



回答2:

Python 3.2+:

'{Thing1} and {other_thing}'.format_map(my_mapping)


回答3:

This may be a bit of necromancy, but I recently came across this problem, and this SO question was the first result. I wasn't happy with using string.Formatter, and wanted it to Just Work (TM).

If you implement a keys() function for your class as well as __getitem__(), then **my_mapping will work.

I.e:

class Mapping(object):

  def __getitem__(self, key):
    return 'Proxied: %s' % key

  def keys(self):
    return proxy.keys()

where

>>> my_mapping = Mapping()
>>> my_mapping.keys()
['Thing1','other_thing',...,'Thing2']

will result in a successful mapping that will work with .format.

Apparently (though I haven't actually looked at the source for str.format), it appears to use keys() to get a list of keys, then map the identifiers given in the string to those keys, then use __getitem__() to retrieve the specified values.

Hope this helps.

EDIT:

If you are in @aaron-mcmillin's position, and the key set is large, then a possible approach is to not generate a full set of keys, but generate a smaller subset. This only works of course if you know you will only need to format a small subset.

I.e:

class Mapping(object):
  ...
  def keys(self):
    return ['Thing1','other_thing', 'Thing2']


回答4:

This is the best I could come up with:

If you have a custom mapping object that you want to pass to a func taking key-word arguments, then it must have a set of keys (which may be dynamically generated, but it must be a finite set), and it must be able to map those keys somehow. So, if you can assume that it will have an __iter__ to get the keys, and a __getitem__ that will succeed for each of those keys, e.g.:

class Me(object):
    def __init__(self):
        pass

    def __iter__(self):
        return iter(['a', 'b', 'c'])

    def __getitem__(self, key):
        return 12

Say the function is:

def myfunc(**kwargs):
    print kwargs, type(kwargs)

Then we can pass it along by making a dict:

m = Me()
myfunc(**dict((k, m[k]) for k in m))

Resulting in:

{'a': 12, 'c': 12, 'b': 12} <type 'dict'>

Apparently this must be the way it's done... even if you pass in an object derived from dict, the function will still have a dict for the kwargs:

class Me(dict): pass

m = Me()
print type(m) #prints <class '__Main__.Me'>

def myfunc(**kwargs):
    print type(kwargs)
myfunc(**m) #prints <type 'dict'>

Since it sounds like you wanted to do something like return a value based on what the key was, without having a particular set of keys in mind, it seems like you can't use the format function.