目录

“is”和“==” 的区别

前言

聊聊Python的type函数和isinstance函数 一文中,本人简略提到了==is两种比较方式的区别,Python 有不同的方式来比较两种任何类型的对象是否相等,比如,用==运算符、is关键字两种方式来比较对象,或者用 __eq__方法来自定义比较行为。本文将详细介绍两种比较方式的区别、__eq__方法和它们的关系,以及一些特例和背后的原理。

注意

本文中的命令行示范,都是基于以下环境:

  • 操作系统: CentOS 7
  • Python: 3.7.0 3.7.9 (更新)

 

“==” 运算符

==运算符比较两个对象的值(内容)是否相等。如何判断,取决于类的__eq__方法如何实现。

示例1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
x = [1, 2, 3]
y = [1, 2, 3]
z = x

print("x == y : ", x == y)
print("z == y : ", z == y)

print("id(x) is ", id(x))
print("id(y) is ", id(y))
print("id(z) is ", id(z))

执行结果:

1
2
3
4
5
x == y :  True
z == y :  True
id(x) is  2708888048136
id(y) is  2708888048648
id(z) is  2708888048136

例子中,x 和 y 指向两个不同的列表对象,但它们的内容相同,z 和 x 指向同一个对象。当用==比较 x 和 y、z 和 y 的时候,它们都相等,因为对于列表而言,__eq__的实现就是判断列表的内容是否一样;打印它们的标识号(identity),x 和 z 的标识号一样,因为它们是一个对象,y 是另一个对象,所以标识号不同。

示例2:

1
2
3
4
5
6
x = [1, [4, 5], 3]
y = [1, [4, 5], 3]
print("x == y : ", x == y)
x[1].append(6)
print(x, y, sep='\n')
print("x == y : ", x == y)

执行结果:

1
2
3
4
x == y : True
[1, [4, 5, 6], 3]
[1, [4, 5], 3]
x == y : False

这个例子和上一次例子的区别在于列表内嵌套了另一个列表。可以看到,对于列表类型而言,==不仅会比较列表内第一层元素,还会递归地比较嵌套的列表内的元素。其实,对于 Python 标准库的任何可嵌套类型,==都会递归地比较所有元素。

示例3:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Student:
  def __init__(self, name):
    self.name = name

jack = Student('Jack')
tom = Student('Tom')
jack2 = Student('Jack')

print(jack == tom)
print(jack == jack2)

执行结果:

1
2
False
False

这个例子中,对于第一个结果,比较好理解,因为 Jack 和 tom 指向的两个对象,name 属性不同,所以这两个对象也肯定不同。但第二个例子,两个对象的 name 属性都初始化为相同的值,为什么比较结果也是 False 呢?

这是因为,对于用户自定义的类,我们需要显示地指明,==运算符应该如何比较它们的对象。 这里,对于 Student 类,我们没有指明如何比较,当执行==运算符的时候,Python 也就无法知道用户希望应该如何比较,这种情况下,它会默认比较对象的标识号,即执行id(obj1) == id(obj2)。例子中,jack 和 jack2 两个对象的标识号显然不同,所以比较结果为 False。

显示地指明如何比较,就需要我们自定义__eq__方法。 这是 Python 的一个dunder 方法,意思是以双下划线开头并以双下划线结束的方法,dunder是 “Double Underscores” 的缩写。__eq__这个dunder 方法,允许用户覆盖==运算符的默认实现。

示例4:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Student:
  def __init__(self, name):
    self.name = name

  def __eq__(self, other):
    if isinstance(other, Student) and self.name == other.name:
      return True
    return False

jack = Student('Jack')
jack2 = Student('Jack')

print(jack == jack2)

执行结果:

1
True

这个例子中,我们自定义了__eq__方法,覆盖了默认的==运算符的实现,当两个 Student 对象的 name 属性相等时,对象本身就相等。所以执行结果为 True。

 

“is” 运算符

is运算符比较两个对象的标识号(identity),所谓标识号,就是用来标识对象的唯一性,可以把它看成是对象的地址。不同的对象,标识号一定是不同的,相同的对象,标识号是相同的。可以用id()来获得对象的标识号。

