Python 3.5选择在比较字典键时使用哪些键。

61

构建一个如下的字典时:

dict = { True: 'yes', 1: 'No'}

当我在交互式的Python解释器中运行它时,字典会以这种方式呈现:

dict = {True: 'No'}

我明白 True1 值相等是因为类型强制转换,比较数字类型时,缩小的类型会扩展到另一种类型(布尔型是整数的子类)。所以从文档中我理解当我们输入 True == 1 时,Python 将 True 转换为 1 并进行比较。

但我不理解为什么选择 True 作为键而不是 1

我有什么漏掉的吗?


9
只有最后一个键值对起作用。由于 1 == True,第二个键覆盖了第一个键的值。然而,在此操作中没有必要重新编写键,只需要更新值即可。 - Martijn Pieters
13
大家停止回答这个问题!提问者想知道为什么即使将值分配为1,True仍然保留为键。这个问题是关于“为什么选择值的机制不同于选择键”的问题。 - Tadhg McDonald-Jensen
7
为什么要重写这个键?键是不可变的,True1 的哈希值和比较结果相等,因此它们是相等的,True 是先存储的,所以就使用了 True。如果你调换字典中键值对的顺序,将键 1 放在前面,那么结果就是 1。当添加一个新项时,如果已经在字典中出现过(因为 True1 是一样的),那么就会覆盖之前的值,这是标准的字典行为。但是它不应该覆盖,那没有意义。 - dwanderson
10
@Muposat:这里所做的事情并没有什么“愚蠢”的,让我们保持建设性,并且不要暗示程序员有任何问题。 - Martijn Pieters
3
Python 字典是无序的 key: value 结构,但在构建时,它们会按顺序处理。@dwanderson 这就是你想说的吗?@Muposat 这是伴随着另一个问题而提出的一个问题,那个问题是为什么 True1 被映射为 True,我回答了那个问题,因此一些程序员面临了这个问题。我想知道为什么 Python 3 选择了 True 值。唯一愚蠢的问题是我们不问的问题。感谢大家帮助我理解。 - Ikra_5
显示剩余9条评论
6个回答

43

字典在Python中是用哈希表实现的,在添加键/值时有两个重要的概念:哈希和相等性。

为了插入特定的键/值,Python首先计算键的哈希值。这个哈希值用于确定表格的行,Python应该首先尝试将键/值放入其中。

如果哈希表的行为空,则可以插入新的键/值对到字典中,填充空行。

但是,如果该行已经有东西,则Python需要测试键的相等性。如果键相等(使用==),则它们被视为相同的键,Python只需要更新该行上相应的值。

(如果键不相等,则Python会查看表格中的其他行,直到找到键或到达空行,但这与本问题无关。)


当你写{True: 'yes', 1: 'No'}时,你告诉Python创建一个新的字典,然后用两个键/值对填充它。这些按从左到右的顺序处理:True: 'yes'然后是1: 'No'

我们有hash(True)等于1。键True在哈希表的第1行处,字符串'yes'是其值。

对于下一个键/值对,Python发现hash(1)也是1,因此查看表格的第1行。那里已经有东西了,所以现在Python检查键是否相等。我们有1 == True,因此1被视为与True相同的键,因此其对应的值更改为字符串'No'

这将产生一个只有一个条目的字典:{True: 'No'}


