首页 > 编程语言 >一文了解 Python 中的对象比较方法 is 和 == 及其本质

一文了解 Python 中的对象比较方法 is 和 == 及其本质

时间:2022-10-11 22:05:08浏览次数:99  
标签:__ 一文 Python self 本质 对象 eq id

一文了解 Python 中的对象比较方法 is 和 == 及其本质_魔术方法

1 Python 中的对象 ID

我们在学习基础的时候没听说 Python 有 C 或 C++ 中的指针啊,Python 中指针是什么?

先把指针这个概念放一放,一提到指针可能初学 C 和 C++ 的人都害怕(本人也害怕),先来理解一下 Python 里面对象的本质。

比如下面的代码,当声明 ​​ a = 100​​ 和 ​​b = 100​​ 的时候,能发现 ​​id(a) == id(b)​​,为什么 a 和 b 的 id 值是一样的呢?

>>> a = 100
>>> b = 100
>>> id(a)
4343720720
>>> id(b)
4343720720
>>>

我们来看一下这个图:

一文了解 Python 中的对象比较方法 is 和 == 及其本质_python_02

利用上图来打一个比喻,可能不是很准确但方便我们进行理解。如果计算机被当成是一栋楼,那么内存空间就相当楼中的每个房间,内存地址就是这个房间的门牌号,这个房间内可以存储数据(比如数字 100 ,数字 10 或者其他类型)。

假如有一天,来了个要租房的小 a,小 a 说:“我看中了门牌号为(内存地址 4343720720 )的这个房间”,并且放心的租用了这个房,所以 a = 100。小 a就住在了这个房间里,当我们查询 ​​id(a)​​的时候,计算机就返回给我们这个房间的门牌号(也就是内存地址 4343720720 )。

同理,小 b 也看中了这个房子,并且也放心的住了下来。而且因为房间里存储的数据都是 100,即使虽然 a 和 b 的名字不同,但他们住同一房间,所以内存地址就相同。

再看如下的代码:

>>> a, b = 10, 20
>>> id(a)
4343717840
>>> id(b)
4343718160
>>> a, b = b, a
>>> id(a)
4343718160
>>> id(b)
4343717840
>>>

当声明 ​​a = 10​​ 和 ​​b = 20​​ 的时候,情况发生了改变,这个过程其实也好理解,就是相当于小 a 和小 b 分别看中了不同的房间(小 a 看中的是门牌号 4343717840 的房间,小 b 看中的是门牌号 4343718160 ),当他们住下来后,这个房间存着不同数据( a=10, b=20 )。

当他们进行交换的时候​​a, b = b, a​​,就相当于交换了房间,但是房间里的数据是没有变。最后​​a=20, b =10​​,因为内存地址 4343717840 存的数字就是 10, 内存地址 4343718160 存的数字是 20。

一文了解 Python 中的对象比较方法 is 和 == 及其本质_python_03

2 ​​is​​ 比较符

如上图的例子,我们可以看到,如何确定 a 和 b 是否是一致的呢?通过:

>>> a is b

如果这两个对象确实是内存中的同一个对象将返回 True, 否则不一致就会返回 False 。

什么是对象 id

在 Python 中我们可以通过 ​​id()​​ 函数查看每个对象的内存 id。

  • ​id(x)​​ 是一个整数
  • ​id(x) == id(y)​​ 当且仅当 x 和 y 在内存中引用相同的对象时才为真
  • ​id(x)​​ 在 x 的生命周期内是恒定的,也就说,只要 x 存在,这个内存 id 也是不变的

Python 解释器有许多语言的实现,虽然以上三点在每一种实现的 ​​id()​​ 函数都是正确的,但它们背后的实现方式并不相同。例如 CPython (用 C 语言编写的 Python 解释器),使用对象的内存地址作为它的 id --但不要认为所有的实现都会这样做!

例如,另一个 Python 实现是 Skulpt,这是一个 Python 到 JavaScript 的编译器,Anvil 使用它来在浏览器中运行 ​​Python 客户端代码​​(这样您就可以为 Web 开发而无需编写 JavaScript)。 JavaScript 中的对象没有稳定的内存地址,JavaScript 也没有为每个对象公开一个稳定的标识符。所以,Skulpt 不能使用对象在内存中的地址作为它的 id。

在本文的其余部分,我们将使用使用 CPython 生成的示例,它确实使用对象的内存地址作为其 id。 CPython 能够做到这一点,因为一旦 CPython 中的对象存在,它就不能在内存中移动。这意味着使用对象的地址作为其 id 将保证该对象的生命周期内的稳定值,不像(例如)Skulpt。

我们来看一个列表的例子:

