有序字典推导式

67

我能扩展Python中字典解析的语法以适用于其他字典,比如collections模块中的OrderedDict或继承自dict的自定义类型吗?

显然仅重新绑定dict名称是不行的,因为字典解析和字面常量仍然会给你一个普通的字典。

>>> from collections import OrderedDict
>>> olddict, dict = dict, OrderedDict
>>> {i: i*i for i in range(3)}.__class__
<type 'dict'>

那么,如果可能的话,我该怎么做呢?如果只在CPython中有效也可以。对于语法,我想我会尝试使用类似于O{k: v}前缀的形式,就像我们在r'various' u'string' b'objects'上所做的那样。

注意:当然,我们也可以使用生成器表达式,但我更感兴趣的是看看Python在语法方面有多容易被黑客攻击。


“可以扩展语法”是指通过制作CPython或PyPy的自定义版本,还是从语言内部进行扩展? - abarnert
这正是我有点不确定的地方。CPython扩展是否可以用于这种复杂的事情,还是我们要进入构建自己的Python领域? - wim
1
你可以使用以下代码扩展字典推导式的 {} 语法:https://dev59.com/Gmsz5IYBdhLWcg3weHoA#7880276,并且**如果你*真的*需要,你需要向 DictDisplayTransformer 类添加 visit_DictComp() 方法**。 - Alex Bitek
1
只需转向Python3.6,其中字典是有序的 - gerrit
3个回答

95

抱歉,无法实现。字典字面值和字典推导映射到内置的dict类型,在C级别上硬编码方式实现。这是无法被覆盖的。

不过您可以使用以下替代方法:

OrderedDict((i, i * i) for i in range(3))

附录:从 Python 3.6 开始,所有的 Python 字典都是有序的。自 3.7 版本起,这甚至成为了语言规范的一部分。如果你正在使用这些版本的 Python,则无需使用 OrderedDict:dict 推导式会正常运行(TM)。


1
FYI,这个语法是一个生成器表达式,它将元组传递给OrderedDict()构造函数。为了演示,list((i, i * i) for i in range(3))会得到[(0, 0), (1, 1), (2, 4)] - wisbucky

32

没有直接的方法可以从语言内部更改Python的语法。无论是使用字典推导式(或普通显示),它总是会创建一个 dict,你无法改变这个事实。如果你正在使用CPython,则使用特殊的字节码直接生成字典,最终调用PyDict API函数和/或该API使用的底层函数。如果你正在使用PyPy,则这些字节码代替上面所述的 API 函数和对象,基于一个编译和优化的Python dict对象和RPython dict对象来实现。如此类推。

有一种间接的方法可以做到这一点,但你可能不会喜欢它。如果你阅读导入系统的文档,你会发现是导入器搜索缓存编译代码或调用编译器,编译器调用解析器等等。在Python 3.3+中,几乎所有这些链条中的东西都是用纯Python编写的,或者有一个替代的纯Python实现,这意味着你可以分叉代码并做自己的事情。这包括使用你自己的PyParsing代码构建AST来解析源代码,或将字典推导式AST节点编译成你自己的自定义字节码而不是默认值,或后处理字节码等等。

在许多情况下,导入钩子就足够了;如果不行,你可以编写自定义的查找器和加载器。

如果你还没有使用Python 3.3或更高版本,我强烈建议在尝试这些内容之前进行迁移。在旧版本中,这很难,并且文档也不是很好,你最终将花费10倍的精力学习某些东西,而无论何时你迁移,它都将过时。

无论如何,如果您对此方法感兴趣,您可能想看看MacroPy。您可以从中借鉴一些代码,更重要的是,了解如何使用一些在文档中没有好的示例的功能。

或者,如果你愿意接受不太酷的东西,你可以使用MacroPy构建一个"odict comprehension macro"并使用它。(注意,MacroPy目前仅适用于Python 2.7,而不是3.x。) 你不能完全得到o{…},但你可以得到,比如说,od[{…}],这也不错。下载od.py, realmain.py, 和 main.py,然后运行python main.py查看它的工作原理。关键在于这段代码,它将DictionaryComp AST转换为等效的基于键值TupleGeneratorExpr,并将其包装在对collections.OrderedDictCall中:

