在Python中,“is”关键字可能与相等运算符等价的类型
在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
这引出了几个问题:
==
和is
的相似性和不可变性有关系吗?- 上面的行为是有规定的,还是只是实现细节?
- 最重要的是(也是最基本的),我怎么知道一个赋值操作是会创建对象的副本,还是只是引用它?
更新:如果赋值总是通过引用,那为什么下面的代码不打印2
呢?:
>>> a = 1
>>> b = a
>>> a = 2
>>> b
1
为什么这和下面的C语言代码片段不一样呢:
int a = 1;
int *b = &a;
a = 2;
printf("%d\n", *b);
抱歉我问的问题有点基础,但我还是Python新手,觉得理解这些很重要。你有什么推荐的阅读材料来帮助理解这些问题吗?
3 个回答
如果你之前学过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实际上并没有指针的概念。换句话说,你不能直接传递一个函数来指定某个变量应该存储什么。
要做到这一点,你需要传递一个名称(如果变量是全局的)或者传递一个设置函数(如果变量是局部的)。这并不是一个大问题,因为在大多数情况下,你只想要多个返回值,而Python已经很好地处理了这个问题:
def foo(x):
return x+1, x-1
a, b = foo(12)
另一个小麻烦是,如果你真的需要传递一个没有名称的局部变量的设置器(例如,列表中的一个元素),那么它不能是一个匿名的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)
,但这算是一个意外;lambda
在Python中是非常不受欢迎的,可能一旦大家发现可以这样做,语言就会增加限制来禁止它 ;-))。
如果你想改变一个有名称的局部变量,在Python 2.x中这是不可能的(但在Python 3.x中可以用nonlocal
实现)。
关于何时进行复制,何时只是复制指针的问题,答案非常简单。Python从来不会自动进行复制... 如果你想复制,必须自己明确地去做。这就是为什么你常常会看到这样的代码:
class Polygon:
def __init__(pointlist):
self.pointlist = pointlist[:]
这里的[:]
表示类实例想要存储传入列表的一个副本,这样如果你用一个点的列表创建一个Polygon
实例,然后修改这个列表,几何形状就不会改变。
== 和 is 这两个比较方式跟不可变性有关系吗?
没有关系。
你可以看看这个链接 Python 中 '==' 和 'is' 比较字符串时,'is' 有时会失败,为什么?,了解为什么在字符串上它们的表现不同;还有这个链接 Python 中 'is' 操作符在整数上表现得很意外,了解为什么在整数上它们的表现也不同(布尔值也是同样的原因)。
上面这些行为是规定好的,还是实现细节?
是实现细节。
我怎么知道一个赋值操作会生成对象的副本,还是只是引用它?
赋值操作总是通过引用进行的。只有当你明确使用 copy.copy
(或者类似的东西)时,才会进行复制。
补充:这里说的“通过引用”并不是指 C++ 中的引用。Python 的赋值会 重新绑定 变量。更像是
// int* a, *b;
a = new int(1);
b = a;
a = new int(2);
printf("%d\n", *b);
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 中,赋值操作总是将对象的引用绑定到变量名上,而不是复制对象。
更新
可以把 Python 的变量想象成指针,这和 C 语言类似:
>>> 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 */