解构绑定字典内容

160

我正在尝试“解构”一个字典,并将其键与变量名称关联起来,以使用其值。就像这样:

params = {'a':1,'b':2}
a,b = params.values()

但是由于字典是无序的,不能保证params.values()按照(a, b)的顺序返回值。有没有一种好的方法可以做到这一点?


3
懒惰?也许吧……但当然我只是为了举例展示最简单的情况。理想情况下,我想做的是这样: for x in params.items: eval('%s = %f' % x) 但我猜eval()不允许赋值。 - hatmatrix
25
我很确定大多数ES6(JavaScript)用户喜欢新的对象解构语法:let {a, b} = params。它可以提高可读性,完全符合你想要讨论的任何禅意。 - Andy
32
@Andy,我喜欢JS中的对象解构。这是一种干净、简单和易读的方式,可以从字典中提取一些键。我来到这里希望能在Python中找到类似的东西。 - Rotareti
4
我也喜欢ES6对象解构,但我担心它在Python中不能工作,原因与ES6的Map对象不支持解构相同。在ES6 Map和Python dict中,键不仅仅是字符串。此外,虽然我喜欢ES6对象解构的“pluck”风格,但赋值样式并不简单。这里发生了什么?let {a: waffles} = params。即使你习惯了它,也需要几秒钟才能弄清楚。 - John Christopher Jones
1
@naught101 在特定情况下有用,但也会带来一些不好的后果。对于用户而言,在Python中,任何对象都可以提供自己的str/repr方法。甚至可能会诱人为稍微复杂的键对象(例如,命名元组)提供这种方法,以便更轻松地进行JSON序列化。现在你会想为什么无法按名称解构键。此外,为什么这适用于项目而不适用于属性?许多库更喜欢属性。对于实现者而言,这个ES6功能混淆了符号(可绑定名称)和字符串:在JavaScript中是合理的,但Python有更丰富的想法。此外,这看起来很丑。 - John Christopher Jones
显示剩余5条评论
19个回答

275
from operator import itemgetter

params = {'a': 1, 'b': 2}

a, b = itemgetter('a', 'b')(params)

可以使用内置库,而不是复杂的lambda函数或字典推导式。


22
这可能是可接受的答案,因为这是最符合Python风格的方法。你甚至可以扩展答案使用相同标准库模块中的attrgetter,它适用于对象属性(obj.a)。这与JavaScript有很大不同,JavaScript中obj.a === obj["a"] - John Christopher Jones
3
如果字典中不存在该键,则会引发KeyError异常。 - Tasawar Hussain
11
但是你在解构语句中现在打了两次a和b。 - Otto
4
@JohnChristopherJones:这对我来说似乎不太自然,使用现有的东西并不意味着它会变得易懂。我怀疑很多人在真正的代码中不会立即理解。另一方面,像建议的a,b = [d[k] for k in('a','b')]这样的形式更加自然/可读(这种形式更为常见)。这仍然是一个有趣的答案,但不是最直接的解决方案。 - cglacet
@cglacet 我认为这取决于你需要读取/实现多少次。如果您经常选择相同的3个键,则使用get_id = itemgetter(KEYS)之类的东西,然后稍后使用serial,code,ts = get_id(document)更简单。不可否认,您必须熟悉高阶函数,但Python通常非常适合它们。例如,请参见装饰器@contextmanager的文档。 - John Christopher Jones
4
谢天谢地它抛出了“KeyError”。它还应该做什么? - Danon

44

为什么没有人发布最简单的方法?

params = {'a':1,'b':2}

a, b = params['a'], params['b']

6
哈哈,是的,b, a = params["b"], params["a"] - Glenn Mohammad

39
一种比Jochen的建议更少重复的方法是使用一个辅助函数。这样可以灵活地按任意顺序列出变量名,并且只解构字典中的一部分内容。
pluck = lambda dict, *args: (dict.get(arg, -1) for arg in args)

things = {'blah': 'bleh', 'foo': 'bar'}
foo, blah = pluck(things, 'foo', 'blah')

另外,你可以通过对键进行排序并获取对应的值,而不是使用joaquin的OrderedDict。唯一的注意事项是你需要按字母顺序指定变量名,并在字典中解构所有内容。
sorted_vals = lambda dict: (t[1] for t in sorted(dict.items()))