is的特殊之处在于,它不能被覆盖(override),即用户不能定义一个像__eq__一样的dunder方法来覆盖is的行为。下面来看一些例子。

示例5:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Student:
  def __init__(self, name):
    self.name = name

  def __eq__(self, other):
    if isinstance(other, Student) and self.name == other.name:
      return True
    return False

jack = Student('Jack')
jack2 = Student('Jack')
jack3 = jack

print(jack is jack2)
print(jack is jack3)

执行结果:

1
2
False
True

可以看到,这个例子和上一个例子相同处是类的实现,区别是用is比较 jack、Jack2 和 Jack3 三个对象,而不是用==比较。第一个结果是 False,因为 jack 和 jack2 指向两个不同的对象,第二个结果为 True,因为 Jack3 和 jack 指向同一个对象。

示例6:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
x = [1, 2, 3]
y = [1, 2, 3]
z = x

print("y is x : ", y is x)
print("z is x : ", z is x)

print("id(x) is ", id(x))
print("id(y) is ", id(y))
print("id(z) is ", id(z))

执行结果;

1
2
3
4
5
y is x :  False
z is x :  True
id(x) is  2058952462792
id(y) is  2058952463304
id(z) is  2058952462792

x 和 y 是不同的列表对象,id 不同,所以is比较结果为 False。z 和 x 指向同一个对象,所以结果为 True。

 

“is” 比较中的 interning 优化

示例6 中我们看到,两个不同的列表对象,即使它们内容相同,但 id 也是不同的,所以它们的is比较结果为 False 。那么是不是对于所有对象都成立呢?

示例7:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
x = (1, 2, 3)
y = (1, 2, 3)
print("y is x : ", y is x)
print("id(x) is ", id(x))
print("id(y) is ", id(y))

x = "hello world"
y = "hello world"
print("y is x : ", y is x)
print("id(x) is ", id(x))
print("id(y) is ", id(y))

x = (1, [2], 3)
y = (1, [2], 3)
print("y is x : ", y is x)
print("id(x) is ", id(x))
print("id(y) is ", id(y))

执行结果:

1
2
3
4
5
6
7
8
9
y is x :  True
id(x) is  2976096607272
id(y) is  2976096607272
y is x :  True
id(x) is  2976374381424
id(y) is  2976374381424
y is x :  False
id(x) is  2976098318360
id(y) is  2976098318280

这个例子中,一开始,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 保证在程序运行过程中只有一份实例对象。比如NoneNotImplementedEllipsis。它们是单例,所以当它们和自己用is来比较的时候,一定返回 True:

1
2
3
None is None
NotImplemented is NotImplemented 
Ellipsis is Ellipsis

执行结果:

1
2
3
True
True
True

TrueFalse也是单例,所以:

1
2
True is True
False is False

执行结果:

1
2
True
True

另外,和 C/C++ 语言不同,非空对象本身,并不会表示为 bool 值,所以如果要判断它们是否为真,必须用 bool 函数修饰后,才能和 True 对象比较:

1
2
3
4
print([1] is True)
print([1] == True)
print(bool([1]) is True)
print(bool([1]) == True)

执行结果:

1
2
3
4
False
False
True
True

需要注意的是,非空对象如果单独出现在条件语句中,Python 会把它隐式地转换成 bool 对象:

1
2
3
4
5
if [1]:
  print("in condition without True")

if [1] is True:
  print("in condition with True")

执行结果:

1
in condition without True

可以看到,对于第一种情况,非空列表 [1] 单独出现在条件语句中,Python 会用 bool([1]) 把它转换成 True 对象;第二种情况,Python 直接判断 “[1] is True” 条件语句,它的值是 False。所以,在条件语句中,建议不要轻易把测试对象和 True/False 进行is==比较,除非你能确定测试对象一定是个 bool 类型。

 


- 全文完 -

相关文章

「 您的赞赏是激励我创作和分享的最大动力! 」