def od(tree, **kw):
    pair = ast.Tuple(elts=[tree.key, tree.value])
    gx = ast.GeneratorExp(elt=pair, generators=tree.generators)
    odict = ast.Attribute(value=ast.Name(id='collections'), 
                          attr='OrderedDict')
    call = ast.Call(func=odict, args=[gx], keywords=[])
    return call

另一种选择是修改Python解释器。我建议您在首次尝试时放弃使用O{...}语法想法,而只需使普通字典推导编译为有序字典。好消息是,您不需要真正改变语法(这是超级复杂的...),只需要更改以下任意一项:
  • 字典推导编译成的字节码
  • 解释器运行这些字节码的方式
  • PyDict类型的实现
坏消息是,虽然所有这些都比更改语法容易得多,但它们都不能从扩展模块中完成。(好吧,您可以通过执行基本上与纯Python相同的操作来执行第一个操作...并且您可以通过钩入.so/.dll/.dylib来补丁自己的函数来执行其中任何一个,但那就像是对Python进行黑客攻击加上额外的工作,即在运行时挂钩。)
如果您想要研究CPython源码,那么您需要的代码在Python/compile.cPython/ceval.cObjects/dictobject.c中,而dev guide会告诉您如何找到您需要的一切。但是,考虑到它主要是用Python(而不是C)编写的,您可能希望考虑研究PyPy源码
作为旁注,即使所有内容都在Python语言级别完成,您的尝试也不会成功。 olddict,dict = dict,OrderedDict 在模块的全局范围内创建了一个名为dict 的绑定,这个绑定遮蔽了内置的名称,但没有替换它。您可以替换内置的东西(好吧,Python不能保证这一点,但是对于我尝试过的每个实现/版本,都有特定于实现/版本的事情-发生工作),但您所做的并不是这样做的方式。

我对参与Python C api很感兴趣。C api 3和2有很大的不同吗?(我的日常工作是2,永远不会转到3) - user1971598
2
@EdgarAroutiounian:C API比语言本身更加保守——longunicode 改为 intstr,但是 C 类型仍然是 PyLongPyUnicode。几乎所有的差异都与 2.x 中不存在的新功能相关。(如果你深入研究 CPython 本身,那么就会发现有更大的差异。但在大多数情况下——除了 Unicode 的内部存储之外——3.4 比 2.7 更简单,因此最好先掌握简单的方法。) - abarnert
1
@EdgarAroutiounian:无论如何,参与C API的最佳方式是构建一个简单的扩展,将一些C库包装起来,并以良好的方式将其暴露给Python。官方文档中的扩展和嵌入教程非常不错。您可能希望尝试使用ctypes/cffi和本地扩展(也许还有Cython)进行相同的包装,以真正了解从不同方面看待事物的方式。 - abarnert
有一种间接的方法可以做到,但你可能不会喜欢它。-- 我已经很喜欢了 :) - Inversus
先生,这是一个令人印象深刻的答案。 - Yonatan

16
稍微修改一下@Max Noel的答案,你可以使用列表推导式来创建有序字典(OrderedDict),以有序的方式创建(当然使用字典推导式是不可能的)。
>>> OrderedDict([(i, i * i) for i in range(5)])
OrderedDict([(0, 0), 
             (1, 1), 
             (2, 4), 
             (3, 9), 
             (4, 16)])

这似乎会给你与Max的答案相同的最终结果。这个方法有什么优点/区别吗? - user694733
1
@user694733 这表明您可以使用任意值来使用OrderedDict([(0, 2), (2, 5)]) - Quentin Pradet
1
@user694733 的问题以“注意:当然,我们可以使用生成器表达式,但我更感兴趣的是看看 Python 在语法方面有多容易被黑客攻击。” 结束。这个解决方案可以在没有生成器的情况下完成相同的事情。 - Alexander
我认为Python中的“生成器”是一个比较高级的概念,并不是每个使用Python的人都需要知道它。因此,如果目标受众不是语言专家,使用列表贡献是有意义的,这样可以更容易地阅读/维护脚本。 - balki

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