Python 字符串身份悖论

Python 字符串身份悖论,python,string,python-internals,Python,String,Python Internals,我完全被这件事缠住了 >>> s = chr(8263) >>> x = s[0] >>> x is s[0] False 这怎么可能?这是否意味着通过索引访问字符串会创建同一字符的新实例?让我们做个实验: >>> L = [s[0] for _ in range(1000)] >>> len(set(L)) 1 >>> ids = map(id, L) >>> len

我完全被这件事缠住了

>>> s = chr(8263)
>>> x = s[0]
>>> x is s[0]
False
这怎么可能?这是否意味着通过索引访问字符串会创建同一字符的新实例?让我们做个实验:

>>> L = [s[0] for _ in range(1000)]
>>> len(set(L))
1
>>> ids = map(id, L)
>>> len(set(ids))
1000
>>>
哎呀,真是浪费字节;)还是说
str.\uuuu getitem\uuuu
有一个隐藏的特性?有人能解释一下吗

但这并不是我惊讶的结束:

>>> s = chr(8263)
>>> t = s
>>> print(t is s, id(t) == id(s))
True True
这一点很清楚:
t
s
的别名,因此它们表示相同的对象,并且标识一致。但同样,如何实现以下目标:

>>> print(t[0] is s[0])
False
s
t
是同一个对象,那又怎样

但更糟糕的是:

>>> print(id(t[0]) == id(s[0]))
True

t[0]
s[0]
未被垃圾收集,被
is
操作符视为同一对象,但ID不同?有人能解释一下吗?

是比较身份和比较值。检查这个

每个对象都有一个标识、一个类型和一个值。物体的身份 一旦被创造,就永远不会改变;你可以把它看作是 对象在内存中的地址。“is”运算符比较 两个对象;函数的作用是:返回一个表示其 标识(当前作为其地址实现)。对象的类型是 也是不变的


这里有两点需要说明

首先,Python确实使用
\uu getitem\uuu
调用创建了一个新字符,但前提是该字符的序数值大于256

例如:

>>> s = chr(256)
>>> s[0] is s
True

>>> t = chr(257)
>>> t[0] is t
False
这是因为在内部,编译后的函数检查单个chracter的序数值,如果该值小于等于256,则调用。这允许共享一些单个字符串。否则,将创建一个新的unicode对象

第二个问题涉及垃圾收集,并表明解释器可以非常快速地重用内存地址。当你写作时:

>>> s = t # = chr(257)
>>> t[0] is s[0]
False
Python首先创建两个新的单字符串,然后比较它们的内存地址。它们有不同的地址(根据上面的解释,我们有不同的对象),因此将对象与
进行比较将返回False

另一方面,我们可以看到看似矛盾的情况:

>>> id(t[0]) == id(s[0])
True
但这是因为解释器在稍后创建新字符串
s[0]
时会快速重用
t[0]
的内存地址

如果您检查此行生成的字节码(例如,使用
dis
-见下文),您会看到每一方的地址都是一个接一个地分配的(创建一个新的字符串对象,然后对其调用
id

只要返回
id(t[0])
,对对象
t[0]
的引用就会降为零(我们现在对整数进行比较,而不是对象本身)。这意味着
s[0]
可以在以后创建相同的内存地址时重用它


下面是我注释的
id(t[0])==id(s[0])
行的反汇编字节码

您可以看到
t[0]
的生存期在创建
s[0]
之前结束(没有对它的引用),因此它的内存可以重用

  2           0 LOAD_GLOBAL              0 (id)
              3 LOAD_GLOBAL              1 (t)
              6 LOAD_CONST               1 (0)
              9 BINARY_SUBSCR                     # t[0] is created
             10 CALL_FUNCTION            1        # id(t[0]) is computed...
                                                  # ...lifetime of string t[0] over
             13 LOAD_GLOBAL              0 (id)
             16 LOAD_GLOBAL              2 (s)
             19 LOAD_CONST               1 (0)
             22 BINARY_SUBSCR                     # s[0] is created...
                                                  # ...free to reuse t[0] memory
             23 CALL_FUNCTION            1        # id(s[0]) is computed
             26 COMPARE_OP               2 (==)   # the two ids are compared
             29 RETURN_VALUE

您的char实际上是一个单元素字符串,因此是的,通过索引访问char会创建长度为1的新字符串。我得到这个-
ValueError:chr()arg不在范围(256)
当我运行这个-
s=chr(8263)
时,如何解释重复的
t[0]是s[0]->False
即使
id(t[0])==id(s[0])->True
?@CrakC可能是因为您使用的是Python 2。
unichr
标准功能将完成此任务。@P.Ortiz没错!Python 3中的行为有何不同?感谢您提供的信息丰富且令人信服的回答。但它提出了一个问题:它是否与字符串的不变性相矛盾?根据定义,字符串
s
的不变性意味着
s[valid_index]
对象在字符串生命周期内不会更改?@P.Ortiz:没问题,很高兴它有所帮助。如果我理解正确,那么就我所见,这与字符串对象的不变性并不矛盾。对于第一部分,
s[i]
似乎只是检查
s
的第i个字符是否在0-256范围内,如果在0-256范围内,则返回由
chr(i)
给出的单例字符串对象。对于比较
id
部分,就Python而言,
t[0]
的生命周期已经结束,因此
s[0]
可以自由地占用它所在的内存。没有字符串对象发生变化。我想说得更清楚一些:Python Ref.docs解释道:“当我们讨论容器的可变性时,只会暗示直接包含的对象的标识。”因此,现在,假设您有以下字符串:
s=chr(8263)
。如果在
s
上执行索引,例如一个无辜的赋值为
x=s[0]
,则
s
中第一个项目的标识已更改(
str.getitem
返回一个新值,如您的响应中所述)。我的意思是,
str.\uuu getitem\uuu
是一种变异方法(字符串状态已经改变)。但是,不可变对象没有任何变异方法。写入
x=s[0]
根本不会变异或改变
s
。如果
s
只是
chr(8263)
给出的单个字符,则
x=s[0]
在内存中创建一个新的字符串对象,并为其指定名称
x
。因此,现在
s[0]
是与
s
相同的unicode字符的表示形式,但它存在于内存中的新地址(它具有不同的标识)。string对象
s
没有以任何方式进行更改。事实上,正如您所说:它具有不同的标识,这是我的问题(回想一下Python文档所解释的:“当我们讨论容器的易变性时,只暗示了直接包含的对象的标识