使用id()理解Python中的6个关键概念

启动任何Python解释器时,都有70多个内置函数可用。 每个Python学习者都不应不熟悉一些普通的学习者。 例如,我们可以使用len()来获取对象的长度,例如列表或字典中的项目数。 再举一个例子,我们可以使用print()打印出感兴趣的对象,以进行学习和调试。

使用id()理解Python中的6个关键概念

此外,几乎所有Python程序员都应该在教程中看到内置的id()函数的使用,以用于指导特定的Python概念。 但是,据我所知,这些信息是分散的。 在本文中,我想对使用id()函数理解六个关键Python概念进行系统的回顾。

1. 一切都是Python中的对象

作为一种流行的面向对象的编程语言,Python在其实现中随处使用对象。 例如,诸如整数,浮点数,字符串,列表和字典之类的内置数据类型都是对象。 而且,函数,类甚至模块也被用作对象。

根据定义,id()函数接受一个对象并返回该对象的标识,即以整数表示的内存地址。 因此,我们可以使用此函数来证明Python中的所有对象都是真实的。

>>> import sys 
>>> class Foo: 
...     pass 
...  
>>> def foo(): 
...     pass 
...  
>>> a_tuple = ('Error', 404) 
>>> a_dict = {'error_code': 404} 
>>> a_list = [1, 2, 3] 
>>> a_set = set([2, 3, 5]) 
>>> objects = [2, 2.2, 'hello', a_tuple, a_dict, a_list, a_set, Foo, foo, sys] 
>>>  
>>> for item in objects: 
...     print(f'{type(item)} with id: {id(item)}') 
...  
<class 'int'> with id: 4479354032 
<class 'float'> with id: 4481286448 
<class 'str'> with id: 4483233904 
<class 'tuple'> with id: 4483061152 
<class 'dict'> with id: 4483236000 
<class 'list'> with id: 4483236720 
<class 'set'> with id: 4483128688 
<class 'type'> with id: 140235151304256 
<class 'function'> with id: 4483031840 
<class 'module'> with id: 4480703856 

在上面的代码片段中,您可以看到对象列表中的每个项目都可以在id()函数中使用,该函数显示每个对象的内存地址。

我认为很有趣的以下操作是,作为函数本身,id()函数也应具有其内存地址。

>>> print(f'{type(id)} with id: {id(id)}') 
<class 'builtin_function_or_method'> with id: 4480774224 

2. 变量分配和别名

在Python中创建变量时,通常使用以下语法:

var_name = the_object 

此过程基本上将在内存中创建的对象绑定到特定的变量名称。 如果为变量分配另一个变量,例如var_name1 = var_name,会发生什么?

考虑以下示例。 在下面的代码片段中,我们首先创建了一个名为hello的变量,并为其分配了字符串值。 接下来,我们通过分配之前的变量hello创建了另一个名为world的变量。 当我们打印出他们的内存地址时,我们发现hello和world都具有相同的内存地址,这表明它们是内存中的同一对象。

>>> hello = 'Hello World!' 
>>> print(f'{hello} from: {id(hello)}') 
Hello World! from: 4341735856 
>>> world = hello 
>>> print(f'{world} from: {id(world)}') 
Hello World! from: 4341735856 
>>> 
>>> bored = {'a': 0, 'b': 1} 
>>> print(f'{bored} from: {id(bored)}') 
{'a': 0, 'b': 1} from: 4341577200 
>>> more_bored = bored 
>>> print(f'{more_bored} from: {id(more_bored)}') 
{'a': 0, 'b': 1} from: 4341577200 
>>> more_bored['c'] = 2 
>>> bored 
{'a': 0, 'b': 1, 'c': 2} 
>>> more_bored 
{'a': 0, 'b': 1, 'c': 2} 

在这种情况下,变量世界通常称为变量hello的别名,通过分配现有变量来创建新变量的过程可以称为别名。 在其他编程语言中,别名非常类似于与内存中基础对象有关的指针或引用。