如果您想查看CPython 3.5的内部结构来了解在Python层面下创建字典的情况,这里提供更多详细信息。

  • The Python code {True: 'yes', 1: 'No'} is parsed into tokens and given to the compiler. Given the syntax, Python knows that a dictionary must be created using the values inside the braces. Byte code to load the four values onto the virtual machine's stack (LOAD_CONST) and then build the dictionary (BUILD_MAP) is queued up.

  • The four constant values are pushed onto the top of the stack in the order that they're seen:

    'No'
    1
    'yes'
    True
    
  • The opcode BUILD_MAP is then called with the argument 2 (Python counted two key/value pairs). This opcode is responsible for actually creating dictionary from the items on the stack. It looks like this:

    TARGET(BUILD_MAP) {
        int i;
        PyObject *map = _PyDict_NewPresized((Py_ssize_t)oparg);
        if (map == NULL)
            goto error;
        for (i = oparg; i > 0; i--) {
            int err;
            PyObject *key = PEEK(2*i);
            PyObject *value = PEEK(2*i - 1);
            err = PyDict_SetItem(map, key, value);
            if (err != 0) {
                Py_DECREF(map);
                goto error;
            }
        }
    
        while (oparg--) {
            Py_DECREF(POP());
            Py_DECREF(POP());
        }
        PUSH(map);
        DISPATCH();
    }
    

以下是三个关键步骤:

  1. 使用 _PyDict_NewPresized 创建一个空的哈希表。对于只有几个项的小字典(例如此处为2项),需要一个有8行的表格。

  2. 进入 for 循环,从2开始(本例中)向下计数到0。 PEEK(n) 是指向栈第n个项目的宏。因此,在循环的第一次迭代中,我们将获得

PyObject *key = PEEK(2*2);       /* item 4 down the stack */  
PyObject *value = PEEK(2*2 - 1); /* item 3 down the stack */

这意味着在第一次循环中,*key将是True*value将是'yes'。在第二次循环中,它将是1'No'

在每个循环中都会调用PyDict_SetItem函数将当前的*key*value放入字典中。这与写dictionary[key] = value时调用的函数相同。它计算键的哈希值以确定在哈希表中首先查找的位置,然后(如果需要)将键与该行上任何现有键进行比较(如上所述)。


@IljaEverilä:不,字典不会考虑类型。只测试相等性。在Python 2中,strunicode对象也是如此,因为u'foo' == 'foo',所以{u'foo': 42, 'foo': 81}的结果是{u'foo': 81} - Martijn Pieters
@MartijnPieters 哈希然后比较相等。 - Tadhg McDonald-Jensen
@MartijnPieters 仔细阅读文档后,你是正确的。 - Ilja Everilä
@TadhgMcDonald-Jensen:测试相等的对象必须产生相同的哈希值。然后使用哈希值在哈希表中查找正确的插槽,但如果对象最初就相等,那么这是一个确定的结果。 - Martijn Pieters
2
@MartijnPieters float("nan") == float("nan") -> False 但是 hash(float("nan")) == hash(float("nan")) -> True - Tadhg McDonald-Jensen
显示剩余2条评论

31

基本前提是 - True1 具有相同的哈希值,并且彼此相等 - 这就是为什么它们不能成为哈希表中的不同键(技术上具有相同哈希值但不相等的对象可能会出现哈希冲突从而降低性能)。

>>> True == 1
True
>>> hash(1)
1
>>> hash(True)
1

现在,让我们考虑一下字节码:

import dis
dis.dis("Dic = { True: 'yes', 1: 'No'}")

这将打印:

      0 LOAD_CONST               0 (True)
      3 LOAD_CONST               1 ('yes')
      6 LOAD_CONST               2 (1)
      9 LOAD_CONST               3 ('No')
     12 BUILD_MAP                2
     15 STORE_NAME               0 (Dic)
     18 LOAD_CONST               4 (None)
     21 RETURN_VALUE

基本上,字典字面量被令牌化为键和值,并推送到堆栈中。然后,BUILD_MAP 2 将两个 (key, value) 对转换为字典。

在堆栈上数据的顺序(似乎由字典字面量中键的顺序确定)和BUILD_MAP 的实现细节决定了结果字典的键和值。

看起来键值分配是按照字典字面量中定义的顺序进行的。 赋值行为与 d[key] = value 操作相同,因此它基本上是这样的:

  • 如果键key不在字典中 (通过相等性):添加key到字典中
  • value存储在key

