当前位置:  开发笔记 > 编程语言 > 正文

"是"运算符与整数意外行为

如何解决《"是"运算符与整数意外行为》经验,为你挑选了9个好方法。

为什么以下在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> Cybis..:

看看这个:

>>> a = 256
>>> b = 256
>>> id(a)
9987148
>>> id(b)
9987148
>>> a = 257
>>> b = 257
>>> id(a)
11662816
>>> id(b)
11662828

编辑:这是我在Python 2文档中找到的,"普通整数对象"(Python 3也是如此):

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


有谁知道如何选择范围(-5,256)?如果它是(0,255)或甚至(-255,255),我不会太惊讶,但从-5开始的262个数字的范围似乎令人惊讶地随意.
根据https://www.reddit.com/r/Python/comments/18leav/python_integer_range_5_256_and_identity_comparison/c8iq3c4/,过去的范围是[-5,100].它被扩展为包括全范围的字节值 - 加上256,因为这可能是一个常见的数字.
@WoodrowBarlow:我认为-5只是一种启发式来捕获常见的负占位符.0..255涵盖单字节值的数组.它是神秘的256,但我猜这是(dis)将整数组装成字节.
据我了解,范围是通过查看多个项目(和多种语言)的常用值来选择的。

2> Aaron Hall..:
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中对象的id(Python的参考实现)是内存中的位置这一事实是一个实现细节.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双方引用的ab是整数的同一个实例.整数在Python中是不可变的,因此它们无法改变.这应该对任何代码都没有影响.不应该这样.它只是一个实现细节.

但也许我们应该感到高兴的是,每当我们声明一个值等于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> JimB..:

这取决于你是否想要看两件事是否相同,或者是同一个对象.

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

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

您应该==用来比较任意对象的相等性.您可以使用__eq____ne__属性指定行为.



4> Jim Fasaraki..:

我迟到了,你想要一些消息来源吗?*

关于CPython的好处是你可以真正看到它的来源.我现在要使用链接进行int发布; 找到相应PyLong_FromLong(long v)的是微不足道的.

在CPython中,Objects处理创建新PyLong_FromLong对象的函数是long.该功能的描述是:

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

不知道你,但我看到了这一点并想:让我们找到那个阵列!

如果你还没有与拨弄longobject.c实现CPython的代码,你应该,一切都非常有组织性和可读性.对于我们而言,我们需要在看CHECK_SMALL_INT(ival);子目录中的主源代码目录树.

get_small_int处理ival对象因此不难推断出我们需要窥视内部NSMALLNEGINTS.看完后你可能会认为事情很混乱; 他们是,但不要担心,我们正在寻找的功能让我们NSMALLPOSINTS等待我们查看它.这是一个小功能,所以主体(不包括声明)很容易粘贴在这里:

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; 
}

现在,我们不是if (-5 <= ival && ival < 257) 主码 - haxxorz,但我们也不傻,我们可以看到get_small_int所有人都在诱惑地偷看我们; 我们可以理解它与此有关.让我们来看看:

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

所以它是一个宏,get_small_int如果值PyObject满足条件则调用函数:

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

什么是small_intsint?如果你猜到了宏,你什么也得不到,因为这不是一个很难的问题.无论如何,这里它们是:

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

所以我们的条件是[NSMALLNEGINTS, NSMALLPOSINTS)电话id().

没有其他的地方可以继续我们的旅程,看看is它的所有荣耀(好吧,我们只是看看它的身体,因为这是有趣的事情):

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

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

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

257 is 257看起来很像我们一直在寻找的阵列..而且,它是!我们可以只阅读该死的文档,我们一直都知道!:

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

所以,是的,这是我们的家伙.如果要PyLongObject在范围内创建新的,则257只需返回对已预先分配的现有对象的引用.

由于引用引用同一个对象,因此is直接发布或检查其身份True将返回完全相同的内容.

但是,什么时候分配?

在初始化期间,int Python会很乐意进入for循环,为你做这个:

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

我希望我的解释现在可以PyLong_FromLong(long v)清楚地表明你(双关语显然是有意的).


但是,257是257?这是怎么回事?

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

>>> 257 is 257

作为单个块.在对此声明进行编译时,CPython将看到您有两个匹配的文字,并将使用相同的Objects表示PyLong_FromLong.如果您自己编译并检查其内容,您可以看到这个:

>>> 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)

所以long会回来longobject.c.


* - 我会尝试用更多的介绍方式来说明这一点,以便大多数人能够遵循.



5> Angel..:

由于您可以检查源文件intobject.c,因此Python会缓存小整数以提高效率.每次创建对小整数的引用时,都指的是缓存的小整数,而不是新对象.257不是一个小整数,因此它被计算为一个不同的对象.

最好是==为此目的使用.



6> Amit..:

我认为你的假设是正确的.试验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> babbageclunk..:

对于不可变值对象,如整数,字符串或日期时间,对象标识不是特别有用.考虑平等更好.身份本质上是值对象的实现细节 - 因为它们是不可变的,所以对同一个对象或多个对象进行多次引用之间没有任何有效的区别.



8> Yann Vernier..:

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我们所知道的所有数字类型,但这不可避免地是不完整的.types模块包含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也有丰富多彩的历史,其中===显然is只表现在PHP5中的对象,而不是PHP4.这是跨越语言(包括版本的一种)的不断增长的痛苦.



9> abarnert..:

现有答案中都没有指出另一个问题。允许Python合并任何两个不可变的值,并且预先创建的小int值不是发生这种情况的唯一方法。永远不能保证 Python实现能够做到这一点,但是他们所做的不仅仅只是小的整数。


一方面,还有一些其他预先创建的值,例如empty 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-there是一个12两个(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的规则。但是在代码中不值得依赖它们。唯一安全的规则是:

不要编写假定两个相等但分别创建的不可变值相同的代码。

不要编写假定两个相等但分别创建的不可变值是不同的代码。

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

推荐阅读
小色米虫_524
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有