关于python中字符串实例唯一性的问题

A question regarding string instance uniqueness in python

我试图找出python只实例化一次的整数(看起来是6到256),在这个过程中遇到了一些我看不到的字符串行为。有时,以不同方式创建的相等字符串共享相同的ID,有时不共享。此代码:

1
2
3
4
5
6
7
8
9
10
11
A ="10000"
B ="10000"
C ="100" +"00"
D ="%i"%10000
E = str(10000)
F = str(10000)
G = str(100) +"00"
H ="0".join(("10","00"))

for obj in (A,B,C,D,E,F,G,H):
    print obj, id(obj), obj is A

印刷品:

1
2
3
4
5
6
7
8
10000 4959776 True
10000 4959776 True
10000 4959776 True
10000 4959776 True
10000 4959456 False
10000 4959488 False
10000 4959520 False
10000 4959680 False

我甚至看不到这个模式——除了前四个没有显式函数调用的事实之外——但肯定不是这样,因为C中的"EDOCX1"〔0〕表示要添加的函数调用。我尤其不明白为什么C和G是不同的,因为这意味着添加的组件的ID比结果更重要。

那么,A-D所接受的特殊治疗是什么,使它们以同样的方式出现?


在语言规范方面,对于不可变类型的任何实例,完全允许任何兼容的python编译器和运行时生成新实例或查找与所需值相同类型的现有实例,并使用对该实例的新引用。这意味着使用is或通过不可变项之间的ID比较总是不正确的,任何次要的发布都可能在这方面调整或更改策略以增强优化。

在实现方面,权衡是非常清楚的:尝试重用现有实例可能意味着花费(可能是浪费)时间试图找到这样的实例,但如果尝试成功,则会节省一些内存(以及分配和稍后释放保存新实例所需的内存位的时间)。

如何解决这些实现权衡并不是完全显而易见的——如果您可以确定一些启发式方法,这些方法表明找到一个合适的现有实例是可能的,并且搜索(即使失败)将很快,那么您可能希望尝试搜索,并在启发式方法建议时重新使用,但如果不这样做,就跳过它。

在您的观察中,您似乎发现了一个特定的点发布实现,它在完全安全、快速和简单的情况下执行少量的窥视孔优化,因此分配a到d都归结为与a完全相同(但e到f不相同,因为它们涉及优化器作者可能合理的命名函数或方法已经考虑到假设语义不完全安全——如果这样做的话,投资回报率很低——所以他们没有优化窥视孔。

因此,A到D重用相同的实例可以归结为A和B这样做(C和D将窥视孔优化为完全相同的结构)。

反过来,这种重用也清楚地表明了编译器策略/优化器启发法,即在同一函数的本地命名空间中,不可变类型的相同文本常量被折叠为对函数的.func_code.co_consts中的一个实例的引用(使用当前cpython对函数和代码对象属性的术语)——合理性。e策略和启发式方法,因为在一个函数中重复使用相同的不变的常量文本有点频繁,并且价格只支付一次(在编译时),而优势则累积多次(每次函数运行,可能在循环中等等)。

(事实上,这些特定的策略和启发式方法,考虑到它们明显的积极权衡,已经普遍存在于所有最新版本的cpython中,而且,我相信,Ironpython、jython和pypypy中;-)。

如果您计划为Python本身或类似的语言编写编译器、运行时环境、窥视孔优化器等,那么这是一个有一定价值和有趣的研究。我想应该深入研究内部结构(当然,理想情况下是许多不同的正确实现,这样就不会局限于某个特定实现的怪癖——好消息是,目前Python至少有4个独立的、值得生产使用的实现,更不用说每个实现的几个版本了!)也可以间接地帮助我们成为一个更好的Python程序员——但是,特别重要的是要关注语言本身所保证的东西,这比在不同的实现中所发现的共同点要少一些,因为"恰好"的部分现在是共同的(而不是语言要求如此)。e specs)在下一次发布一个或另一个实现时,可能会在您的控制下完全改变,并且,如果您的生产代码错误地依赖于这些细节,那么可能会导致令人讨厌的意外;-)。另外——几乎没有必要,甚至特别有帮助地依赖这些变量的实现细节,而不是语言强制的行为(当然,除非您正在编写优化器、调试器、分析器等类似的代码;-)。