things = {'foo': 'bar', 'blah': 'bleh'}
blah, foo = sorted_vals(things)

7
虽然这只是小小的挑剔,但如果您要将一个 lambda 分配给变量,最好使用正常的 def 函数语法。 - Arthur Tacca
点赞了,你不能像 JS 那样做吗,它会是 const {a,b} = {a: 1, b: 2}。 - PirateApp
3
你已经在标准库中实现了 operator 中的 itemgetter。 :) - John Christopher Jones

27

Python只能“解构”序列,而不能解构字典。因此,为了编写您想要的内容,您需要将所需条目映射到适当的序列。就我个人而言,我找到的最接近的匹配是(不是很引人注目):

a,b = [d[k] for k in ('a','b')]

这也适用于生成器:

a,b = (d[k] for k in ('a','b'))

这是一个完整的例子:

>>> d = dict(a=1,b=2,c=3)
>>> d
{'a': 1, 'c': 3, 'b': 2}
>>> a, b = [d[k] for k in ('a','b')]
>>> a
1
>>> b
2
>>> a, b = (d[k] for k in ('a','b'))
>>> a
1
>>> b
2

23
这是另一种类似于JS中的解构赋值的方式来实现它:
params = {'b': 2, 'a': 1}
a, b, rest = (lambda a, b, **rest: (a, b, rest))(**params)

我们所做的是将params字典解包成键值对(使用**,就像Jochen's answer中所示),然后我们将这些值在lambda签名中进行分配,根据键名进行赋值 - 这里还有一个额外的好处 - 我们还可以获得一个不在lambda签名中的任何内容的字典,所以如果你有:
params = {'b': 2, 'a': 1, 'c': 3}
a, b, rest = (lambda a, b, **rest: (a, b, rest))(**params)

应用lambda后,rest变量将包含: {'c': 3}
对于从字典中省略不需要的键非常有用。
如果你不想保留rest,可以这样做:
a, b = (lambda a, b, **_): (a, b))(**params)

1
有趣的是,我觉得把它放在一个函数里会更好。你可能会多次使用它,这样你也可以给它命名。(当我说函数时,我的意思不是lambda函数)。 - cglacet
你无法将它移动到一个函数中,因为它依赖于你在赋值的左侧和lambda参数中使用相同的标识符。 - Rodrigo Rodrigues
这样做的好处是你在字符串中不需要提及变量名。不足之处在于你需要三次提及每个变量名... - undefined

16

也许你真的想做这样的事情?

def some_func(a, b):
  print a,b

params = {'a':1,'b':2}

some_func(**params) # equiv to some_func(a=1, b=2)

谢谢,但不是那个...我正在函数内部进行解构。 - hatmatrix
@hatmatrix,看起来你可以创建一个函数,使你的代码更加简洁。 - Karolius
1
在我看来,这个答案完全符合 OP 明确要求的,并且适当地使用了语言特性。唯一的妥协是将控制流程移动到一个函数中。 - darw
没有使用过 JavaScript 解构的人会认为这是一个合理的替代品。它不仅使用了四行代码(而不是"直接"实现只需两行),还在命名空间中引入了一个新函数,你以后可能不会再使用它,每个 "种类" 的解构都需要一个新函数,并且它仍然要求你在每次调用时按照相应的顺序提供参数,这意味着它没有提供任何拼写错误的安全性。与其使用这种方式,你最好直接写 a = data['a'], b = data['b'] - Richard Rast
1
@RichardRast 不需要相应的顺序。正如评论所说,**params 等同于 Python 的 命名 参数传递,其整个目的是不关心顺序。some_func(a=1, b=2) == some_func(b=2, a=1) - Beni Cherniavsky-Paskin
这种风格的一个限制是它不能紧凑地嵌套。Python 3 移除了元组参数,而且那些参数本来就没有命名。所以,如果你有一个字典的列表的字典,你最终写出来的代码将不会像构造{ "key": [ { "inner_key": var } ] }那样,而是需要一层一层地解构——外部函数def f1(key):,然后在其中[ item ] = key,然后内部函数def f2(inner_key): ... - Beni Cherniavsky-Paskin

11

如果您担心使用locals字典所涉及的问题,并且更喜欢遵循原始策略,那么Python 2.7和3.1中的有序字典collections.OrderedDicts可以让您按照最初插入的顺序检索字典项。