在上面的代码中,我们还可以看到,当我们为字典创建别名并修改别名的数据时,该修改也将应用于原始变量,因为在后台,我们修改了内存中的同一字典对象。

3. 比较运算符:== vs. is

在各种情况下,我们需要比较两个对象作为决策点,以便在满足或不满足特定条件时应用不同的功能。 就相等比较而言,我们可以使用两个比较运算符:==和is。 一些新的Python学习者可能会错误地认为它们是相同的,但是有细微差别。

考虑以下示例。 我们创建了两个相同项目的列表。 当我们使用==运算符比较两个列表时,比较结果为True。 当我们使用is运算符比较两个列表时,比较结果为False。 他们为什么产生不同的结果? 这是因为==运算符会比较值,而is运算符会比较标识(即内存地址)。

正如您所期望的,这些变量引用了内存中的同一对象,它们不仅具有相同的值,而且具有相同的标识。 这导致==和is运算符的评估结果相同,如下面涉及str0和str1的示例所示:

>>> list0 = [1, 2, 3, 4] 
>>> list1 = [1, 2, 3, 4] 
>>> print(f'list0 == list1: {list0 == list1}') 
list0 == list1: True 
>>> print(f'list0 is list1: {list0 is list1}') 
list0 is list1: False 
>>> print(f'list0 id: {id(list0)}') 
list0 id: 4341753408 
>>> print(f'list1 id: {id(list1)}') 
list1 id: 4341884240 
>>> 
>>> str0 = 'Hello' 
>>> str1 = str0 
>>> print(f'str0 == str1: {str0 == str1}') 
str0 == str1: True 
>>> print(f'str0 is str1: {str0 is str1}') 
str0 is str1: True 
>>> print(f'str0 id: {id(str0)}') 
str0 id: 4341981808 
>>> print(f'str1 id: {id(str1)}') 
str1 id: 4341981808 

4. 整数缓存

我们在编程中经常使用的一组数据是整数。 在Python中,解释器通常会缓存介于-5到256之间的小整数。这意味着在启动Python解释器时,这些整数将被创建并可供以后在内存中使用。 以下代码片段显示了此功能:

>>> number_range = range(-10, 265) 
>>> id_counters = {x: 0 for x in number_range} 
>>> id_records = {x: 0 for x in number_range} 
>>>  
>>> for _ in range(1000): 
...     for number in number_range: 
...         idid_number = id(number) 
...         if id_records[number] != id_number: 
...             id_records[number] = id_number 
...             id_counters[number] += 1 
...  
>>> [x for x in id_counters.keys() if id_counters[x] > 1] 
[-10, -9, -8, -7, -6, 257, 258, 259, 260, 261, 262, 263, 264] 

在上面的代码中,我创建了两个字典,其中id_counters跟踪每个整数的唯一标识的计数,而id_records跟踪整数的最新标识。 对于介于-10到265之间的整数,如果新整数的标识与现有整数不同,则相应的计数器将递增1。 我重复了这个过程1000次。

代码的最后一行使用列表推导技术向您显示具有多个同一性的整数。 显然,经过1000次后,从-5到256的整数对于每个整数仅具有一个标识,如上一段所述。 要了解有关Python列表理解的更多信息,您可以参考我以前关于此的文章:

5. 浅层和深层副本

有时,我们需要制作现有对象的副本,以便我们可以更改一个副本而不更改另一个副本。 内置的复制模块为此提供了两种方法:copy()和deepcopy(),它们分别进行浅拷贝和深拷贝。 如果您不知道它们是什么,让我们利用id()函数来了解这两个概念。