允许python内联字符串常量;a、b、c、d实际上是相同的文本(如果python看到常量表达式,则将其视为常量)。

str实际上是一个类,所以str(whatever)正在调用这个类的构造函数,它应该产生一个新的对象。这就解释了e,f,g(注意,每一个都有独立的标识)。

至于h,我不确定,但我想解释一下,这个表达式对于Python来说太复杂了,无法确定它实际上是一个常量,所以它会计算一个新的字符串。


回答S.lott关于检查字节码的建议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import dis
def moo():
    A ="10000"
    B ="10000"
    C ="100" +"00"
    D ="%i"%10000
    E = str(10000)
    F = str(10000)
    G ="1000"+str(0)
    H ="0".join(("10","00"))
    I = str("10000")

    for obj in (A,B,C,D,E,F,G,H, I):
        print obj, id(obj), obj is A
moo()
print dis.dis(moo)

产量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
10000 4968128 True
10000 4968128 True
10000 4968128 True
10000 4968128 True
10000 2840928 False
10000 2840896 False
10000 2840864 False
10000 2840832 False
10000 4968128 True
  4           0 LOAD_CONST               1 ('10000')
              3 STORE_FAST               0 (A)

  5           6 LOAD_CONST               1 ('10000')
              9 STORE_FAST               1 (B)

  6          12 LOAD_CONST              10 ('10000')
             15 STORE_FAST               2 (C)

  7          18 LOAD_CONST              11 ('10000')
             21 STORE_FAST               3 (D)

  8          24 LOAD_GLOBAL              0 (str)
             27 LOAD_CONST               5 (10000)
             30 CALL_FUNCTION            1
             33 STORE_FAST               4 (E)

  9          36 LOAD_GLOBAL              0 (str)
             39 LOAD_CONST               5 (10000)
             42 CALL_FUNCTION            1
             45 STORE_FAST               5 (F)

 10          48 LOAD_CONST               6 ('1000')
             51 LOAD_GLOBAL              0 (str)
             54 LOAD_CONST               7 (0)
             57 CALL_FUNCTION            1
             60 BINARY_ADD          
             61 STORE_FAST               6 (G)

 11          64 LOAD_CONST               8 ('0')
             67 LOAD_ATTR                1 (join)
             70 LOAD_CONST              12 (('10', '00'))
             73 CALL_FUNCTION            1
             76 STORE_FAST               7 (H)

 12          79 LOAD_GLOBAL              0 (str)
             82 LOAD_CONST               1 ('10000')
             85 CALL_FUNCTION            1
             88 STORE_FAST               8 (I)

 14          91 SETUP_LOOP              66 (to 160)
             94 LOAD_FAST                0 (A)
             97 LOAD_FAST                1 (B)
            100 LOAD_FAST                2 (C)
            103 LOAD_FAST                3 (D)
            106 LOAD_FAST                4 (E)
            109 LOAD_FAST                5 (F)
            112 LOAD_FAST                6 (G)
            115 LOAD_FAST                7 (H)
            118 LOAD_FAST                8 (I)
            121 BUILD_TUPLE              9
            124 GET_ITER            
        >>  125 FOR_ITER                31 (to 159)
            128 STORE_FAST               9 (obj)

 15         131 LOAD_FAST                9 (obj)
            134 PRINT_ITEM          
            135 LOAD_GLOBAL              2 (id)
            138 LOAD_FAST                9 (obj)
            141 CALL_FUNCTION            1
            144 PRINT_ITEM          
            145 LOAD_FAST                9 (obj)
            148 LOAD_FAST                0 (A)
            151 COMPARE_OP               8 (is)
            154 PRINT_ITEM          
            155 PRINT_NEWLINE      
            156 JUMP_ABSOLUTE          125
        >>  159 POP_BLOCK          
        >>  160 LOAD_CONST               0 (None)
            163 RETURN_VALUE

因此,似乎编译器确实理解A-D的含义是相同的,所以它只生成一次就节省了内存(如Alex、Maciej和Greg建议的那样)。(新增的案例I似乎只是str()意识到它试图从一个字符串中生成一个字符串,并通过它。)

谢谢大家,现在说得更清楚了。


我相信可以在编译时对短字符串进行评估的字符串将自动进行内部处理。在最后的例子中,由于可以重新定义strjoin,所以不能在编译时评估结果。