给定{True:'yes',1:'No'}

  1. {} 开始
  2. 添加(True, 'yes')

    1. True 不在字典中 -> (True, hash(True)) == (True, 1) 是字典中的新键
    2. 更新键等于1的值为'yes'
  3. 添加(1, 'no')

    1. 1 在字典中 (1 == True) -> 在字典中不需要新键
    2. 更新键等于1 (True)的值为'no'

结果:{True: 'No'}

正如我所评论的,我不知道这是否被 Python 规范所保证,还是只是 CPython 实现定义的行为,在其他解释器实现中可能会有所不同。


21

True1是不同的对象,但它们的值相同:

>>> True is 1
False
>>> True == 1
True

这就像是两个字符串可能拥有相同的值,但存储在不同的内存位置一样:

>>> x = str(12345)
>>> y = str(12345)
>>> x == y 
True
>>> x is y
False

首先向字典中添加一个项目;当添加另一个项目时,该值已经存在作为一个键。因此键被更新,键值是唯一的。

>>> {x: 1, y: 2}
{"12345": 2}

9
如果字典中已经存在该键,则不会覆盖键,只会替换对应的值。
我认为编写x = {True:"a", 1:"b"}类似于:
x = {}
x[True] = "a"
x[1] = "b"

当它到达x [1] =“b”时,键True已经存在于字典中,所以为什么要更改它呢?为什么不只覆盖相关的值。


唯一有影响的情况是当对象被认为是相等的(并产生相同的哈希值),但行为不同。我除了True vs 1False vs 0之外想不到其他情况,就像你看到的那样,除了在打印字典时(d [1] <==> d [True])没有任何区别。 - Tadhg McDonald-Jensen
想象一下,如果每次执行 d['a'] = 4 都会更改用于键的 'a' 对象,那还有什么意义呢?虽然这不是 Python 规范,但我认为默认情况下应该采用这种实现方式,因为我不明白为什么要以其他方式实现它... - Tadhg McDonald-Jensen
我不想用其他方式实现它,我只是想理解,这种行为没有问题。明白字典是按照 @Rogalski 解释的方式构建的非常有趣。 - Ikra_5
无论是什么,我不建议在实际代码中依赖这种行为。 - Tadhg McDonald-Jensen
我同意,乍一看很混乱,容易出错。 - Ikra_5
显示剩余3条评论

6
订购似乎是问题的原因。 代码示例:
>>> d = {True: 'true', 1: 'one'}
>>> d
{True: 'one'}
>>> d = {1: 'one', True: 'true'}
>>> d
{1: 'true'}

3
当然,这就是重点。第一个关键字设置了一个值,然后下一个关键字与第一个关键字相等,并为该关键字设置一个新值。 - Martijn Pieters
@MartijnPieters 当然,顺序不是问题的重点...正如其他许多答案所展示的那样。 - rll
6
交换顺序可以说明发生了什么。对于相等的键,最后一个键值对获胜,但只使用其值。对于不熟悉布尔值是int子类的人来说,这些键看起来并不相等也很重要,但"排序"也是解释的重要部分。 - Martijn Pieters

3

这是一个模棱两可的陈述。

逻辑: d = { True: 'no', 1: 'yes'}

当Python评估表达式时,它会顺序执行,因此与以下代码等效。

d = dict() d[True] = 'no' d[1] = 'yes'

常量True是键,但它的值为1,因此您只是为键设置了两次值。


你是从我的答案复制的还是我们同时想到了同样的推理? - Tadhg McDonald-Jensen
我在浴缸里用手机打字,当你发布答案时(我在手机上打字很慢)。发现重复时已经发布了,但保留了它,因为我们用稍微不同的措辞解释了它,也许有助于理解。 - Jason Lee Eaton
2
这比你需要的更具体。这仍然主要回答了关于为什么True和1对应于字典中相同位置的另一个问题。(无论如何,我很高兴你没有抄袭) - Tadhg McDonald-Jensen

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