启动任何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)}')
- ...
- <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
如果这让您感到困惑,让我们看看可变数据类型发生了什么—在我们的例子中是列表变量编号。 如上面的代码所示,当我们尝试更改数字的值时,变量号得到了更新,并且更新后的列表仍具有相同的标识,从而确认了列表类型的对象的可变性。
总结
在本文中,我们利用内置的id()函数来了解Python中的六个关键概念。 以下是这些概念的快速回顾:
- Python中的所有内容都是一个对象。
- 我们通过赋值创建变量,别名指向内存中的相同对象。
- 比较运算符==比较值,而比较运算符正在比较标识。
- Python解释器在启动时会创建从-5到256的整数对象。
- 浅副本和深副本均具有与其原始对象相同的值,但是浅副本仅复制原始对象的嵌套对象的引用。
- 可变对象的值可以在内存中更改,而不可变对象不支持值更改。