views:

98

answers:

3

Hello guys, I'm trying to do switch statement (with dictionary) and the answer needs to be a formatted string, so for example:

descriptions = {
   'player_joined_clan': "%(player)s joined clan %(clan)s." % {"player": token1, "clan": token2},
   #etc...
  }

Now, this would work if those both tokens were always defined, which is not the case. Plus I believe it is formatting all the strings in the dictionary, which is not needed, it should only format the one that will be needed. So, I came up with this very unusual and partially dumb solution using lambda

descriptions = {
   'player_joined_clan': lambda x: "%(player)s joined clan %(clan)s." % {"player": token1, "clan": token2},
  }

Which I can then call with descriptions["player_joined_clan"](0), it will work as expected but ehh, so ugly and unintuitive... I'm clearly missing something here.

Any tips appreciated, thanks in advance.

+1  A: 

I would think that you want the descriptions dictionary to contain only the format strings, e.g.:

descriptions = {
   'player_joined_clan': "%(player)s joined clan %(clan)s.",
   #etc...
  }

Then you'd have a function that received a description key and a dictionary of event-specific data, that would produce the formatted message, something like:

def getMessage( key, eventDataDict ):
  return descriptions[key] % eventDataDict

In fact, I think that the way you have written your example, token1 etc. would be evaluated at the time of the declaration of descriptions -- when I presume what you want is for the message to be formatted for different values of these variables at different times.

Dave Costa
That would be a nice solution. But the problem with eventDataDict is the following: I may or may not have from token1 to token4 data. Token1 may be a player, clan or whatever depending on the description keyword. So eventDataDict will be likely different for every description key, which puts us back on a switch. Did I make myself clear? Thanks!
Clash
If you already know the mapping per message, just put `%(tokenX)s` into your formatting string instead of `%(player)s`. It seems like that is the one place that knows the mapping of "token" to "spot in the string". (Possibly using `return descriptions[key] % locals()`, don't know where your data is coming from.)
dash-tom-bang
But I wanted the name to be pretty :(...The text is going to be translated, so putting %(token1)s will not be intuitive, thanks!
Clash
+1  A: 

I think what you want to do is add another layer that selects a formatter based on the presence or absence of the various dictionary keys.

So you could use something like

formatters = {
    set('player', 'team'): "{player} joined {team}".format,
    set('player'): "Hello {player}.".format,
    set('team'): "{team} FTW!".format,
    set(): "Something happened.".format}

to establish which format string will be used. Note that I'm using the new-style format strings that work with str.format rather than the old-style ones. They're recommended over the older template % data ones.

And then to get a formatting function you can do

fmt = descriptions[set(eventDataDict.keys())]

and then call

formatted_greeting = fmt(eventDataDict)

This is inferior to a case statement in that there's no default case; if you need that you could wrap the access of descriptions in a try ... except KeyError construct, probably all within a function called e.g. format_description. You might want to subclass dict and make this a method of that class, depending on how your code is structured.

intuited
+3  A: 

If I'm understanding correctly, I'd recommend a collections.defaultdict. This isn't really what I'd call a "switch" statement, but I think the end result is close to what you're looking for.

I can best explain with full code, data, and application. Obviously, the key line is the defualtdict line.

>>> import collections
>>> 
>>> descriptions = {
...     'player_joined_clan' : '%(player)s joined clan %(clan)s',
...     'player_left' : '%(player)s left',
...     'player_hit_player' : '%(player)s (of %(clan)s) hit %(player2)s (of %(clan2)s)',
...     }
>>> 
>>> data = [
...     {'player': 'PlayerA'},
...     {'player': 'PlayerB', 'clan' : 'ClanB'},
...     {'clan' : 'ClanC'},
...     {'clan' : 'ClanDA', 'player2': 'PlayerDB'},
...     ]
>>> 
>>> for item in data:
...     print item
...     item = collections.defaultdict(lambda : '"<unknown>"', **item)
...     for key in descriptions:
...         print '  %s: %s' % (key, descriptions[key] % item)
...     print
... 
{'player': 'PlayerA'}
  player_joined_clan: PlayerA joined clan "<unknown>"
  player_left: PlayerA left
  player_hit_player: PlayerA (of "<unknown>") hit "<unknown>" (of "<unknown>")

{'clan': 'ClanB', 'player': 'PlayerB'}
  player_joined_clan: PlayerB joined clan ClanB
  player_left: PlayerB left
  player_hit_player: PlayerB (of ClanB) hit "<unknown>" (of "<unknown>")

{'clan': 'ClanC'}
  player_joined_clan: "<unknown>" joined clan ClanC
  player_left: "<unknown>" left
  player_hit_player: "<unknown>" (of ClanC) hit "<unknown>" (of "<unknown>")

{'clan': 'ClanDA', 'player2': 'PlayerDB'}
  player_joined_clan: "<unknown>" joined clan ClanDA
  player_left: "<unknown>" left
  player_hit_player: "<unknown>" (of ClanDA) hit PlayerDB (of "<unknown>")

Or, if you want it more customizable than simply a lambda with one string, you can define your own defaultdict class, such as:

class my_defaultdict(collections.defaultdict):
    def __missing__(self, key):
        return '<unknown %s>' % key

change the line to use your class instead of the default one:

#item = collections.defaultdict(lambda : '"<unknown>"', **item)
item = my_defaultdict(**item)

and, voila, the output:

{'player': 'PlayerA'}
  player_joined_clan: PlayerA joined clan <unknown clan>
  player_left: PlayerA left
  player_hit_player: PlayerA (of <unknown clan>) hit <unknown player2> (of <unknown clan2>)

{'clan': 'ClanB', 'player': 'PlayerB'}
  player_joined_clan: PlayerB joined clan ClanB
  player_left: PlayerB left
  player_hit_player: PlayerB (of ClanB) hit <unknown player2> (of <unknown clan2>)

{'clan': 'ClanC'}
  player_joined_clan: <unknown player> joined clan ClanC
  player_left: <unknown player> left
  player_hit_player: <unknown player> (of ClanC) hit <unknown player2> (of <unknown clan2>)

{'clan': 'ClanDA', 'player2': 'PlayerDB'}
  player_joined_clan: <unknown player> joined clan ClanDA
  player_left: <unknown player> left
  player_hit_player: <unknown player> (of ClanDA) hit PlayerDB (of <unknown clan2>)

See the documentation for collections.defaultdict for more examples.

Edit:
I forgot that this __missing__ functionality was added to the standard dict class in python 2.5. So an even simpler approach doesn't even involve collections.defaultdict-- just subclass dict:

class my_defaultdict(dict):
    def __missing__(self, key):
        return '<unknown %s>' % key
ma3
+1. I was trying to remember how I implemented *missing key* handling the last time that I needed to do it. I finally looked at my code and I did use `defaultdict` and `__missing__`.
D.Shawley