原标题:Python ‘!=’ Is Not ‘is not’: Comparing Objects in Python
原地址:https://realpython.com/python-is-identity-vs-equality/
Python 中的一致操作符 (is
) 和 相等的操作符 (==
) 之间有些微妙的不同。直到某些情况之前,当你使用 python is
操作符比较两个数字 (number) 时,你的代码都可以运行的非常好。你可能听说过在某些情况下,is
操作符要比 ==
操作符执行的更快,或者你可能感觉前者看起来更加的 pythonic 。然而,需要谨记的是这些操作符所表达的含义并不相同。
操作符 ==
比较的是两个对象的值是否相等,而 is
操作符是用来检查两个变量是否指向内存中同一个对象。在绝大多数的情况下,这意味着应该使用相等操作符 ==
和 !=
,除非你是和 None
进行比较。
在这篇文章中,你将会看到:
对象相等和相同之间有什么不同
比较对象时,何时使用相等操作符,何时使用一致操作符
Python 操作符如何实现的
为什么使用
is
和is not
比较两个值会导致一些不可预测行为如何写一个常规的
__eq__()
类中的方法去定义相等操作行为
Comparing Identity With the Python is and is not Operators
Python 的 is
和 is not
操作符是用来比较两个对象是否相同的。在 CPython 中,它代表了两个对象的内存地址。在 Python 中的任何东西都是对象,每个对象都存储在一个特定的内存位置。Python 的 is
和 is not
操作符检查两个变量是否指向内存中同一个对象。
你可以使用 id() 来检查一个对象的标识:
1 | help(id) |
最后一行展示了内置方法 id 自己存储的内存地址。
在某些常见情况下,具有相同值的对象默认情况下具有相同的 id。例如,数字 -5 ~ 256 在 CPython 中是内置的。每个数字都存储在内存中的单个固定位置,这样可以节省常用整数的内存。
你也可以使用 sys.intern()
去内置一些字符串。这个函数允许你比较这些字符串的内存地址,而不是一个字符一个字符的进行比较:
1 | from sys import intern |
变量 a 和 b 初始化指向内存中两个不同的对象,所以他们有不同的 id。当你 intern 它们, 你会发现 a 和 b 在内存中指向了相同的对象。任何一个值为 “hello world” 的新字符串会被创建到新的内存地址,但是当你 intern 这个字符串时,你会发现它指向了你第一个 intern 为 “hello world” 字符串相同的内存地址。
其它的一些默认的内置是 None, True, False
,以及一些简单的字符串。记住,在大多数情况下,有着相同值得不同对象会存储在内存的不同位置。这意味着你不应该使用 Python is 操作符去比较不同对象的值。
When Only Some Integers Are Interned
在幕后,Python 内置常用值的对象 (例如,整数中的 -5 ~ 256) 用来节省内存。下面的代码向您展示如何只有一些整数具有固定的内存地址:
1 | 256 a = |
最初,a和b指向内存中的同一对象,但是当它们的值超出公共整数范围(从-5到256)时,它们将存储在单独的内存地址中。
When Multiple Variables Point to the Same Object
当您使用赋值运算符 (=
) 从而让一个变量等于另一个变量时,实际上你是让这些变量指向内存中的同一对象。这可能会导致易变对象意料之外的行为:
1 | 1, 2, 3] a = [ |
刚刚发生了什么?您向 a 添加了一个新元素,但是现在 b 也包含了这个元素!好吧,在 b = a
的行中,将 b 设置为指向与 a 相同的内存地址,这样两个变量现在都指向同一对象。
如果您分别独立定义这些 lists,那么它们将存储在不同的内存地址中并独立运行:
1 | 1, 2, 3] a = [ |
因为 a 和 b 现在 指向内存中不同的对象,改变一个不会影响另一个。
Comparing Equality With the Python == and != Operators
回想一下,具有相同值的对象通常存储在单独的内存地址中。如果要检查两个对象是否具有相同的值,而不管它们在内存中的存储位置,请使用等于运算符 ==
和 !=
。在大多数情况下,这就是您要做的。
When Object Copy Is Equal but Not Identical
在下面的示例中,将 b设置为 a 的副本 (这是一个可变对象,例如 list 或 dictionary )。这两个变量将具有相同的值,但是每个变量将存储在不同的内存地址中:
1 | 1, 2, 3] a = [ |
现在 a 和 b 存储在不同的内存地址,因此 a is b
将不再返回 True
。但是,a == b
返回 True
,因为两个对象具有相同的值。
How Comparing by Equality Works
等号运算符 ==
的魔力发生在 ==
符号左侧的对象的__eq__()
类方法中。
这是一个[魔术类方法],只要将该类的实例与另一个对象进行比较,就会调用该方法。如果未实现此方法,则默认情况下 ==
比较两个对象的内存地址。
作为练习,制作一个从 str
继承的 SillyString
类,并实现 __eq__()
来比较此字符串的长度是否与另一个对象的长度相同:
1 | class SillyString(str): |
现在,一个 SillyString
‘hello world’ 应该等于字符串 ‘world hello’ ,甚至等于任何其他相同长度的对象:
1 | # Compare two strings |
当然,对于一个对象来说这是愚蠢的行为,除非这个对象是一个字符串,但是他解释了当你用 == 比较两个对象时发生了什么。!= 操作符给出了一个相反的结果,除非实现了类中的 __ne__()
方法。
The example above also clearly shows you why it is good practice to use the Python is operator for comparing with None, instead of the == operator. Not only is it faster since it compares memory addresses, but it’s also safer because it doesn’t depend on the logic of any eq() class methods.
上面的示例还清楚地向您展示了为什么使用 Python is 运算符与 None(而不是==运算符)进行比较是一种好习惯。由于它可以比较内存地址,因此不仅速度更快,而且由于它不依赖于任何 __eq__()
类方法的逻辑,因此更加安全。
Comparing the Python Comparison Operators
根据经验,除与 None
进行比较,否则应始终使用等号运算符 ==
和 !=
。
- 使用Python
==
和!=
运算符比较对象是否相等。这里,通常是比较两个对象的值。如果要比较两个对象是否具有相同的内容,而不关心它们在内存中的存储位置,一般使用==
和!=
。 - *当您想比较对象身份时,请使用Python
is
和is not
*。这里,您比较的是两个变量是否指向内存中的同一对象。这些运算符的主要用来与None
进行比较时使用的。与使用类方法相比,按内存地址与None
进行比较更加安全快捷。
具有相同值的变量通常存储在独立的内存地址中。这意味着正常情况下你应该使用 ==
和 !=
比较它们的值,仅当您要检查两个变量是否指向同一内存地址时,才去使用 Pythonis
和 is not
运算符。
Conclusion
在这篇文档中,你学习了 ==
和 !=
是用来比较两个对象值是否相等,而 is
和 is not
操作符是用来比较两个对象是否指向相同内存地址的同一个对象。
如果您牢记这种区别,那么您应该能够防止代码中的意外行为。如果您想了解更多有关 *object interning * 和Python 是操作符的奇妙世界,可以查看下面这篇文章:为什么? 应该几乎永远不要在Python中使用 is
。你也可以看看如何使用 sys.intern()
优化字符串的内存使用和比较的时间,尽管有可能 Python 已经在后台自动为您处理了。
现在,您已经了解相等操作符和一致操作符的作用,你也可以尝试编写自己的 __eq__()
类方法,该方法定义使用 ==
运算符时如何比较此类实例。
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
扫描二维码,分享此文章