说起 Python怎么比较 这件事,我脑子里立马浮现出无数画面。哎呀,刚入门那会儿,我在这上面栽的跟头可不少。总觉得“这不就是比大小嘛”,或者“这不就是看是不是一样嘛”,简单得不能再简单了。结果呢?现实啪啪打脸,代码一跑错,傻眼了。所以啊,别小瞧 Python的比较操作,里头门道深着呢。
最基础的,咱得从 数值的比较 说起。这玩意儿最直接,小学生都懂的加减乘除,比大小。比如 `a > b`,`c <= d`,这没啥说的,就是字面意思。整数、浮点数,老老实实按大小来。等于 用的是双等号 `==`,不等于 是 `!=`。记住了,一个等号 `=` 是赋值,两个等号 `==` 才是判断相等。这 rookie 错误我犯过,信不信由你。
但是,数值比较里有个坑,尤其是 浮点数。你拿 `0.1 + 0.2 == 0.3` 去 Python 里试试?多半结果会让你怀疑人生——False!为啥?因为浮点数在计算机里是用二进制表示的,有些十进制小数没法精确表示,会有微小的误差。就像 `1/3` 你用小数写永远写不完一样。所以,比较浮点数是否相等,最好别直接用 `==`。常见的做法是看看它们的差的绝对值是不是小于一个很小的阈值(比如 `1e-9`)。哎,这点儿事儿,一开始谁告诉我啊,都是自己摸索或者看别人的吐槽才知道。
再来聊聊 字符串的比较。字符串比较,默认是 按字典序。简单说,就是一个字符一个字符地比,就像查字典一样。先看第一个字符,谁的 ASCII 值小(或者在 Unicode 表里排得靠前),谁就“小”。比如 `'apple'` 和 `'banana'`,第一个字符 'a' 和 'b','a' 小,所以 `'apple' < 'banana'`。如果第一个字符一样,就比第二个,以此类推。比如 `'cat'` 和 `'car'`,前两个 'c' 和 'a' 都一样,比第三个,'t' 的 ASCII 值比 'r' 大,所以 `'cat' > 'car'`。大小写是区分的,大写字母通常比小写字母“小”(ASCII 值小)。 `'A' < 'a'`,是不是有点反直觉?但它就是这么工作的。字符串的 `==` 呢?那就要求 两个字符串的内容完全一样,包括大小写、空格、标点,一个不多一个不少。
接下来的重点戏来了:对象的比较。Python 里一切皆对象。你创建个列表 `[1, 2]`,创建个字典 `{'a': 1}`,甚至定义个函数,它们都是对象。对象的比较,可不像数值那么简单粗暴。这里涉及两个截然不同的概念:值相等 (equality) 和 身份相等 (identity)。
值相等,用的是我们熟悉的 `==` 运算符。它比较的是 对象所包含的值。如果两个对象的值看起来一样,那 `==` 就返回 True。比如:
“`python
list1 = [1, 2, 3]
list2 = [1, 2, 3]
print(list1 == list2)
“`
这段代码会输出 `True`。为啥?因为 `list1` 和 `list2` 尽管是两个不同的列表对象(待会儿讲身份),但它们里面的元素和顺序完全一样,值是一样的。再比如:
“`python
str1 = "hello"
str2 = "hello"
print(str1 == str2)
“`
这个也会输出 `True`。字符串的值就是它包含的字符序列。
那么 身份相等 是啥?它用的是 `is` 运算符。`is` 比较的是 两个变量是不是指向内存中的同一个对象。你可以把每个对象想象成住在内存里的一个“房子”,每个“房子”都有个唯一的地址。`is` 就是看两个变量名是不是指着同一个“房子”。用 Python 内置函数 `id()` 可以获取一个对象的唯一标识符,你可以理解为它的内存地址(当然,这只是个概念上的类比,实际可能更复杂)。如果 `object1 is object2` 返回 True,那么 `id(object1)` 肯定等于 `id(object2)`。反过来,如果 `id(object1) == id(object2)`,那么 `object1 is object2` 也肯定返回 True。
看刚才列表的例子:
“`python
list1 = [1, 2, 3]
list2 = [1, 2, 3]
print(list1 is list2)
“`
这个会输出 `False`!虽然它们的值一样,但 `list1 = [1, 2, 3]` 会在内存里新建一个列表对象,`list2 = [1, 2, 3]` 又会在内存里新建一个完全独立的列表对象。它们是两个不同的“房子”,只是长得一样、里面的东西一样罢了。
再看字符串的例子:
“`python
str1 = "hello"
str2 = "hello"
print(str1 is str2)
“`
嘿,这个竟然输出 `True`!是不是有点意外?这是因为 Python 为了优化内存使用,对于一些不可变对象(immutable objects),比如短字符串、小整数等,会进行 intern(驻留)操作。也就是说,如果内存里已经有了 `"hello"` 这个字符串对象,当你再次创建 `"hello"` 时,Python 可能不会新建对象,而是直接让新变量指向那个已有的对象。这样就省空间了。但要注意,这不是所有字符串都会这样,长字符串或者包含特殊字符的字符串就不一定会被 intern。这属于 Python 的实现细节,了解一下就好,但不要依赖它来做判断。判断字符串是否内容相等,永远用 `==` 才是王道。
所以啊,`==` 比值,`is` 比身份。这是 Python怎么比较 对象的核心。什么时候用哪个?如果你是想看两个东西的内容是不是一样,用 `==`。如果你是想看两个变量是不是指向了同一个具体的对象实例,用 `is`。
有个特别常用的场景,就是判断一个变量是不是 None。None 在 Python 里是个特殊的单例对象(Singleton)。表示空、没有值 的概念。判断一个变量 `my_variable` 是不是 None,一定要用 `my_variable is None`,而不是 `my_variable == None`。虽然 `==` 大多数情况下也能工作,但 `is None` 是 Python 社区约定俗成的最佳实践,更清晰、更符合语义,而且某些极少数特殊情况下 `== None` 可能会被重载导致行为异常,而 `is None` 永远只检查身份。记住这句话:判断 None,永远用 `is`!
除了这些内置类型的比较,咱们自己定义的类呢?当你创建一个自定义的类:
“`python
class MyObject:
def __init__(self, value):
self.value = value
obj1 = MyObject(10)
obj2 = MyObject(10)
obj3 = obj1
“`
现在,`obj1 == obj2` 会返回什么?默认情况下,自定义类的 `==` 行为和 `is` 是一样的,它只会比较对象的身份。也就是说,`obj1 == obj2` 默认是 `False`,因为 `obj1` 和 `obj2` 是两个不同的对象。但 `obj1 == obj3` 会返回 `True`,因为 `obj3 = obj1` 让 `obj3` 指向了和 `obj1` 同一个对象。
如果你想让你的自定义对象也能像内置类型那样,根据它们内部的值来判断相等,你就需要重载 (override) 特殊方法 `__eq__(self, other)`。这个方法决定了当你使用 `==` 运算符时,你的对象会如何进行比较。`__eq__` 方法应该接收另一个对象 `other` 作为参数,然后根据你的逻辑判断 `self` 和 `other` 的值是否相等,并返回 `True` 或 `False`。
比如,为了让上面的 `MyObject` 类能按值比较:
“`python
class MyObject:
def __init__(self, value):
self.value = value
def __eq__(self, other):
if isinstance(other, MyObject): 确保other也是MyObject类型
return self.value == other.value 比较它们的value属性
return False 如果类型不同,则不相等
obj1 = MyObject(10)
obj2 = MyObject(10)
obj3 = MyObject(20)
print(obj1 == obj2) 现在会输出 True
print(obj1 == obj3) 输出 False
print(obj1 is obj2) 依然输出 False
print(obj1 is obj1) 输出 True
“`
看,通过重载 `__eq__`,我们改变了 `==` 的行为,让它去比较对象的内部属性,而不是默认的身份比较。
类似的,你还可以重载其他比较运算符对应的特殊方法:
– `<` 对应 `__lt__(self, other)` (less than)
– `<=` 对应 `__le__(self, other)` (less than or equal to)
– `>` 对应 `__gt__(self, other)` (greater than)
– `>=` 对应 `__ge__(self, other)` (greater than or equal to)
– `!=` 对应 `__ne__(self, other)` (not equal to)
通常情况下,如果你重载了 `__eq__`,也建议同时重载 `__ne__`。不过,如果没有重载 `__ne__`,Python 会默认使用 `__eq__` 的结果取反。如果你重载了 `__lt__`,通常也会重载其他几个大小比较方法,或者至少重载 `__lt__` 和 `__eq__`,然后使用 `functools.total_ordering` 这个装饰器,它可以根据你实现的这两个方法自动“填补”其他比较方法,省去重复劳动。这都是为了让你的自定义对象在参与比较时,行为更加符合直觉和逻辑。
说到底,Python怎么比较,从最简单的数值、字符串,到复杂的对象,再到身份和值的区分,以及如何为自定义对象定义比较行为,这些都是 Python 编程中绕不开的基础知识。理解它们,才能写出健壮、不出错的代码。别像我当年那样,掉进那些看起来不起眼的小坑里。多实践,多思考,遇到问题翻翻官方文档,或者像现在这样,看看别人是怎么理解和踩坑的。
所以,下次再遇到比较的问题,脑子里要绷紧这几根弦:比的是数值、字符串还是对象? 比的是值还是身份? 对于自定义对象,有没有定义比较的逻辑? 把这几个问题想清楚,很多困惑自然就解开了。Python 的世界,就是这样,简单中藏着精妙,细节里全是魔鬼(或者说,是乐趣)。掌握了这些,你在 Python的比较 之路上,就能走得更稳当啦。
评论(0)