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

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

此外,几乎所有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)}')... with id: 4479354032 with id: 4481286448 with id: 4483233904 with id: 4483061152 with id: 4483236000 with id: 4483236720 with id: 4483128688 with id: 140235151304256 with id: 4483031840 with id: 4480703856

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

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

>>> print(f'{type(id)} with id: {id(id)}') 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:... id_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>>> copy0 = 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>>> copy1 = 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

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

总结

在本文中,我们利用内置的id()函数来了解Python中的六个关键概念。 以下是这些概念的快速回顾:

· Python中的所有内容都是一个对象。

· 我们通过赋值创建变量,别名指向内存中的相同对象。

· 比较运算符==比较值,而比较运算符正在比较标识。

· Python解释器在启动时会创建从-5到256的整数对象。

· 浅副本和深副本均具有与其原始对象相同的值,但是浅副本仅复制原始对象的嵌套对象的引用。

· 可变对象的值可以在内存中更改,而不可变对象不支持值更改。

【责任编辑:赵宁宁 TEL:(010)68476606】

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章