Python中“is”关键字可能等同于相等运算符的类型

4

对于Python中的某些类型,is运算符似乎等同于==运算符。例如:

>>> 1 is 1
True
>>> "a spoon" is "a spoon"
True
>>> (1 == 1) is (2 == 2)
True

然而,并非总是这样:
>>> [] == []
True
>>> [] is []
False

对于可变类型,比如列表,这个说法是有道理的。但是不可变类型,比如元组似乎也会表现出同样的行为:
>>> (1, 2) == (1, 2)
True
>>> (1, 2) is (1, 2)
False

这引出了几个问题:
  1. == / is 的等价性是否与不可变性有关?
  2. 上述行为是指定的还是实现细节?
  3. 最重要的是(基本上),我如何知道赋值会导致对象的副本被创建,还是会创建对它的引用?
更新: 如果赋值总是通过引用进行,为什么以下代码没有打印 2 呢?
>>> a = 1
>>> b = a
>>> a = 2
>>> b
1

为什么这不等同于以下C语言代码片段:
int a = 1;
int *b = &a;
a = 2;
printf("%d\n", *b);

非常抱歉这个问题有点新手,但我是Python的初学者,觉得理解这个问题很重要。您能推荐一些阅读材料来了解这些问题吗?

3个回答

10

is操作符测试两个对象是否在内存中物理上相同,也就是它们是否具有相同的内存地址。这也可以使用id()函数进行测试:

>>> a = 1
>>> b = 1
>>> a is b
True
>>> id(a) == id(b)
True
另一方面,== 运算符用于测试语义相等性。通过实现 __eq__() 函数,自定义类也可以覆盖此操作符。在语义上,如果两个不同的列表的元素都相等,则它们是相等的,但物理上它们将是不同的对象。
像字符串和元组这样的不可变类型可能会被 Python 实现池化,因此两个字面量字符串对象实际上是物理上相同的。但这并不意味着您总是可以使用 is 来比较这些类型,如下面的示例所示:
>>> "foobar" is "foobar"   # The interpreter knows that the string literals are
True                       # equal and creates only one shared object.
>>> a = "foobar"
>>> b = "foobar"
>>> a is b        # "foobar" comes from the pool, so it is still the same object.
True
>>> b = "foo"     # Here, we construct another string "foobar" dynamically that is
>>> b += "bar"    # physically not the same as the pooled "foobar".
>>> a == b
True
>>> a is b
False

在Python中,赋值操作总是将变量名绑定到一个对象上,并且从不暗示复制。

更新

类似于C语言,可以将Python变量视为指针:

>>> a = 1
>>> b = a
>>> a = 2
>>> b
1

大致相当于:

const int ONE = 1;
const int TWO = 2;

int *a = &ONE;
int *b = a;  /* b points to 1 */
a = &TWO;    /* a points to 2, b still points to 1 */

1
Python 中没有变量,只有名称。 - user3850
1
“Python 中没有变量,只有名称”这句口号是误导性的、无益的。 - Ned Batchelder

7
== / is 相等性是否与不可变性相关?
不相关。
请参考关于字符串的 Python '==' vs 'is' comparing strings, 'is' fails sometimes, why? 以及关于整数(因此布尔值也是同样道理)的 Python "is" operator behaves unexpectedly with integers
上述行为是规定好的吗,还是实现细节?
是实现细节。
如何知道赋值操作是否会复制对象还是只是引用它?
赋值总是通过引用进行。仅在您明确使用 copy.copy(或类似方法)时才会复制。
编辑:我所说的“通过引用”并不是指 C++ 中的引用。Python 的赋值会使变量重新绑定。这更像是…
// int* a, *b;
a = new int(1);
b = a;
a = new int(2);
printf("%d\n", *b);

回应中更新了问题,请确认是否可以更新答案。 - fmark
啊哈,我现在理解得更好了,谢谢。你能推荐一些阅读材料吗? - fmark

1
如果您具有C或C++背景,那么理解Python中的所有变量实际上都是指针可能更加简单。因此,这个语句:
 a = 1

确实大致相似

 Object *a = new Integer(1);

is 运算符检查指针相等性,而 == 运算符则涉及取决于对象类型的计算。

这个方案的一个小复杂之处在于,如果对象是不可变的(例如整数),那么出于效率原因,上面的代码确实有点像:

 int *a = getFromCacheOrCreateNewInteger(1);

有时候(但这只是实现细节),不可变对象可能从逻辑上独立创建,但在is操作下仍然是同一对象。(例如,可能会出现1+1 is 2-1的情况,但不保证一定如此):
>>> 1+2 is 2+1
True
>>> 99999+1 is 1+99999
False
>>> 

为了增加一点混淆,即使 Python 中的所有变量确实都是指针,令人惊讶的是 Python 中没有指针的概念,换句话说,没有办法传递一个函数来存储你的变量中的某些内容。
要做到这一点,你需要传递一个名称(如果变量是全局的)或者传递一个 setter 函数以便调用(如果变量是本地的)。这并不是一个真正的大烦恼,因为在大多数情况下,你只想要多个返回值,Python 已经很好地处理了这个问题:
def foo(x):
    return x+1, x-1

a, b = foo(12)

另一个额外的烦恼是,如果你真的需要传递一个没有名称的局部变量(例如列表的元素)的 setter,它不能是匿名 lambda,因为赋值是一个语句,而 lambda 只允许单个表达式。但是,你可以为此定义局部函数...
def foo(x, setter):
    setter(x + 1)

def bar():
    mylocal = [1,2,3]

    def setFirst(value):
        mylocal[0] = value

    foo(32, setFirst)

好吧,我承认……实际上可以使用lambda value: mylocal.__setitem__(0, value),但这更多的是一个不必要的事件;在Python中,lambda是如此被憎恶,以至于一旦他们发现这是可能的,语言中可能会添加另一个限制来禁止它;-))。

如果您想改变命名的本地变量,那么在Python 2.x中就不可能(但在Python 3.x和nonlocal中是可能的)。

关于何时执行副本以及何时仅复制指针的问题,答案非常简单。Python从不自动进行复制……如果您想进行复制,则必须明确地执行。这就是为什么例如经常看到像下面这样的代码:

class Polygon:
    def __init__(pointlist):
        self.pointlist = pointlist[:]

[:] 符号表示类实例希望存储传递的列表的副本,因此如果您使用点列表创建Polygon实例,然后稍后修改此列表,则几何图形不会更改。


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