30.2.6. get处理字典数据

假设我们要给一家三明治店设计菜单,所以想先确定大家喜欢吃哪些类型的面包。我们定义一个字典,把每种款式的名字和它当前的得票数关联起来。

counters = {
    'pumpernickel': 2,
    'sourdough': 1,
}

如果要记录新的一票。首先要判断对应的键在不在字典里。如果不在,那就把这个键的票数设成0,然后增加所得票数。这需要两次访问这个键,第一次是为了判断它是否在字典里,第二次为了用它来获取对应的值,而且还要做一次赋值。 下面我们用if语句来实现该逻辑。

In [6]: counters = {
   ...:     'pumpernickel': 2,
   ...:     'sourdough': 1,
   ...: }

In [7]: key = 'wheat'
   ...:
   ...: if key in counters:
   ...:     count = counters[key]
   ...: else:
   ...:     count = 0
   ...:
   ...: counters[key] = count + 1
   ...:
   ...:

In [8]: counters
Out[8]: {'pumpernickel': 2, 'sourdough': 1, 'wheat': 1}

这有个办法也能实现相同的功能,就是利用KeyError异常。如果程序抛出了这个异常,那说明要获取的键不在字典里。 这个写法比刚才的简单,因为只需要访问一次键名就可以了。

In [9]: key = 'brioche'
   ...:
   ...: try:
   ...:     count = counters[key]
   ...: except KeyError:
   ...:     count = 0
   ...:
   ...: counters[key] = count + 1
   ...:
   ...:

In [10]: counters
Out[10]: {'pumpernickel': 2, 'sourdough': 1, 'wheat': 1, 'brioche': 1}

获取字典中存在的键,或给字典中不存在的键指定默认值,这两种操作非常常见。 Python的内置字典dict提供了get方法,可以指定键不存在时返回的默认值。 这种写法也只需要在查询键值时访问一次键名,然后做一次赋值操作,但要比刚才那种通过KeyError实现的方案简单得多。

In [11]: count = counters.get(key, 0)
    ...: counters[key] = count + 1
    ...:
    ...:
In [12]: counters
Out[12]: {'pumpernickel': 2, 'sourdough': 1, 'wheat': 1, 'brioche': 2}

对于通过in表达式与KeyError实现的那两种方案来说,确实可以通过各种技巧来简化代码,但不管怎样简化,都无法完全消除重复赋值。所以,优先考虑用get方法来实现,因为in方案与KeyError方案无论如何读比它复杂。

if key not in counters:
    counters[key] = 0
counters[key] += 1

if key in counters:
    counters[key] += 1
else:
    counters[key] = 1

try:
    counters[key] += 1
except KeyError:
    counters[key] = 1

如果字典里保存的数据比较复杂,比如列表,那该怎么办?例如,这次不仅要记录每种面包得的得票数,而且要记录投票的人。那可以像下面这样,把面包的名称(key)跟一份列表关联起来,而那份列表指的就是喜欢该面包的人。

In [16]: votes = {
    ...:     'baguette': ['Bob', 'Alice'],
    ...:     'ciabatta': ['Coco', 'Deb'],
    ...: }
    ...:
    ...: key = 'brioche'
    ...: who = 'Elmer'
    ...:
    ...: if key in votes:
    ...:     names = votes[key]
    ...: else:
    ...:     votes[key] = names = []
    ...:
    ...: names.append(who)
    ...:
    ...:

In [17]: names
Out[17]: ['Elmer']

In [18]: votes
Out[18]:
{'baguette': ['Bob', 'Alice'],
 'ciabatta': ['Coco', 'Deb'],
 'brioche': ['Elmer']}

votes[key] = names = []既可以把空白列表赋给names变量,又可以把这份列表与key相关联,这两项操作,只需要一行语句即可表达出来。 把空白列表(默认值)插入字典后,不需要再用另一条赋值语句给其中的某个元素赋值,一维可以直接在指向这份列表的names变量上调用append方法把投票人的名字添加进去。

还可以利用KeyError异常来实现。

