我试图弄清楚哪些整数python只实例化一次(看起来是-6到256),并且在这个过程中偶然发现了一些字符串行为,我无法看到该模式。有时,以不同的方式创建相等的字符串分享相同的ID,有时不。这段代码:
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
打印:
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中的“+
”示例意味着对添加的函数调用。我特别不明白为什么C和G是不同的,因为这意味着加法的组成部分的ID比结果更重要。
那么,A-D经历的特殊处理是什么,使它们成为同一个实例?
答案 0 :(得分:10)
就语言规范而言,对于任何不可变类型的实例,任何兼容的Python编译器和运行时都可以完全允许创建新实例或查找与所需值相同的现有实例,并使用对同一个实例的新引用。这意味着在不可变项之间使用is
或by-id比较始终是不正确的,并且任何次要版本都可能在此问题中调整或更改策略以增强优化。
在实现方面,权衡非常明确:尝试重用现有实例可能意味着花费时间(可能是浪费)试图找到这样的实例,但如果尝试成功则会保存一些内存(以及时间分配并稍后释放保存新实例所需的内存位。)
如何解决这些实现权衡并不是完全明显的 - 如果你能够识别出表明找到合适的现有实例的启发式方法并且搜索(即使它失败)会很快,那么你可能想要尝试当启发式建议时,搜索和重用,但跳过它。
在你的观察中,你似乎找到了一个特定的点发布实现,当它完全安全,快速和简单时执行一点点的窥视孔优化,所以A到D的赋值都归结为与A完全相同(但是E到F没有,因为它们涉及命名函数或方法,优化器的作者可能合理地认为不是100%安全地假定语义 - 如果这样做了则ROI低 - 因此它们不是窥视孔 - 优化)。
因此,重复使用相同实例的A到D归结为A和B这样做(因为C和D将窥孔优化到完全相同的构造)。
反过来,重用反过来清楚地表明编译器策略/优化器启发式方法,即同一函数的本地命名空间中不可变类型的相同文字常量被折叠为仅引用函数.func_code.co_consts
中的一个实例(以使用当前CPython的函数和代码对象属性的术语) - 合理的策略和启发式,因为在一个函数中重用相同的不可变常量文字有点频繁,并且价格只支付一次(在编译时),而优势累计很多时间(每次函数运行,可能在循环等等)。
(恰恰相反,这些特定的策略和启发式方法,鉴于它们明显积极的权衡,在所有最新版本的CPython中都很普遍,而且我相信,IronPython,Jython和PyPy也是如此; - )。
如果您计划为Python本身或类似语言编写编译器,运行时环境,窥孔优化器等,这是一项有点值得研究的有趣内容。我想深入研究内部结构(理想情况下是许多不同的正确实现,当然,为了不注意特定的一个怪癖 - Python目前享有至少4个独立的生产价值实现的优点,更不用说了每个版本的几个版本!)也可以帮助,间接地帮助一个更好的Python程序员 - 但是关注语言本身的保证是特别重要的,这比你要小得多在单独的实现中找到共同点,因为现在“恰好发生”的部分(没有必需是由语言规范这样)可能在下一点完全改变你释放一个或另一个实现,如果您的生产代码错误地依赖于这些细节,那可能会导致令人讨厌的意外;-)。另外 - 依靠这种变量实现细节而不是语言强制行为几乎没有必要,甚至没有特别有用(当然,除非你编写类似优化器,调试器,分析器等的东西; - )。
答案 1 :(得分:4)
允许Python内联字符串常量; A,B,C,D实际上是相同的文字(如果Python看到一个常量表达式,它将它视为常量)。
str
实际上是一个类,因此str(whatever)
正在调用此类的构造函数,它应该产生一个新对象。这解释了E,F,G(注意,每个都有不同的身份)。
对于H,我不确定,但我会解释说这个表达式太复杂,Python无法弄清楚它实际上是一个常量,所以它计算一个新的字符串。
答案 2 :(得分:1)
我相信可以在编译时评估的短字符串将自动实现。在最后的示例中,无法在编译时评估结果,因为可能会重新定义str
或join
。
答案 3 :(得分:1)
回答S.Lott关于检查字节码的建议:
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)
的产率:
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()意识到它正在尝试从字符串中创建一个字符串,并将其传递通过。)
谢谢大家,现在更清楚了。