【问题标题】:(Yet Another) List Aliasing Conundrum(又一个)列表别名难题
【发布时间】:2012-02-18 07:59:19
【问题描述】:

我以为我已经弄清楚了整个列表别名,但后来我遇到了这个:

    l = [1, 2, 3, 4]
    for i in l:
        i = 0
    print(l)

导致:

    [1, 2, 3, 4]

到目前为止一切顺利。

但是,当我尝试这样做时:

    l = [[1, 2], [3, 4], [5, 6]]
    for i in l:
        i[0] = 0

我明白了

    [[0, 2], [0, 4], [0, 5]]

这是为什么?

这与混叠的深度有关吗?

【问题讨论】:

  • python中没有“别名”之类的东西
  • @user102008:如果我定义一个函数f,然后将某个变量设置为等于f(即x = f)。然后我可以使用x() 调用f。这对我来说听起来确实像是别名。如果我理解正确,使用赋值x = f 使x 引用f,它不会创建f 的实例?
  • 正确。 f 是在 def 时间创建的。 x = f 只是将另一个名字指向 f。
  • 但这不是别名,这只是复制参考。事后重新绑定f 不会影响x
  • 奇怪的是,“又一个”通常是伪装成“同一个”。

标签: python


【解决方案1】:

第一个重新绑定名称。重新绑定名称只会更改本地名称。第二个改变对象。改变一个对象会在它被引用的任何地方改变它(因为它总是同一个对象)。

