`is` 运算符在 Python 中使用 __魔法__ 方法吗?

22 投票
2 回答
3013 浏览
提问于 2025-04-17 19:01

is 操作符用来测试对象的身份。

我在想,is 操作符和 id() 函数是否会调用某个 __magic__ 方法,就像 == 会调用 __eq__ 一样。

我尝试查看 __hash__ 的时候发现了一些有趣的事情:

class Foo(object):
    def __hash__(self):
        return random.randint(0, 2 ** 32)

a = Foo()
b = {}
for i in range(5000):
    b[a] = i

想想字典 bb[a] 的值。

每次后续查找 d[a] 要么会出现 KeyError 错误,要么返回一个随机整数。

但是正如文档中关于特殊方法的说明

[默认实现的] x.__hash__() 返回 id(x)。

所以这两者之间确实有关系,但正好是反过来的。

我在这里看到很多关于 isid问题,而且答案帮助了许多困惑的人,但我找不到这个问题的答案。

2 个回答

18

简单来说:不,它们不一样。正如你链接的文档所说:

运算符 isis not 用来测试对象的身份:x is y 只有在 xy 是同一个对象时才为真。

所谓“同一个对象”是不能被你改变的。如果你的对象和另一个对象不是同一个,那它就不能假装是。


那么……为什么呢?允许你重写 is 和/或 id 有什么坏处呢?显然,这几乎总是个愚蠢的做法,但如果你努力的话,Python 允许你做很多愚蠢的事情。

设计的常见问题和类似的文档没有说明。但我猜主要是因为这样可以更容易地调试 Python 和一些更深层的标准库模块,知道在解释器内部有某种方法可以验证两个名字确实指向同一个对象,或者打印出 id 来确保一个名字没有随着时间改变等等。想象一下没有这些功能去调试 weakref 或者 pickle


那么,“同一个对象”到底是什么意思呢?这取决于解释器。显然,在语言层面上必须不可能区分同一个对象的两个实例,可能在解释器层面也是如此(尤其是因为大多数解释器实现都有一个明确的 API 可以接入)。

所有主要的实现都通过在更低层次上遵循身份的概念来处理这个问题。CPython 比较 PyObject* 指针的值,Jython 在 Java 引用上进行身份比较,PyPy 在对象空间对象上执行 is……

值得看看 PyPy 源码,它要求 "x is y 当且仅当 xy 是同一个对象" 在两个方向上都成立。顶层表达式 x is y 只有在适当的对象空间中,无论 wxwy 是什么对象,wy.is_(wx) 为真时才为真,而 is_ 被实现为 wy is wx。因此,x is y 在 N 层时,当且仅当 y is x 在 N-1 层时。


注意,这意味着你可以相对容易地使用 PyPy 构建一个 Python 的方言,在那里 is 可以 被重写,只需将 is_ 附加到一个名为 __is__ 的特殊方法上。但还有更简单的方法来做到这一点:

def is_(x, y):
    if hasattr(x, '__is__'):
        return x.__is__(y)
    elif hasattr(y, '__is__'):
        return y.__is__(x)
    else:
        return x is y

现在使用 is_(x, y) 而不是 x is y,看看你能否在修改解释器的艰苦工作之前找到一些有趣的问题(即使在这种情况下,这并不那么困难)。


那么,isid 有什么关系呢?is 可以在 id 的基础上实现吗——例如,x is y 只是检查 id(x) == id(y)?好吧,id

返回一个对象的“身份”。这是一个在对象的生命周期内保证唯一且恒定的整数。两个生命周期不重叠的对象可能会有相同的 id() 值。

所以,一个对象的 id 在其生命周期内是唯一且恒定的,而 x is y 只有在它们是同一个对象时才为真,因此 x is y 只有在 id(x) == id(y) 时才为真,对吧?

其实,id 可以被重新绑定到你想要的任何东西,而这不允许影响 is。如果你非常小心地构造定义(请记住,如果你丢弃了对 builtinsid 的引用,之前的实现甚至不保证仍然存在,或者如果存在也不保证正常工作……),你 可以 在默认的 id 实现上定义 is

但这样做会很奇怪。在 CPython 中,id(x) 只是“返回对象在内存中的地址”,这与指向对象的指针的值是一样的。但这只是 CPython 的一个特性;并没有规定其他实现必须让 id 返回用于身份比较的基础值作为整数。实际上,在一个没有指针(可以转换为整数)的语言中,你甚至不清楚该如何做到这一点。在 PyPy 中,一个对象的 id 甚至可能是第一次访问时计算出的值,并存储在对象空间中的一个字典中,以对象本身为键。


至于 __hash__,你误解了文档中的一个重要部分。

[...] x.__hash__() 返回 id(x)

你省略的部分明确表示这仅对用户定义的类的实例(没有重定义 __hash__)成立。显然,对于 tuple 等对象并不成立。简而言之,身份与哈希没有关系,除了对于某些对象,身份提供了一个方便的哈希值。

25

不,is 是直接比较两个指针,而 id 只是返回对象的地址,并把这个地址转成了 long 类型。

来自 ceval.c 的内容:

case PyCmp_IS:
    res = (v == w);
    break;
case PyCmp_IS_NOT:
    res = (v != w);
    break;

这里的 vw 只是 PyObject * 类型的指针。

来自 bltinmodule.c 的内容:

static PyObject *
builtin_id(PyObject *self, PyObject *v)
{
    return PyLong_FromVoidPtr(v);
}

PyDoc_STRVAR(id_doc,
"id(object) -> integer\n\
\n\
Return the identity of an object. This is guaranteed to be unique among\n\
simultaneously existing objects. (Hint: it's the object's memory address.)");

撰写回答