>>> a = ["Learning", "Python"]
>>> id(a)
1769202632064
>>> b = a
>>> id(a), id(b)
(1769202632064, 1769202632064)
>>>

我们定义了一个 a 列表,然后通过 ​​b = a​​ 建立指向 a 的新指针,当我们检查他们的 id 时,发现它们的结果是相同的 —— 1769202632064。

接下来,我们使用 ​​copy()​​ 函数来看一下会不会有不同的结果:

>>> c = a.copy()
>>> id(a), id(c)
(1769202632064, 1769171462656)
>>>

如上所示,a 和 c 有着不同的 id,说明调用 ​​copy()​​ 函数完全生成了一个全新的列表对象。

a is b 的本质

所以本质上来说,说 ​​a is b​​ 直接相当于说 ​​id(a) == id(b)​​ 。当你在两个对象上调用 is 时,Python 会获取它们的 id 并直接比较它们。而已!

>>> a is b
True
>>>

然而,这还不是故事的结局。 Python 有另一种更灵活的方式来定义对象相等性。

3 ​​==​​ 比较符

在 Python 中,想知道 “a 和 b 是否相同” 的另一种方法是使用如下的代码:

>>> a == b
True
>>>

就和 ​​is​​ 一样,​​==​​ 比较符也是返回 True 或 False。但是 Python 决定返回哪个的方式与 ​​is​​ 运算符不同,这意味着 ​​==​​ 和 ​​is​​ 可以为相同的对象提供不同的结果。

用同样的例子来看结果:

>>> a = ["Learning", "Python"]
>>> b = a
>>> c = a.copy()

>>> a == b
True
>>> a is b
True
>>> a == c
True
>>> a is c
False
>>>

将看到 ​​a ==c​​ 为 True, 反而 ​​a is c​​ 为 False。为什么?原因是:

  • ​is​​ 用于两个指针是否指向内存中完全相同的对象
  • ​==​​ 用于两个对象是否是被看做是相等的

​==​​ 运算符的本质

当您编写 ​​a == b​​ 时,实际上是在调用一个魔术方法(magic method),也称为 dunder 方法(函数名的两边的都有下划线,the double-underscore)。日常学习中你看能已经见过一些魔术方法,例如:

  • ​__init__​​ 方法:在初始化 Python 类的实例时调用
  • ​__str__​​ 方法:使用 str 函数时调用,例如 str(xxx)
  • ​__repr__​​ 方法,类似于 __str__ 但在其他情况下也会调用,例如错误消息

感兴趣可以跳转看我的这篇文章:​​一文让你彻底搞懂Python中__str__和__repr__?​

魔术方法只是 Python 类上的方法,双下划线表示它们与内置的 Python 函数交互。例如,在 Python 类上重写 ​​__str__​​ 方法会改变 str 函数在该修改类的实例上调用时的行为方式。

当谈到 == 和 ​​__eq__​​ 时,通过一些例子最容易理解。让我们来看看!

重写 ​​__eq_​​ 方法

下面,我们用自己的 ​​__eq__​​ 方法定义一个类。每个 ​​__eq__​​ 方法都有两个参数:

  • self,所讨论的类的实例
  • 第二个参数(在下面的代码片段中,我们定义为 other),它是与 self 进行比较的任何对象。
class MyEqualClass:
def __eq__(self, other):
return self is other

然后进行测试:

>>> class MyEqualClass:
def __eq__(self, other):
return self is other


>>> a = MyEqualClass()
>>> a == 'Hello World'
False
>>>

然后这会调用 MyEqualClass 的 ​​__eq__​​ 方法,其中 a 作为 self 参数,字符串 ​​'Hello World'​​ 作为另一个参数。

在上面的例子中,​​MyClass.__eq__​​ 使用了 is 比较器,相当于比较每个对象的 id。这实际上是 Python 中任何用户定义类的默认行为。

如果我们想自定义一个类,所以对比的对象都相等,就可以这样写:

class MyAlwaysTrueClass:
def __init__(self, name):
self.name = name

def __eq__(self, other):
return True

因为我们重写了 ​​__eq__​​ 方法以始终返回 True,这意味着在 == 比较器下,此类的所有实例都将被视为相等,即使它们的名称具有不同的值!

>>> jane = MyAlwaysTrueClass("Jane")
>>> bob = MyAlwaysTrueClass("Bob")
>>> jane.name == bob.name
False

>>> jane == bob
True

此外,与不同类型的对象相比,我们还可以看到 MyAlwaysTrueClass 的实例表现得很奇怪:

>>> jane = MyAlwaysTrueClass("Jane")

>>> jane == "some string"
True
>>> jane == None
True
>>> jane == True
True

