接上一篇内容 Python函数式编程之自定义函数get_in 。这次我们来编写函数式风格的修改字典/列表值的通用函数。
我们先来假设一种情况,你有一个嵌套的动态字典,想要更新里面某个键对应的值,我们用代码来说明
现在有一个变量data
他的值是一个两层的字典,如果我要更新 b
的值到 2
常规的做法是这样的
如果我有一个更加深的字典
1
|
data = {'a': {'b': {'c': {'d': 1}}}}
|
更新值的写法
1
|
data['a']['b']['c']['d''] = 2
|
这里有一个情况发生了,当 data
是一个动态数组,你不确定中间的某个键是否存在的时候就会有报错
假如这个数据是你的系统里面其他团队开发的一个微服务,报错的信息是int
对象是不可读取索引的,这会让人很奇怪,2这个int对象明明是一个值,而不是用来查找索引数据的。
这时候程序的健壮性是比较差的,要想让程序保证健壮,我们可以使用 if-else
来判断每个键是否存在,就像这样
这里的三个if
看起来一点都不够优雅,如果其他地方也有同样的写法,只是键的顺序和名字不一样而已呢。能不能封装一个通用的函数来处理类似的情况呢?
update_in
Python支持面向过程、面向对象、函数式等多范式编程。我们可以使用函数式编程思想来达到这一点。先来看一个简单一点的 update_in
函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
from typing import Dict, List, T
def update_in(data: Dict, ks: List, value: T):
"""Updates a value in a nested associative structure.
Args:
data (Dict): nested associative structure
ks (List): sequence of keys
value (Any): new value
"""
if len(ks) == 0:
return
cursor = data
for k in ks[:-1]:
if k not in cursor:
cursor[k] = {}
elif not isinstance(cursor[k], dict):
cursor[k] = {}
cursor = cursor[k]
cursor[ks[-1]] = value
|
这里有一个类型 T
,这是Python内置的任意类型,在 typing
模块可以看到定义 T = TypeVar('T')
对update_in函数解释一下
首先判断键序列是否为空,为空的话什么也不做。然后使用一个游标cursor
来指定当前取得是字典所在的位置,然后不停的循环下去,拿到除最后一个键之外的值。在最后把值赋给找到的最后一个键所指定的值。
来看几个例子
来看几个例子,了解函数的作用。假设我有一个空字典 data = dict()
1
|
update_in(data, ['a'], 1)
|
执行完成update_in
函数后,data的值为 {'a': 1}
1
|
update_in(data, ['a', 'b'], 2)
|
执行完后,data的值为 {'a': {'b': 1}}
1
|
update_in(data, ['a', 'b', 'c', 'd'], 3)
|
执行完后,data的值为 {'a': {'b': {'c': {'d': 3}}}}
我们封装了一个通用函数,可以对任意的字典,任意的键序列进行值的修改。
再更进一步,如果不是单纯的给某个键序列进行赋值,而是要取出那个值,然后再进行一些变换呢?比如,我要取出 ['a', 'b']
对应的值,进行一个平方计算,或者取个对数呢?我们增加一个参数,变成update_in_fn
函数
update_in_fn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
from typing import Dict, List, Callable, T
def update_in_fn(data: Dict, ks: List, fn: Callable, default: T):
"""Updates a value in a nested associative structure.
Args:
data (Dict): nested associative structure
ks (List): sequence of keys
fn (Callable): take the old value and return the new value
default (Any): default value
Examples:
>>> a = {}
>>> update_in_fn(a, ['a', 'b'], lambda x: x + 1, default=0)
>>> assert a == {'a': {'b': 0}}
"""
if len(ks) == 0:
return
cursor = data
for k in ks[:-1]:
if k not in cursor:
cursor[k] = {}
elif not isinstance(cursor[k], dict):
cursor[k] = {}
cursor = cursor[k]
key = ks[-1]
if key not in cursor:
cursor[key] = default
else:
cursor[key] = fn(cursor[key])
|
我们对这个函数加以说明:
第一个参数是被修改的字典数据
第二个参数是键序列
第三个参数是一个可调用函数,传入一个参数,返回一个结果
第三个参数表明如果字典里面没有找到对应的值,使用default填充键序列指定的那个位置的值
还是使用几个例子来说明用法及结果
1
2
3
|
data = dict()
update_in_fn(data, ['a'], lambda x: x**2, default=2)
# data: {'a': 2}
|
在一个空字典上执行函数调用时,会填充默认的值 default
1
2
3
|
data = {'a': 3}
update_in_fn(data, ['a'], lambda x: x**2, default=0)
# data: {'a': 9}
|
对键 'a'
指定的值进行平方计算,计算后的结果为 9
1
2
3
|
data = {'a': {'b': 4}}
update_in_fn(data, ['a', 'b'], lambda x: x**2, default=0)
# data: {'a': {'b': 16}}
|
对嵌套字典里面的值进行更新,4的平方16
结论
借助Python强大的函数式编程范式,我们可以编写非常通用的函数及模块,上面的两个函数代码还有优化的空间,比如用户自定义的字典类型,可以像get_in函数一样支持数组。