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

About the changing id of an immutable string

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

1
2
3
4
5
6
>>> id('so')
140614155123888
>>> id('so')
140614155123848
>>> id('so')
140614155123808

所以在这期间,它一直在变化。但是,在有一个变量指向该字符串之后,情况会发生变化:

1
2
3
4
5
6
7
8
9
>>> 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内部结构的人不会比我惊讶,所以我正试图达到同样的目的…

更新

用不同的字符串尝试相同的方法会得到不同的结果…

1
2
3
4
5
6
>>> id('hello')
139978087896384
>>> id('hello')
139978087896384
>>> id('hello')
139978087896384

现在它是平等的…


cpython不承诺在默认情况下对字符串进行实习生,但是在实践中,python代码库中的很多地方都会重用已经创建的字符串对象。许多python内部使用(c等价于)intern()函数调用显式地在python字符串中进行内部调用,但除非遇到这些特殊情况,否则两个相同的python字符串文本将产生不同的字符串。

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

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

仅在repl中运行行id(),需要执行几个步骤:

  • 将编译该行,其中包括为字符串对象创建常量:

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

    这将显示已编译字节码的存储常量;在本例中,是字符串'foo'Nonesingleton。

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

    1
    2
    3
    4
    5
    6
    7
    8
    >>> 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
  • 代码对象未被任何对象引用,引用计数降至0,代码对象将被删除。因此,字符串对象也是如此。

  • 然后,如果您重新运行相同的代码,Python可能会为新的字符串对象重用相同的内存位置。如果重复此代码,通常会导致打印相同的内存地址。这取决于您对Python内存的其他操作。

    ID重用是不可预测的;如果垃圾收集器同时运行以清除循环引用,则可以释放其他内存,您将获得新的内存地址。

    接下来,如果Python编译器看起来足够像一个有效的标识符,它还将实习生存储为常量的任何Python字符串。python code对象工厂函数pycode_new将实习仅包含ASCII字母、数字或下划线的任何字符串对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* 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));
    }

    由于您创建了符合该标准的字符串,因此它们被实习生,这就是为什么您在第二个测试中看到相同的ID被用于'so'字符串:只要对实习生版本的引用仍然存在,实习生将导致未来的'so'文本重用实习生字符串对象,即使是在新的代码块中,并且绑定到dif引用标识符。在第一个测试中,您不会保存对字符串的引用,因此在可以重用之前,将丢弃这些被截取的字符串。

    顺便说一下,您的新名称so = 'so'将字符串绑定到包含相同字符的名称。换句话说,您正在创建一个名称和值相等的全局。当python同时实习标识符和限定常量时,最终会为标识符及其值使用相同的字符串对象:

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    >>> 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 peephole优化器确实预先计算简单表达式的结果,但是如果这导致序列长度超过20,则会忽略输出(以防止代码对象膨胀和内存使用);因此,如果结果是20个字符或短字符串,则连接仅由名称字符组成的较短字符串仍然会导致插入字符串。R.


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

    1
    2
    3
    print id('so')
    print id('so')
    print id('so')

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

    1
    2
    3
    2888960
    2888960
    2888960

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
      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是相同的。


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

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


    理解行为的一个更简单的方法是检查以下数据类型和变量。

    "字符串特性"一节以特殊字符为例说明您的问题。


    因此,虽然不能保证python在字符串之间进行交互,但它会经常重用同一个字符串,并且is可能会误导用户。重要的是要知道,你不应该检查idis的字符串是否相等。

    为了证明这一点,我发现了一种在Python2.6中强制使用新字符串的方法:

    1
    2
    3
    4
    >>> so = 'so'
    >>> new_so = '{0}'.format(so)
    >>> so is new_so
    False

    这里还有一点关于Python的探索:

    1
    2
    3
    4
    5
    6
    >>> id(so)
    102596064
    >>> id(new_so)
    259679968
    >>> so == new_so
    True