7
目前在 Python 3.5 及以上版本中,所有的字典都是有序的。但这并不是一个“保证”,这意味着它可能会改变。 - Charles Merriam
10
从3.6版本及以上,它是有保证的。 - naught101
@Zachary822的答案虽然需要更多的代码,但它有一个优点,就是你不必知道(或记住)字典内容的顺序,而且如果它发生变化,它也不会出错——所以我认为了解这两种方法都是值得的。 - martineau

10

使用Python 3.10,您可以执行以下操作:

d = {"a": 1, "b": 2}

match d:
    case {"a": a, "b": b}:
        print(f"A is {a} and b is {b}")

但它会增加两个额外的缩进级别,并且你仍然需要重复键名。


这种方法还有一些细节(适用于数据类对象而不是字典),这里有更多信息:值得注意的是,在match...case语句之后,变量ab仍然存在。 - undefined

8

滥用导入系统

from ... import语句允许我们拆解和绑定一个对象的属性名。当然,它只适用于sys.modules字典中的对象,因此可以使用以下技巧:

import sys, types

mydict = {'a':1,'b':2}

sys.modules["mydict"] = types.SimpleNamespace(**mydict)

from mydict import a, b

一个更为严肃的黑客技巧是编写上下文管理器来加载和卸载模块:

with obj_as_module(mydict, "mydict_module"):
    from mydict_module import a, b

通过将模块的 __getattr__ 方法直接指向字典的 __getitem__ 方法,上下文管理器还可以避免使用 SimpleNamespace(**mydict)。参见此答案以获取实现和一些扩展思路。
还可以通过临时替换整个 sys.modules 字典为感兴趣的字典,并在不使用 from 的情况下进行 import a, b

1
如果这些黑科技还不够满足你的口味,你也可以使用模块级别的__getattr__ :) - decorator-factory
1
如果在使用线程时暂时替换整个sys.modules是非常冒险的。如果一个函数可以从多个线程中调用,那么在其中使用任何这些方法也是有风险的;在顶层使用更为安全。但是,非常聪明的观察——确实,import...from语法具有解构的特性 :-) - Beni Cherniavsky-Paskin

7

警告1:如文档所述,这个函数不能保证在所有Python实现中都能正常工作:

CPython实现细节:此函数依赖于解释器中的Python堆栈帧支持,而这并不保证存在于所有的Python实现中。如果在一个没有Python堆栈帧支持的实现中运行,则此函数返回None。

警告2:虽然这个函数确实可以使代码变得更短,但它可能与Python的显式原则相矛盾。此外,它也没有解决John Christopher Jones在注释中指出的问题,尽管您可以创建一个类似的函数来处理属性而不是键。这只是一个演示,表明如果您真的想要这样做,您是可以做到的!

def destructure(dict_):
    if not isinstance(dict_, dict):
        raise TypeError(f"{dict_} is not a dict")
    # the parent frame will contain the information about
    # the current line
    parent_frame = inspect.currentframe().f_back

    # so we extract that line (by default the code context
    # only contains the current line)
    (line,) = inspect.getframeinfo(parent_frame).code_context

    # "hello, key = destructure(my_dict)"
    # -> ("hello, key ", "=", " destructure(my_dict)")
    lvalues, _equals, _rvalue = line.strip().partition("=")

    # -> ["hello", "key"]
    keys = [s.strip() for s in lvalues.split(",") if s.strip()]

    if missing := [key for key in keys if key not in dict_]:
        raise KeyError(*missing)

    for key in keys:
        yield dict_[key]

In [5]: my_dict = {"hello": "world", "123": "456", "key": "value"}                                                                                                           

In [6]: hello, key = destructure(my_dict)                                                                                                                                    

In [7]: hello                                                                                                                                                                
Out[7]: 'world'

In [8]: key                                                                                                                                                                  
Out[8]: 'value'

这个解决方案允许你像在JavaScript中一样选择一些键而不是全部。它也对用户提供的字典安全。

这是一个Java程序员会做的事情(为了简单的方法而有大量的代码行:D)。有更好的解决方案。 - Tony
@Tony 如果有更好的方法,您可以添加编辑或自己的答案 :) - decorator-factory

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接