>>> import copy 
>>> original = [[0, 1], 2, 3] 
>>> print(f'{original} id: {id(original)}, embeded list id: {id(original[0])}') 
[[0, 1], 2, 3] id: 4342107584, embeded list id: 4342106784 
>>> copycopy0 = copy.copy(original) 
>>> print(f'{copy0} id: {id(copy0)}, embeded list id: {id(copy0[0])}') 
[[0, 1], 2, 3] id: 4341939968, embeded list id: 4342106784 
>>> copycopy1 = copy.deepcopy(original) 
>>> print(f'{copy1} id: {id(copy1)}, embeded list id: {id(copy1[0])}') 
[[0, 1], 2, 3] id: 4341948160, embeded list id: 4342107664 

我们首先创建了一个名为original的列表变量,它由一个嵌套列表和两个整数组成。 然后,我们分别使用copy()和deepcopy()方法制作了两个副本(copy0和copy1)。 如我们所料,原始的copy0和copy1具有相同的值(即[[0,1],2,3])。 但是,它们具有不同的身份,因为与别名不同,copy()和deepcopy()方法均会在内存中创建新对象,从而使新副本具有不同的身份。

浅层副本和深层副本之间最本质的区别是,深层复制将为原始复合对象递归创建副本,而浅层复制将在适用的情况下保留对现有对象的引用。 在上面显示的示例中,变量original实际上是一个复合对象(即一个列表嵌套在另一个列表中)。

在这种情况下,使用copy()方法,变量copy0的第一个元素与原始的第一个元素具有相同的标识(即,相同的对象)。 相比之下,deepcopy()方法在内存中复制嵌套列表,以使copy1中的第一个元素具有与原始元素不同的标识。

但是在深度复制中"递归"是什么意思? 这意味着如果存在多层嵌套(例如,嵌套在列表中的列表,又嵌套在另一个列表中),则deepcopy()方法将为每一层创建新对象。 请参见以下示例以了解此功能:

>>> mul_nested = [[[0, 1], 2], 3] 
>>> print(f'{mul_nested} id: {id(mul_nested)}, inner id: {id(mul_nested[0])}, innermost id: {id(mul_nested[0][0])}') 
[[[0, 1], 2], 3] id: 4342107824, inner id: 4342106944, innermost id: 4342107424 
>>> mul_nested_dc = copy.deepcopy(mul_nested) 
>>> print(f'{mul_nested_dc} id: {id(mul_nested_dc)}, inner id: {id(mul_nested_dc[0])}, innermost id: {id(mul_nested_dc[0][0])}') 
[[[0, 1], 2], 3] id: 4342107264, inner id: 4342107984, innermost id: 4342107904 

6. 数据可变性

Python编程中的一个高级主题与数据可变性有关。 一般来说,不可变数据是指其值在创建后便无法更改的对象,例如整数,字符串和元组。 相比之下,可变数据是指其值在创建后可以更改的那些对象,例如列表,字典和集合。

需要注意的一件事是,通过"更改值",我们的意思是是否可以更改内存中的基础对象。 在我的上一篇文章中可以找到关于数据可变性的详尽讨论:

不可变与可变

为了本文讨论id()函数的目的,让我们考虑以下示例。 对于不可变数据类型(代码片段中的整数变量千),当我们尝试更改其值时,会在内存中创建一个新的整数,这由千变量的新标识所反映。 换句话说,原始的基础整数对象无法更改。 尝试更改整数只会在内存中创建一个新对象。

>>> thousand = 1000 
>>> print(f'{thousand} id: {id(thousand)}') 
1000 id: 4342004944 
>>> thousand += 1 
>>> print(f'{thousand} id: {id(thousand)}') 
1001 id: 4342004912 
>>> numbers = [4, 3, 2] 
>>> print(f'{numbers} id: {id(numbers)}') 
[4, 3, 2] id: 4342124624 
>>> numbers += [1] 
>>> print(f'{numbers} id: {id(numbers)}') 
[4, 3, 2, 1] id: 4342124624 

如果这让您感到困惑,让我们看看可变数据类型发生了什么—在我们的例子中是列表变量编号。 如上面的代码所示,当我们尝试更改数字的值时,变量号得到了更新,并且更新后的列表仍具有相同的标识,从而确认了列表类型的对象的可变性。