【讨论】:

    【解决方案2】:

    i = 0i[0] = 0 非常不同。

    Ignacio 简洁而正确地解释了原因。因此,我将尝试用更简单的语言来解释这里实际发生的情况。

    在第一种情况下,i 只是一个指向某个对象(列表中的成员之一)的标签。 i = 0 更改对某个其他对象的引用,因此 i 现在引用整数 0。该列表未修改,因为您从未要求修改l[0]l 的任何元素,您只修改了i

    在第二种情况下,i 也只是一个指向列表中成员之一的名称。那部分没有什么不同。但是,i[0] 现在正在调用列表成员之一的.__getitem__(0)。同样,i[0] = 'other' 就像在做i.__setitem__(0, 'other')。它不仅仅是将i 指向不同的对象,就像常规的赋值语句那样,实际上它正在改变对象i

    一种简单的思考方式是,Python 中的名称始终只是对象的标签。范围或命名空间就像将名称映射到对象的字典。

    【讨论】:

    • 那么,如果我理解正确的话,在这种情况下,i 是否会根据它是否被编入索引而被区别对待?
    • @Joel:i 永远不会被索引。对象i 绑定到 已编入索引。
    • i 只是命名空间中的一个名称。 i = 0 将该名称 i 分配给不同的对象,而 i[0] = 0 没有。
    • @Joel:并不是i 被区别对待,而是i[0] = 0i = 0 是不同的操作。第一个只是使i 成为0 的名称。第二个在 i 当前是引用 0 的名称中的第 0 个插槽。如果i 绑定到一个数字,i[0] = 0 将具有相同的效果,只是数字不包含任何“槽”,因此该操作会引发错误。
    【解决方案3】:
    for i in l:
    

    这意味着“每次循环,i 应该是l 的下一个元素的名称”。

    i = 0
    

    这意味着“i 将不再是它当前名称的名称,而是开始成为整数对象 0 的名称”。

    i[0] = 0
    

    这意味着“i 命名的事物的第零个元素应替换为 0”。 (你不能真的说“i[0] 将不再是……的名字”,因为它不是名字。)

    【讨论】:

      【解决方案4】:

      分配总是只是在 Python 中重新绑定名称,这意味着您最终会得到同一个对象的别名。 任何时候你给任何类型的对象一个名字(你给一个现有的对象一个新的名字,或者你接收它传递给一个函数,或者你把它从某个容器),您为实际更改此对象所做的任何事情都会影响它原来所在的位置(即,将它传递给您的函数的调用者,或者查看您从中拉出它的容器的任何其他人)。

      如果需要,您可以让您的代码显式复制事物;使用mylist[:] 进行列表切片可能是您最熟悉的方式。许多内置操作都是这样做的;特别是,通常是内置函数/方法的一个安全假设,即如果它们返回一个对象,则它们没有修改原始对象(这是大多数时候遵循的一个很好的规则;如果你的函数or 方法通过更改现有对象来发挥作用,它应该返回None 并让调用者只查看他们给你的对象)。事实上,特别是对于列表,有很多对方法/函数做同样的事情;通常有一个函数返回列表的新修改副本,还有一个方法只返回更改列表。例如比较sorted_list = sorted(mylist) vs mylist.sort()reversed_list = reversed(mylist) vs mylist.reverse()

      但是在进行复制的情况下,您确实需要注意复制的深度深度;在大多数情况下,它只在最外层,因此对副本中包含的任何内容进行变异将从原始对象中可见。

      新的 Python 程序员需要尽早掌握这一点,因为它遍及 Python 编程的各个方面。

      不幸的是,这个问题被新程序员最自然的凝视点所掩盖。您首先要弄清楚如何对数字和文本字符串进行基本操作,但这些在 Python 中是不可变的。这并不意味着它们的工作方式与列表“不同”,它只是意味着您无法执行任何会导致它们发生变化的操作。所以你不需要关心它们是否被共享,因为即使它们被共享也不会产生影响。

      其他答案已经更详细地解释了您的示例中发生了什么。但是每当你修改一个列表(或任何其他对象)时,你需要思考“这个列表是从哪里来的?”。因为大多数时候,除非是刚刚创建的新列表,否则程序的其他部分将能够看到您所做的更改。列表操作几乎总是存在别名。很多时候它并不重要,特别是对于数字或文本列表。但是你需要时刻注意它,这样你才能决定它是否重要。

      不过,它确实很容易成为第二天性,所以坚持下去,它会变得容易得多。话虽如此,即使是非常有经验的 Python 程序员仍然会被偶尔出现的别名错误所困扰!

      【讨论】:

      • deepcopy 的深度有限制吗?
      • deepcopy 试图一路走下去;它是你对抗混叠战争中的反物质炸弹。它不能对所有可能的对象都起作用,而且几乎所有时间你都可以编写程序,这样你就不必对所有内容进行深度复制。但是对于你作为初学者 Python 程序员可能能够构建的任何对象,deepcopy 都可以正常工作,并且会给你一个没有相关别名的对象。
      【解决方案5】:

      当您说i = 0 时,您为变量i 分配了一个新值。

      当您说i[0] = 0 时,您修改变量i 中的列表,将第一个元素设置为新值。由于i 中的列表是l 的一个元素,所以l 以修改后的元素结束。

      【讨论】:

        【解决方案6】:

        有时我发现在不那么抽象的 C 世界中思考起来更容易。在 Python 中,将每个变量都视为一个指针。当您执行i = 0; i = 1 时,您并没有这样做:

        int * i = malloc(sizeof(int));
        *i = 0;
        *i = 1;
        

        更像是这样:

        int * i = malloc(sizeof(int));
        *i = 0;
        free(i);
        i = malloc(sizeof(int));
        *i = 1;
        

        您没有更改值,而是将指针移动到新值。

        虽然使用列表,l 指针保持不变,但您正在更新指定索引处的值。因此l = [0]; l[0] = 1 看起来像:

        int * l[] = {0};
        l[0] = 1;
        

        (注意:我意识到 Python 整数和列表并不是真正的 C 整数和数组,但出于这个目的,它们是相似的。)

        Protip:“别名”不是 Python 术语,所以请避免使用它。 “参考”或只是“变量”更好。

        【讨论】:

          猜你喜欢
          • 2011-09-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-08-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多