Python 中的 magic int

TL;DR: [-5, 257) 范围内的整数,CPython 做了优化,内存中只会有一份,使用单例模式


在 Python 中判断对象相等性时,会经常使用 ==is 两种判断方法

is 如其字面意思,判断两个对象是不是同一个对象,比较对象的内存地址,相当于调用 hex(id(a)) == hex(id(b))

对于 == 而言,一般继承自 object 的对象默认都是比较的内存地址,和 is 没有区别 但是对象内部实现时可以通过覆盖 __eq__ 方法,自定义想要的比较效果:

class Person:
	def __init__(self, name, age):
    	self.name = name
    	self.age = age

	def __eq__(self, other):
    	return abs(self.age - other.age) < 1

	def get_info(self):
    	return self.name, self.age

Person.__eq__ = lambda x, y: abs(x.age - y.age) < 1
a = Person("Dan", 17.5)
b = Person("Link", 18)
print(a == b)      # turns out True

也许有些时候对象是封装得比较好的,并不方便修改,这种时候可以通过 lambda 表达式修改对象方法达到同样的效果:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def get_info(self):
        return self.name, self.age

Person.__eq__ = lambda x, y: abs(x.age - y.age) < 1
a = Person("Dan", 17.5)
b = Person("Link", 18)
print(a == b)

比如在 LeetCode 刷题时,ListNode.__lt__ = lambda a, b: a.val < b.val 然后就可以直接 mode_list.sort() 排序

is 除了保持代码的可读性之外,按照 PEP8 的建议,也经常用来比较单例对象,最常见的就是 if node is None:

了解以上内容,其实可以尝试回答一下比较诡异的一个问题了:

In [1]: a = 420
In [2]: b = 420
In [3]: a is b      # shit happens
Out[3]: False
In [4]: a == b
Out[4]: True

其实是 Python 的实现上的一个优化 首先 Python 已经没有 int 类型了,使用的都是 long 型,相比于原来的 int 类型,占用了 2 倍的空间 所以对于使用比较频繁的比较小的整数,做成了单例的形式。 可以参考 CPython longobject 的实现

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
/* Small integers are preallocated in this array so that they
   can be shared.
   The integers that are preallocated are those in the range
   -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/
static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

[-5, 257) 范围内的整数,内存中只会有一份,不会重复分配内存

对于之前的测试代码,a 与 b 已经超出范围,所以占用了不同的内存地址,is 判断为 False; 但是 __eq__ 实现中,比较的是值的相等性,判断为 True