Python 身份:多重人格障碍,需要代码精简

11 投票
5 回答
716 浏览
提问于 2025-04-15 17:59

可能重复的问题:
Python 的“is”运算符在处理整数时表现得很奇怪

我遇到了一个关于Python的奇怪现象:

>>> two = 2
>>> ii = 2

>>> id(two) == id(ii)
True
>>> [id(i) for i in [42,42,42,42]]
[10084276, 10084276, 10084276, 10084276]

>>> help(id)
Help on built-in function id in module __builtin__:

id(...)
    id(object) -> integer

    Return the identity of an object.  This is guaranteed to be unique among
    simultaneously existing objects.  (Hint: it's the object's memory address.)
  1. 每个数字都是一个独特的对象吗?
  2. 不同的变量如果存储相同的值(比如两个变量都是2),它们是同一个对象吗?
  3. Python是如何生成一个数字的ID的?
  4. 在上面的例子中,两个变量和2的值是不是指向同一个内存单元?这听起来太奇怪了。

帮我理清这个身份危机。

还有一些其他的奇怪现象:

>>> a,b=id(0),id(1)
>>> for i in range(2,1000):
   a,b=b,id(i)
   if abs(a-b) != 12:
    print('%i:%i -> %i' % (i,a,b))

上面的代码检查连续整数的ID是否也是连续的,并打印出一些异常情况:

77:10083868 -> 10085840
159:10084868 -> 10086840
241:10085868 -> 10087840
257:10087660 -> 11689620
258:11689620 -> 11689512
259:11689512 -> 11689692
260:11689692 -> 11689548
261:11689548 -> 11689644
262:11689644 -> 11689572
263:11689572 -> 11689536
264:11689536 -> 11689560
265:11689560 -> 11689596
266:11689596 -> 11689656
267:11689656 -> 11689608
268:11689608 -> 11689500
331:11688756 -> 13807288
413:13806316 -> 13814224
495:13813252 -> 13815224
577:13814252 -> 13816224
659:13815252 -> 13817224
741:13816252 -> 13818224
823:13817252 -> 13819224
905:13818252 -> 13820224
987:13819252 -> 13821224

注意,从413开始出现了一些规律。也许这和每个新内存页面开始时的一些神秘操作有关。

5 个回答

8

每个Python的实现都可以自由地进行优化,包括完全不优化,特别是对于不可变对象(比如数字、元组和字符串)[[而对于可变对象,比如列表、字典和集合,则没有这样的灵活性]]。

在两个不可变对象的引用ab之间,所有实现必须保证的是:

  1. id(a) == id(b),也就是a is b,必须总是意味着a == b
  2. 因此a != b必须总是意味着id(a) != id(b),也就是a is not b

特别要注意的是,即使对于不可变类型,也没有限制说a == b必须意味着a is b(也就是id(a) == id(b))。只有None能保证这一点(所以你可以总是用if x is None:来测试,而不是if x == None:)。

当前的CPython实现利用了这些灵活性,通过“合并”小范围内的小整数和在某个函数中出现多次的内置不可变类型对象(比如,如果你的函数f中有四次出现字面量'foobar',那么它们都会指向函数常量中的同一个字符串实例'foobar',这样可以节省一些空间,相比于允许的实现会存储四个相同但独立的常量副本)。

所有这些实现细节对Python程序员来说兴趣不大(除非你在做Python的实现,或者在做一些与特定实现紧密相关的事情,比如调试系统)。

9

在-1到255之间的整数,还有字符串字面量,都是被“内部化”的。也就是说,在代码中出现的每一个实例其实都代表着同一个对象。

在CPython中,id()的结果是PyObject在进程空间中的地址。

4

你提到的第四个问题,“在上面的例子中,两个和ii是指向一个存储值2的内存单元吗?那样会非常奇怪”,其实是理解整个问题的关键。

如果你对C语言或Python有些了解,就会发现这两种语言的“变量”并不是以同样的方式工作的。在C语言中,像这样的变量声明:

int j=1;
int k=2;
k += j;

其实是在说:“编译器,给我在栈上预留两个内存区域,每个区域足够存放一个整数,并把一个记作‘j’,另一个记作‘k’。然后把‘1’放到j里,把‘2’放到k里。”在程序运行时,代码会说:“取出k里的整数,把j里的整数加起来,然后把结果再存回k里。”

而在Python中,看似等价的代码:

j = 1
k = 2
k += j

其实表达的意思不同:“Python,查找一个叫‘1’的对象,并创建一个叫‘j’的标签指向它。查找一个叫‘2’的对象,并创建一个叫‘k’的标签指向它。现在,查找k指向的对象(‘2’),查找j指向的对象(‘1’),然后把k指向对这两个对象进行‘加法’操作后得到的结果。”

dis模块分析这段代码,可以清楚地看到这一点:

  2           0 LOAD_CONST               1 (1)
              3 STORE_FAST               0 (j)

  3           6 LOAD_CONST               1 (2)
              9 STORE_FAST               1 (k)

  4          12 LOAD_FAST                1 (k)
             15 LOAD_FAST                0 (j)
             18 INPLACE_ADD
             19 STORE_FAST               1 (k)

所以,没错,Python中的“变量”其实是指向对象的标签,而不是可以装数据的容器

你提到的其他三个问题都是在问“Python什么时候会从一段代码创建一个新对象,什么时候会重用已经存在的对象?”后者被称为“内部化”;它发生在较小的整数和看起来像符号名称的字符串上。

撰写回答