In [19]: key = 'rye'
    ...: who = 'Felix'
    ...:
    ...: try:
    ...:     names = votes[key]
    ...: except KeyError:
    ...:     votes[key] = names = []
    ...:
    ...: names.append(who)
    ...:
    ...:

In [20]:

In [20]: votes
Out[20]:
{'baguette': ['Bob', 'Alice'],
 'ciabatta': ['Coco', 'Deb'],
 'brioche': ['Elmer'],
 'rye': ['Felix']}

同样,这个列子也能通过get方法改写。这样的话,如果键存在,只需要访问一次键名;如果不存在,那么还要在if块中用键名key作为下标赋一次值。

In [21]: key = 'wheat'
    ...: who = 'Gertrude'
    ...:
    ...: names = votes.get(key)
    ...: if names is None:
    ...:     votes[key] = names = []
    ...:
    ...: names.append(who)
    ...:
    ...:

In [22]:

In [22]: votes
Out[22]:
{'baguette': ['Bob', 'Alice'],
 'ciabatta': ['Coco', 'Deb'],
 'brioche': ['Elmer'],
 'rye': ['Felix'],
 'wheat': ['Gertrude']}

这个方案中,无论votes.get(key)的结果是不是None,都要把这个结果赋给names变量,只不过在结果为None的时候,还需要在if块中做一些处理。这种逻辑用赋值表达式,

if (names := votes.get(key)) is None:
    votes[key] = names = []
names.append(who)

dict类型提供了setdefault方法,能够继续简化代码。

key = 'cornbread'
who = 'Kirk'

names = votes.setdefault(key, [])
names.append(who)

在字典里面没有这个键时,setdefault方法会把默认值直接放到字典里,而不是先给它做副本,然后把副本放到字典中。

In [23]: data = {}
    ...:
    ...: key = 'foo'
    ...: value = []
    ...: data.setdefault(key, value)
    ...: print('Before:', data)
    ...: value.append('hello')
    ...: print('After: ', data)
    ...:
    ...:
Before: {'foo': []}
After:  {'foo': ['hello']}

这意味着每次调用setdefault时都要构造一个新的默认值出来。这可能产生较大的性能开销。 回到之前那个只记录票数而不记录投票人的例子。那个例子为什么不用setdefault改写呢?比如,可以这样写:

In [24]: key = 'dutch crunch'
    ...:
    ...: count = counters.setdefault(key, 0)
    ...: counters[key] = count + 1
    ...:
    ...:

In [25]:

In [25]:

In [25]: counters
Out[25]:
{'pumpernickel': 2,
 'sourdough': 1,
 'wheat': 1,
 'brioche': 3,
 'dutch crunch': 1}

这样写的问题是,根本就没必要调用setdefault,因为不管字典里有没有这个键,我们都要递增它所对应的值。

count = counters.get(key, 0)
counters[key] = count + 1

无论字典里有没有这个键,之前那种get方案只需要一次访问操作与一次赋值操作即可(如上代码,访问key,不存在即返回0,第二行赋值一次。),而目前的setdefault方案(在字典没有键的情况下)需要一次访问操作与两次赋值操作。

只有在少数几种情况下用setdefault处理缺失的键才是最简短的方式,例如:与键相关的默认值构造起来开销很低且可以变化,而且不用担心异常问题。在这种特殊的场合,可以用这个setdefault方案取代get方案。即便如此,一般也应该优先考虑用defaultdict取代dict。

在Python中实现真正的Switch-Case语句

以下是使用字典来模拟开关案例构造的代码

def xswitch(x):
    return xswitch._system_dict.get(x, None)

xswitch._system_dict = {'files': 10, 'folders': 5, 'devices': 2}

print(xswitch('default'))
print(xswitch('devices'))

"""
None
2
"""

要点:

有四种办法可以处理键不在字典中的情况:in表达式、KeyError异常、get方法与setdefault方法。

如果跟键相关联的值是像计数器这样的基本类型,那么get方法就是最好的方案;

如果是那种构造起来开销比较大,或是容易出异常的类型,那么可以把这个方法与赋值表达式结合起来使用。

即使看上去最应该使用setdefault方案,也不一定要真的使用setdefault方案,而是可以考虑用defaultdict取代普通的dict。