“is”运算符对整数的行为异常

2024-11-18 08:41:00
admin
原创
13
摘要:问题描述:为什么下面的行为在 Python 中会出现意外?>>> a = 256 >>> b = 256 >>> a is b True # This is an expected result >>> a = 257...

问题描述:

为什么下面的行为在 Python 中会出现意外?

>>> a = 256
>>> b = 256
>>> a is b
True           # This is an expected result
>>> a = 257
>>> b = 257
>>> a is b
False          # What happened here? Why is this False?
>>> 257 is 257
True           # Yet the literal numbers compare properly

我使用的是 Python 2.5.2。尝试了一些不同版本的 Python,似乎 Python 2.3.3 在 99 到 100 之间表现出上述行为。

基于以上内容,我可以假设 Python 内部实现的方式是“小”整数的存储方式与大整数不同,并且is运算符可以分辨出差异。为什么要使用泄漏抽象?当我事先不知道两个任意对象是否是数字时,有什么更好的方法来比较它们是否相同?


解决方案 1:

看看这个:

>>> a = 256
>>> b = 256
>>> id(a) == id(b)
True
>>> a = 257
>>> b = 257
>>> id(a) == id(b)
False

以下是我在“普通整数对象”文档中找到的内容:

当前实现为介于-5和之间的所有整数保留一个整数对象数组256。当您在该范围内创建一个 int 时,您实际上只是返回对现有对象的引用。

因此,整数 256 是相同的,但 257 不相同。这是 CPython 实现细节,不保证其他 Python 实现也是如此。

解决方案 2:

Python 的“is”运算符对整数的行为异常吗?

总而言之——让我强调一下:不要用它is来比较整数。

您不应该对此抱有任何期望。

相反,使用==!=分别比较相等和不相等。例如:

>>> a = 1000
>>> a == 1000       # Test integers like this,
True
>>> a != 5000       # or this!
True
>>> a is 1000       # Don't do this! - Don't use `is` to test integers!!
False

解释

要了解这一点,您需要了解以下内容。

首先,它起什么作用is?它是一个比较运算符。根据文档:

运算符isis not测试对象身份:x is y当且仅当 x 和 y 是同一个对象时才为真。x is not y产生逆真值。

因此以下内容是等效的。

>>> a is b
>>> id(a) == id(b)

来自文档:

id
返回对象的“标识”。这是一个整数(或长整数),保证在该对象的生命周期内唯一且恒定。两个生命周期不重叠的对象可能具有相同的id()值。

请注意,CPython(Python 的参考实现)中对象的 id 是内存中的位置,这是一个实现细节。其他 Python 实现(如 Jython 或 IronPython)可能很容易拥有不同的实现id

那么用例是什么is? PEP8 描述:

与单例的比较应该总是用或
None来完成,而永远不要用相等运算符。is`is not`

问题

您提出并陈述以下问题(附代码):

为什么下面的行为在 Python 中会出现意外?

>>> a = 256
>>> b = 256
>>> a is b
True           # This is an expected result

不是256预期的结果。为什么这是预期的?这仅意味着和引用的整数值是整数的同一个实例。整数在 Python 中是不可变的ab因此它们不能改变。这应该不会对任何代码产生影响。这不应该是预期的。这只是一个实现细节。

但也许我们应该庆幸,每次我们声明一个值等于 256 时,内存中并没有一个新的单独实例。

>>> a = 257
>>> b = 257
>>> a is b
False          # What happened here? Why is this False?

看起来我们现在在内存中有两个单独的整数实例,其值为257。由于整数是不可变的,这会浪费内存。希望我们没有浪费太多内存。我们可能不会。但这种行为并不能保证。

>>> 257 is 257
True           # Yet the literal numbers compare properly

好吧,这看起来你的 Python 特定实现正在努力变得聪明,除非必要,否则不会在内存中创建冗余值的整数。你似乎表明你正在使用 Python 的引用实现,即 CPython。对 CPython 来说很好。

如果 CPython 可以全局执行此操作,并且能够以低成本执行此操作(因为查找会产生成本),那么可能会更好,也许另一种实现就可以了。

但至于对代码的影响,您不应该关心整数是否是整数的特定实例。您只应该关心该实例的值是什么,并且您可以使用正常的比较运算符,即==

什么is

is检查id两个对象的 是否相同。在 CPython 中,id是内存中的位置,但在其他实现中,它可能是其他唯一标识号。用代码重述这一点:

>>> a is b

等同于

>>> id(a) == id(b)

那么我们为什么要使用它is呢?

相对于检查两个非常长的字符串的值是否相等,这可能是一种非常快速的检查。但由于它适用于对象的唯一性,因此我们对它的用例有限。事实上,我们主要想用它来检查None,它是一个单例(内存中一个地方存在的唯一实例)。如果有可能将它们合并,我们可能会创建其他单例,我们可能会用 来检查is,但这些相对罕见。这是一个示例(适用于 Python 2 和 3)例如