当将 MyAlwaysTrueClass 的实例与任何东西(不仅仅是同一个类的实例)进行比较时,它将返回 True。这是因为,正如我们之前所说,​​__eq__​​ 方法默认情况下不检查被比较对象的类型。

那如果我们写一个功能相反的 ​​__eq__​​ 的 MyAlwaysFalseClass 呢?

class MyAlwaysFalseClass:
def __init__(self, name):
self.name = name

def __eq__(self, other):
return False

你可能觉得这很合理,但是:

>>> class MyAlwaysFalseClass:
def __init__(self, name):
self.name = name

def __eq__(self, other):
return False


>>> a = MyAlwaysFalseClass("name")
>>> a == a
False

上述的列子,​​a == a​​ 的结果是 False,那是因为我们在任何类型作对比中都会返回 ​​Flase​​。

此外,由于 ​​__eq__​​ 的行为取决于哪个对象是 self 哪个是 other,我们可以通过使用 MyAlwaysTrueClass 的一个实例和一个 MyAlwaysFalseClass 的实例来获得以下行为:

>>> jane = MyAlwaysTrueClass("Jane")
>>> bob = MyAlwaysFalseClass("Bob")
>>> jane == bob
True
>>> bob == jane
False
>>>

这是因为我们比较 jane 和 bob 的顺序很重要。当我们执行 ​​jane == bob​​ 时,我们使用 MyAlwaysTrueClass 中定义的 ​​__eq__​​ 方法:jane 是 self 而 bob 是 other,并且该方法总是返回 True。当我们写 ​​bob == jane​​ 时,情况正好相反,所以我们得到该表达式的 False 值。

总而言之:魔术方法是一种有趣的方式,它可以让 Python 做一些看起来很奇怪的事情,并且应该自行承担修改的风险!

4 总结

所以我们学了什么?我们已经介绍了 ​​is​​ 和 ​​==​​ 这两个 Python 中的相等概念,并了解到

  • ​is​​ 的本质使用对象 id;
  • ​==​​ 使用 __eq__ 魔术方法。

我们还探索了如何覆盖 ​​__eq__​​ 来自定义相等概念,并了解为什么这样做会导致一些奇怪的行为。

灵感来源:​​Memory Management in Python - Part 2: Equality (anvil.works)​

标签:__,一文,Python,self,本质,对象,eq,id
From: https://blog.51cto.com/yuzhou1su/5745127

相关文章

  • python装饰器初级
    global与nonlocal1.global的作用:可以在局部空间里直接就该全局名称工具中的数据代码展示:name='moon'#设置了一个全局变量deffucn():name='god'#......
  • (Python)email 邮件发送
    """1.发送邮件的几个步骤:1)与邮件服务器建立会话连接2)指定用户的登录3)发送邮件2.一个标准邮件包含:1)邮件头:标题;收件人、发送人、抄送cc、密送bcc......
  • 【python】ERA5逐小时降水数据计算逐日降水
    自用简单方法参考代码:ERA5:Howtocalculatedailytotalprecipitation-CopernicusKnowledgeBase-ECMWFConfluenceWiki1、先从官网(ERA5-Landhour......
  • Python之斐波那契数列的实现
    1.斐波那契数列的概念斐波那契数列(Fibonaccisequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(LeonardoFibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这......
  • Python学习路程——Day12
    Python学习路程——Day12global与nonlocal'''global: 是一个内置方法,它的作用是在函数体内声明一个全局名称空间,让这个全局名称空间可以在函数体内的局部名称空间中被......
  • python基础之闭包函数与装饰器
    python基础之闭包函数与装饰器目录一、global与nonlocal二、函数名的多种用法1.可以当变量名2.可以当函数的参数3.可以当函数的返回值三、闭包函数1.闭包函数的实际应用四......
  • python重点之装饰器
    global与nonlocal函数名的多种用法闭包函数装饰器简介无参装饰器有参装饰器装饰器模板装饰器语法糖今日详细内容global与nonlocalmoney=666defind......
  • python进阶之路11 闭包函数 装饰器
    函数名的多种用法函数名其实绑定的也是一块内存地址只不过该地址里面存放的不是数据值而是一段代码函数名加括号就会找到该代码并执行1.可以当作变量名赋值defindex......
  • Python爬虫-scrapyd框架部署
    爬虫项目部署1脚本文件部署linux内置的cron进程能帮我们实现这些需求,cron搭配shell脚本,非常复杂的指令也没有问题。1.1crontab的使用crontab[-uusername]/......
  • Python基础12
    今日内容概要global与nonlocal函数名的多种用法闭包函数装饰器简介装饰器推导流程装饰器模板装饰器语法糖今日内容详细global与nonlocal'''通过global声明可......