“is”和“==” 的区别
前言
在 聊聊Python的type函数和isinstance函数 一文中,本人简略提到了==
和is
两种比较方式的区别,Python 有不同的方式来比较两种任何类型的对象是否相等,比如,用==
运算符、is
关键字两种方式来比较对象,或者用 __eq__
方法来自定义比较行为。本文将详细介绍两种比较方式的区别、__eq__
方法和它们的关系,以及一些特例和背后的原理。
本文中的命令行示范,都是基于以下环境:
- 操作系统: CentOS 7
- Python:
3.7.03.7.9 (更新)
“==” 运算符
==
运算符比较两个对象的值(内容)是否相等。如何判断,取决于类的__eq__
方法如何实现。
示例1:
|
|
执行结果:
|
|
例子中,x 和 y 指向两个不同的列表对象,但它们的内容相同,z 和 x 指向同一个对象。当用==
比较 x 和 y、z 和 y 的时候,它们都相等,因为对于列表而言,__eq__
的实现就是判断列表的内容是否一样;打印它们的标识号(identity),x 和 z 的标识号一样,因为它们是一个对象,y 是另一个对象,所以标识号不同。
示例2:
|
|
执行结果:
|
|
这个例子和上一次例子的区别在于列表内嵌套了另一个列表。可以看到,对于列表类型而言,==
不仅会比较列表内第一层元素,还会递归地比较嵌套的列表内的元素。其实,对于 Python 标准库的任何可嵌套类型,==
都会递归地比较所有元素。
示例3:
|
|
执行结果:
|
|
这个例子中,对于第一个结果,比较好理解,因为 Jack 和 tom 指向的两个对象,name 属性不同,所以这两个对象也肯定不同。但第二个例子,两个对象的 name 属性都初始化为相同的值,为什么比较结果也是 False 呢?
这是因为,对于用户自定义的类,我们需要显示地指明,==
运算符应该如何比较它们的对象。 这里,对于 Student 类,我们没有指明如何比较,当执行==
运算符的时候,Python 也就无法知道用户希望应该如何比较,这种情况下,它会默认比较对象的标识号,即执行id(obj1) == id(obj2)
。例子中,jack 和 jack2 两个对象的标识号显然不同,所以比较结果为 False。
显示地指明如何比较,就需要我们自定义__eq__
方法。 这是 Python 的一个dunder 方法
,意思是以双下划线开头并以双下划线结束的方法,dunder
是 “Double Underscores” 的缩写。__eq__
这个dunder 方法
,允许用户覆盖==
运算符的默认实现。
示例4:
|
|
执行结果:
|
|
这个例子中,我们自定义了__eq__
方法,覆盖了默认的==
运算符的实现,当两个 Student 对象的 name 属性相等时,对象本身就相等。所以执行结果为 True。
“is” 运算符
is
运算符比较两个对象的标识号(identity),所谓标识号,就是用来标识对象的唯一性,可以把它看成是对象的地址。不同的对象,标识号一定是不同的,相同的对象,标识号是相同的。可以用id()
来获得对象的标识号。
is
的特殊之处在于,它不能被覆盖(override),即用户不能定义一个像__eq__
一样的dunder
方法来覆盖is
的行为。下面来看一些例子。
示例5:
|
|
执行结果:
|
|
可以看到,这个例子和上一个例子相同处是类的实现,区别是用is
比较 jack、Jack2 和 Jack3 三个对象,而不是用==
比较。第一个结果是 False,因为 jack 和 jack2 指向两个不同的对象,第二个结果为 True,因为 Jack3 和 jack 指向同一个对象。
示例6:
|
|
执行结果;
|
|
x 和 y 是不同的列表对象,id 不同,所以is
比较结果为 False。z 和 x 指向同一个对象,所以结果为 True。
“is” 比较中的 interning 优化
示例6 中我们看到,两个不同的列表对象,即使它们内容相同,但 id 也是不同的,所以它们的is
比较结果为 False 。那么是不是对于所有对象都成立呢?
示例7:
|
|
执行结果:
|
|
这个例子中,一开始,x 和 y 指向了两个似乎是 “不同” 的元祖对象,但出乎意料,它们的 id 都相同,所以is
比较的结果也为 True。接下来,相同的字符串赋值给 x 和 y,和前面相同,它们指向同一个对象,所以is
比较结果也为 True。最后,x 和 y 同样被赋值为元祖对象,区别在于这个元祖对象内嵌套了一个列表对象,它们的 id 却不相同了,is
比较结果也是 False。
之所以这里的结果和 示例6 不太一样,是因为 Python 对于一些不可变类型的对象,会进行interning
优化。所谓interning
优化,就是指在内存中只保留一份该对象,当需要构造新的一摸一样的对象的时候,Python 直接引用该对象,而不是再创建一份新对象。 比如本例子的开头处,似乎创建了两个 tuple 对象并分别赋给 x 和 y,但实际上 Python 在解释代码的时候发现想要创建的 (1, 2, 3) 元祖对象之前已经创建过了,所以它就不会再创建新的元祖对象,而是直接让 y 变量引用到先前创建的元祖对象上。
进行interning
优化的一定是不可变对象,一般是这些类型:数字、字符串、不嵌套可变对象的元祖。 如何interning
优化取决于 Python 解释器,不同实现方式的 Python 解释优化方式是不一样的,甚至同一种实现方式的解释器,如 CPython,在不同的版本间,也存在明显不同。关于interning
优化的细节,本人将在另一篇文章中单独介绍。
在这个例子中,(1, 2, 3) 和 “hello world” 对象都被优化了,内存中只存在一份,所以 x 和 y 的 id 一样,is
比较结果相等。而 (1, [2], 3) 元祖中嵌套了一个列表,所以没有优化,x 和 y 指向两个不同的对象。
“is” 和 “==” 的反义词
既然 ”is“ 和 ”==“ 的含义和作用不同,我们自然也要留意它们各自的反义词。和 ”is“ 相反的判断式是 ”is not“,而和 ”==“ 相反的判断式是 ”!=“ 。
- “is not“ 判断两侧的目标不是同一个对象,即它们的 identity 不同。
- “!=” 判断两侧的目标的值(内容)不同,它会调用
__ne__
方法来决定是 True 还是 False。
使用是要注意的它们的区别。
其它特殊对象
有些特殊的类型,Python 保证在程序运行过程中只有一份实例对象。比如None
、NotImplemented
和Ellipsis
。它们是单例,所以当它们和自己用is
来比较的时候,一定返回 True:
|
|
执行结果:
|
|
True
和False
也是单例,所以:
|
|
执行结果:
|
|
另外,和 C/C++ 语言不同,非空对象本身,并不会表示为 bool 值,所以如果要判断它们是否为真,必须用 bool 函数修饰后,才能和 True 对象比较:
|
|
执行结果:
|
|
需要注意的是,非空对象如果单独出现在条件语句中,Python 会把它隐式地转换成 bool 对象:
|
|
执行结果:
|
|
可以看到,对于第一种情况,非空列表 [1] 单独出现在条件语句中,Python 会用 bool([1]) 把它转换成 True 对象;第二种情况,Python 直接判断 “[1] is True” 条件语句,它的值是 False。所以,在条件语句中,建议不要轻易把测试对象和 True/False 进行is
或==
比较,除非你能确定测试对象一定是个 bool 类型。
「 您的赞赏是激励我创作和分享的最大动力! 」
- 原文链接:https://zhuyinjun.me/2019/difference_among_is_equal_operator/
- 版权声明:本创作采用 CC BY-NC 4.0 国际许可协议,非商业性使用可以转载,但请注明出处(作者、链接),商业性使用请联系作者获得授权。