SENTINEL_SINGLETON = object() # this will only be created one time.

def foo(keyword_argument=None):
    if keyword_argument is None:
        print('no argument given to foo')
    bar()
    bar(keyword_argument)
    bar('baz')

def bar(keyword_argument=SENTINEL_SINGLETON):
    # SENTINEL_SINGLETON tells us if we were not passed anything
    # as None is a legitimate potential argument we could get.
    if keyword_argument is SENTINEL_SINGLETON:
        print('no argument given to bar')
    else:
        print('argument to bar: {0}'.format(keyword_argument))

foo()

打印内容:

no argument given to foo
no argument given to bar
argument to bar: None
argument to bar: baz

因此,我们看到,使用is和 标记,我们能够区分何时bar不带参数调用 和何时使用 调用None。这些是 的主要用例is-不要使用它来测试整数、字符串、元组或其他类似内容是否相等。

解决方案 3:

我迟到了,但你想要答案的来源吗?我会尝试用介绍性的方式表达,以便更多人可以理解。


CPython 的一个优点是您可以实际查看源代码。我将使用3.5版本的链接,但找到相应的2.x版本并不难。

在 CPython 中,处理创建新对象的C-APIint函数是PyLong_FromLong(long v)。此函数的描述为:

当前实现为 -5 到 256 之间的所有整数保留一个整数对象数组,当您在该范围内创建一个 int 时,实际上您只是返回对现有对象的引用。因此应该可以更改 1 的值。我怀疑 Python 在这种情况下的行为是未定义的。:-)

(斜体字是我加的)

我不知道你是怎么想的,但是当我看到这个时,我想:让我们找到那个数组!

如果你还没有弄过实现 CPython 的 C 代码,你应该看看;一切都井然有序且易读。对于我们的情况,我们需要查看主源代码目录树的Objects子目录。

PyLong_FromLong处理long对象,因此不难推断我们需要查看内部longobject.c。查看内部后,您可能会认为事情很混乱;确实如此,但不要害怕,我们正在寻找的函数位于第 230 行,等待我们检查。这是一个较小的函数,因此主体(不包括声明)很容易粘贴在此处:

PyObject *
PyLong_FromLong(long ival)
{
    // omitting declarations

    CHECK_SMALL_INT(ival);

    if (ival < 0) {
        /* negate: cant write this as abs_ival = -ival since that
           invokes undefined behaviour when ival is LONG_MIN */
        abs_ival = 0U-(unsigned long)ival;
        sign = -1;
    }
    else {
        abs_ival = (unsigned long)ival;
    }

    /* Fast path for single-digit ints */
    if (!(abs_ival >> PyLong_SHIFT)) {
        v = _PyLong_New(1);
        if (v) {
            Py_SIZE(v) = sign;
            v->ob_digit[0] = Py_SAFE_DOWNCAST(
                abs_ival, unsigned long, digit);
        }
        return (PyObject*)v; 
}

现在,我们不是 C语言大师,也不是代码狂人,但我们也不傻,我们可以看到,这CHECK_SMALL_INT(ival);一切都在诱惑着我们;我们可以理解这与此有关。让我们来看看:

#define CHECK_SMALL_INT(ival) \n    do if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) { \n        return get_small_int((sdigit)ival); \n    } while(0)

get_small_int因此,如果值ival满足条件,则它是一个调用函数的宏:

if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS)

那么什么是 ?NSMALLNEGINTSNSMALLPOSINTS? 宏!它们如下

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif

所以我们的条件是if (-5 <= ival && ival < 257)呼叫get_small_int

接下来让我们看看get_small_int它的全部魅力(好吧,我们只看它的主体,因为那里才是有趣的地方):

PyObject *v;
assert(-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS);
v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
Py_INCREF(v);

好的,声明一个PyObject,断言前面的条件成立并执行赋值:

v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];

small_ints看起来很像我们一直在寻找的那个数组,确实如此!我们本来可以阅读该死的文档,然后我们就会知道一切!

