关于更改不可变字符串的id

时间:2014-06-16 13:50:08

标签: python string python-2.7 immutability python-internals

关于id类型str对象的某些内容(在python 2.7中)让我感到困惑。 str类型是不可变的,所以我希望一旦创建它,​​它将始终具有相同的id。我相信我不会说自己这么好,所以我会发布一个输入和输出序列的例子。

>>> id('so')
140614155123888
>>> id('so')
140614155123848
>>> id('so')
140614155123808

所以同时,它一直在变化。但是,在指向该字符串的变量之后,事情会发生变化:

>>> so = 'so'
>>> id('so')
140614155123728
>>> so = 'so'
>>> id(so)
140614155123728
>>> not_so = 'so'
>>> id(not_so)
140614155123728

一旦变量保存了该值,它就会冻结id。实际上,在del sodel not_so之后,id('so')的输出会再次开始变化。

与(小)整数的行为相同。

我知道不变性与同一id之间没有真正的联系;仍然,我试图弄清楚这种行为的来源。我相信熟悉python内部的人不会比我更惊讶,所以我试图达到同样的目的......

更新

尝试使用不同的字符串会产生不同的结果......

>>> id('hello')
139978087896384
>>> id('hello')
139978087896384
>>> id('hello')
139978087896384

现在 相等......

5 个答案:

答案 0 :(得分:59)

CPython默认情况下承诺实习字符串,但实际上,Python代码库中的很多地方都会重用已经创建的字符串对象。许多Python内部使用{C-equivalent of} intern() function call来显式实习Python字符串,但除非你遇到其中一个特殊情况,否则两个相同的Python字符串文字将产生不同的字符串。

Python也可以自由地重用内存位置,Python也可以通过在编译时将代码对象中的字节码存储一次来优化不可变的文字。 Python REPL(交互式解释器)还将最新的表达式结果存储在_名称中,这使得事情更加混乱。

因此,您 会不时看到相同的ID。

在REPL中仅运行id(<string literal>)行会经过几个步骤:

  1. 编译该行,包括为字符串对象创建一个常量:

    >>> compile("id('foo')", '<stdin>', 'single').co_consts
    ('foo', None)
    

    这显示了存储的常量和编译的字节码;在这种情况下,字符串'foo'None单例。

  2. 执行时,字符串从代码常量加载,id()返回内存位置。生成的int值绑定到_,并打印:

    >>> import dis
    >>> dis.dis(compile("id('foo')", '<stdin>', 'single'))
      1           0 LOAD_NAME                0 (id)
                  3 LOAD_CONST               0 ('foo')
                  6 CALL_FUNCTION            1
                  9 PRINT_EXPR          
                 10 LOAD_CONST               1 (None)
                 13 RETURN_VALUE        
    
  3. 任何内容都不引用代码对象,引用计数降为0并删除代码对象。因此,字符串对象也是如此。

  4. 如果重新运行相同的代码,Python可以或许为新的字符串对象重用相同的内存位置。如果重复此代码,这通常会导致打印相同的内存地址。 这取决于您使用Python内存做了什么

    ID重用可预测;如果在此期间垃圾收集器运行以清除循环引用,则可以释放其他内存,并且您将获得新的内存地址。

    接下来,Python编译器还将实例化存储为常量的任何Python字符串,前提是它看起来像一个有效的标识符。 Python code object factory function PyCode_New将实习只包含ASCII字母,数字或下划线的任何字符串对象:

    /* Intern selected string constants */
    for (i = PyTuple_Size(consts); --i >= 0; ) {
        PyObject *v = PyTuple_GetItem(consts, i);
        if (!PyString_Check(v))
            continue;
        if (!all_name_chars((unsigned char *)PyString_AS_STRING(v)))
            continue;
        PyString_InternInPlace(&PyTuple_GET_ITEM(consts, i));
    }
    

    由于您创建了符合该条件的字符串,因此它们被实习,这就是为什么您在第二次测试中看到'so'字符串使用相同的ID的原因:只要对实习版本的引用存活, interning将导致未来的'so'文字重用被拦截的字符串对象,即使在新的代码块中并绑定到不同的标识符。在第一次测试中,您不保存对字符串的引用,因此在重用之前会丢弃实例化的字符串。

    顺便提一句,您的新名称so = 'so'会将字符串绑定到包含相同字符的名称。换句话说,您正在创建一个名称和值相等的全局。由于Python实例化了标识符和限定常量,因此最终对标识符及其值使用相同的字符串对象:

    >>> compile("so = 'so'", '<stdin>', 'single').co_names[0] is compile("so = 'so'", '<stdin>', 'single').co_consts[0]
    True
    

    如果您创建的字符串不是代码对象常量,或者包含字母+数字+下划线范围之外的字符,您将看到id()值未被重用:

    >>> some_var = 'Look ma, spaces and punctuation!'
    >>> some_other_var = 'Look ma, spaces and punctuation!'
    >>> id(some_var)
    4493058384
    >>> id(some_other_var)
    4493058456
    >>> foo = 'Concatenating_' + 'also_helps_if_long_enough'
    >>> bar = 'Concatenating_' + 'also_helps_if_long_enough'
    >>> foo is bar
    False
    >>> foo == bar
    True
    

    Python窥孔优化器会预先计算简单表达式的结果,但如果这导致序列长于20,则忽略输出(以防止膨胀的代码对象和内存使用);如果结果是20个字符或更短的话,那么连接只包含名称字符的短字符串 can 仍然会导致字符串。

答案 1 :(得分:4)

此行为特定于Python交互式shell。如果我将以下内容放在.py文件中:

print id('so')
print id('so')
print id('so')

并执行它,我收到以下输出:

2888960
2888960
2888960

在CPython中,字符串文字被视为常量,我们可以在上面代码片段的字节码中看到:

  2           0 LOAD_GLOBAL              0 (id)
              3 LOAD_CONST               1 ('so')
              6 CALL_FUNCTION            1
              9 PRINT_ITEM          
             10 PRINT_NEWLINE       

  3          11 LOAD_GLOBAL              0 (id)
             14 LOAD_CONST               1 ('so')
             17 CALL_FUNCTION            1
             20 PRINT_ITEM          
             21 PRINT_NEWLINE       

  4          22 LOAD_GLOBAL              0 (id)
             25 LOAD_CONST               1 ('so')
             28 CALL_FUNCTION            1
             31 PRINT_ITEM          
             32 PRINT_NEWLINE       
             33 LOAD_CONST               0 (None)
             36 RETURN_VALUE  

相同的常量(即相同的字符串对象)被加载3次,因此ID是相同的。

答案 2 :(得分:1)

在第一个示例中,每次都会创建一个新的字符串'so'实例,因此ID不同。

在第二个示例中,您将字符串绑定到变量,然后Python可以维护字符串的共享副本。

答案 3 :(得分:0)

因此,虽然Python不能保证实习字符串,但它经常会重复使用相同的字符串,而is可能会误导。重要的是要知道不应该检查idis字符串是否相等。

为了证明这一点,我发现了一种至少在Python 2.6中强制使用新字符串的方法:

>>> so = 'so'
>>> new_so = '{0}'.format(so)
>>> so is new_so 
False

这里有一些Python探索:

>>> id(so)
102596064
>>> id(new_so)
259679968
>>> so == new_so
True

答案 4 :(得分:0)

了解行为的更简单方法是检查以下Data Types and Variables

Section&#34; A String Pecularity&#34;以特殊字符为例说明您的问题。