/* 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];

是的,这就是我们要找的人。当您想int在范围内创建新对象时[NSMALLNEGINTS, NSMALLPOSINTS),您只会得到对已预先分配的现有对象的引用。

由于引用指向同一个对象,因此id()直接发出或检查其身份is将返回完全相同的内容。

但是,什么时候分配它们呢?

_PyLong_InitPython 初始化期间将很乐意进入一个 for 循环来为您执行此操作:

for (ival = -NSMALLNEGINTS; ival <  NSMALLPOSINTS; ival++, v++) {

查看源代码来阅读循环主体!

我希望我的解释现在已经让你清楚了C 的事情(显然是双关语)。


但是,257 is 257? 发生什么事了?

这实际上更容易解释,而且我已经尝试这样做了;这是因为 Python 将把这个交互式语句作为单个块来执行:

>>> 257 is 257

在编译此语句期间,CPython 将看到您有两个匹配的文字,并使用相同的PyLongObject表示257。如果您自己进行编译并检查其内容,就会看到这一点:

>>> codeObj = compile("257 is 257", "blah!", "exec")
>>> codeObj.co_consts
(257, None)

当 CPython 执行操作时,它现在只会加载完全相同的对象:

>>> import dis
>>> dis.dis(codeObj)
  1           0 LOAD_CONST               0 (257)   # dis
              3 LOAD_CONST               0 (257)   # dis again
              6 COMPARE_OP               8 (is)

所以is会回来True

解决方案 4:

这取决于您是否想查看两个事物是否相等,或者是否是同一个对象。

is检查它们是否是同一个对象,而不仅仅是相等。小整数可能指向相同的内存位置,以提高空间效率

In [29]: a = 3
In [30]: b = 3
In [31]: id(a)
Out[31]: 500729144
In [32]: id(b)
Out[32]: 500729144

您应该使用来比较任意对象的相等性。您可以使用、 和属性==指定行为。__eq__`__ne__`

解决方案 5:

正如您在源文件intobject.c中看到的,Python 缓存小整数以提高效率。每次创建对小整数的引用时,您引用的是缓存的小整数,而不是新对象。257 不是小整数,因此它被计算为不同的对象。

最好用于==该目的。

解决方案 6:

我认为你的假设是正确的。用id(对象身份)进行实验:

In [1]: id(255)
Out[1]: 146349024

In [2]: id(255)
Out[2]: 146349024

In [3]: id(257)
Out[3]: 146802752

In [4]: id(257)
Out[4]: 148993740

In [5]: a=255

In [6]: b=255

In [7]: c=257

In [8]: d=257

In [9]: id(a), id(b), id(c), id(d)
Out[9]: (146349024, 146349024, 146783024, 146804020)

看起来数字<= 255被视为文字,并且任何高于数字的内容都会被区别对待!

解决方案 7:

还有一个问题,现有的答案都没有指出。Python 可以合并任何两个不可变值,而预先创建的小 int 值并不是发生这种情况的唯一方式。Python 实现永远不能保证这样做,但它们都不仅仅针对小 int 这样做。


首先,还有一些其他预先创建的值,例如空的tuplestrbytes,以及一些短字符串(在 CPython 3.6 中,是 256 个单字符 Latin-1 字符串)。例如:

>>> a = ()
>>> b = ()
>>> a is b
True

但即使非预先创建的值也可能相同。请考虑以下示例:

>>> c = 257
>>> d = 257
>>> c is d
False
>>> e, f = 258, 258
>>> e is f
True

这不仅限于int价值观:

>>> g, h = 42.23e100, 42.23e100
>>> g is h
True

显然,CPython 没有预先创建 的float42.23e100。那么,这里发生了什么?

CPython 编译器会将某些已知不可变类型(如intfloatstrbytes)的常量值合并到同一个编译单元中。对于模块,整个模块是一个编译单元,但在交互式解释器中,每个语句都是一个单独的编译单元。由于cd是在单独的语句中定义的,因此它们的值不会合并。由于ef是在同一个语句中定义的,因此它们的值会合并。


你可以通过反汇编字节码来查看发生了什么。尝试定义一个函数e, f = 128, 128,然后调用dis.dis它,你会看到有一个常量值(128, 128)

>>> def f(): i, j = 258, 258
>>> dis.dis(f)
  1           0 LOAD_CONST               2 ((128, 128))
              2 UNPACK_SEQUENCE          2
              4 STORE_FAST               0 (i)
              6 STORE_FAST               1 (j)
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE
>>> f.__code__.co_consts
(None, 128, (128, 128))
>>> id(f.__code__.co_consts[1], f.__code__.co_consts[2][0], f.__code__.co_consts[2][1])
4305296480, 4305296480, 4305296480

您可能会注意到,即使字节码实际上并未使用它,编译器也会将其存储128为常量,这让您了解 CPython 的编译器所做的优化有多么少。这意味着(非空)元组实际上并没有合并:

>>> k, l = (1, 2), (1, 2)
>>> k is l
False

将其放入一个函数中,dis然后查看co_consts——有一个1和一个2,两个(1, 2)共享相同12但不相同的元组,以及一个((1, 2), (1, 2))具有两个不同相等元组的元组。


CPython 还做了一项优化:字符串驻留。与编译器常量折叠不同,这不仅限于源代码文字:

>>> m = 'abc'
>>> n = 'abc'
>>> m is n
True

另一方面,它仅限于类型和内部存储类型“ascii compact”、“compact”或“legacy ready”str的字符串,并且在许多情况下只有“ascii compact”会被保留。


无论如何,对于值必须是、可以是或不能是不同的规则在不同的实现中是不同的,在同一实现的不同版本之间也是不同的,甚至在同一实现的同一副本上运行的同一代码之间也是不同的。

为了好玩,学习特定 Python 的规则是值得的。但不值得在代码中依赖它们。唯一安全的规则是:

  • 不要编写假设两个相等但单独创建的不可变值相同的代码(不要使用x is y,而要使用x == y

  • 不要编写假设两个相等但单独创建的不可变值是不同的代码(不要使用x is not y,而要使用x != y

或者换句话说,仅使用is来测试已记录的单例(如None)或仅在代码中的一个地方创建的单例(如_sentinel = object()成语)。

解决方案 8:

对于不可变的值对象(如整数、字符串或日期时间),对象标识并不是特别有用。最好考虑相等性。标识本质上是值对象的实现细节 - 因为它们是不可变的,所以对同一对象或多个对象有多个引用之间没有实际区别。

解决方案 9:

is 恒等相等运算符(功能类似id(a) == id(b));只是两个相等的数字不一定是同一个对象。出于性能原因,一些小整数恰好被记忆,因此它们往往相同(这是可以做到的,因为它们是不可变的)。

===另一方面,PHP 的x == y and type(x) == type(y)运算符被描述为检查相等性和类型:根据 Paulo Freitas 的评论。这对于普通数字来说已经足够了,但与以荒谬方式is定义的类不同:__eq__

class Unequal:
    def __eq__(self, other):
        return False

PHP 显然允许“内置”类(我认为是指在 C 级别实现,而不是在 PHP 中)实现相同的功能。一个稍微不那么荒谬的用法可能是计时器对象,每次用作数字时,它的值都会不同。我不知道你为什么要模拟 Visual Basic,Now而不是显示它是求值。time.time()

Greg Hewgill (OP) 做了一个澄清评论“我的目标是比较对象身份,而不是价值相等。除了数字,我希望将对象身份视为与价值相等。”

这又会有另一个答案,因为我们必须将事物归类为数字或不归类为数字,以选择是否与==或进行比较is。CPython定义了数字协议,包括 PyNumber_Check,但这无法从 Python 本身访问。

我们可以尝试使用isinstance我们已知的所有数字类型,但这必然是不完整的。类型模块包含一个 StringTypes 列表,但没有 NumberTypes。从 Python 2.6 开始,内置的数字类有一个基类numbers.Number,但它有同样的问题:

import numpy, numbers
assert not issubclass(numpy.int16,numbers.Number)
assert issubclass(int,numbers.Number)

顺便说一句,NumPy将生成单独的低数字实例。

我实际上不知道这个问题的答案。我想理论上可以使用 ctypes 来调用PyNumber_Check,但即使这个函数也存在争议,而且它肯定不可移植。我们现在必须对测试的内容不那么挑剔。

归根结底,这个问题源于 Python 最初没有像Scheme number?或Haskell 的 类型类 Num那样带有谓词的类型树。is它检查对象身份,而不是值相等性。PHP 也有着丰富多彩的历史,在PHP5 中,似乎只对对象===表现为,但在 PHP4 中则不是。这就是跨语言(包括一种语言的版本)迁移的成长烦恼。is

解决方案 10:

字符串中也会发生这种情况:

>>> s = b = 'somestr'
>>> s == b, s is b, id(s), id(b)
(True, True, 4555519392, 4555519392)

现在一切似乎都很好。

>>> s = 'somestr'
>>> b = 'somestr'
>>> s == b, s is b, id(s), id(b)
(True, True, 4555519392, 4555519392)

这也是意料之中的。

>>> s1 = b1 = 'somestrdaasd ad ad asd as dasddsg,dlfg ,;dflg, dfg a'
>>> s1 == b1, s1 is b1, id(s1), id(b1)
(True, True, 4555308080, 4555308080)

>>> s1 = 'somestrdaasd ad ad asd as dasddsg,dlfg ,;dflg, dfg a'
>>> b1 = 'somestrdaasd ad ad asd as dasddsg,dlfg ,;dflg, dfg a'
>>> s1 == b1, s1 is b1, id(s1), id(b1)
(True, False, 4555308176, 4555308272)

这真是出乎意料。

解决方案 11:

Python 3.8 中的新功能:Python 行为的变化:

当身份检查(和
)与某些类型的文字(例如字符串、整数)一起使用时,编译器现在会产生SyntaxWarning。这些在 CPython 中经常会意外起作用,但语言规范并不保证。警告建议用户改用相等性测试(
和)。is`is not==!=`

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用