查看原文
其他

Python 的指针,有必要理解它

南风草木香 Python开发者 2022-09-07

【导语】:这篇文章主要讲了Python中的指针,Python自动进行内存管理。开发者无需手动为对象分配内存,也不用在使用完对象后释放内存。但了解Python的内存管理机制,有助于开发者更好的编写代码。本文将介绍指针的概念,并对深拷贝进行解释。

简介

与C,C++这种静态语言相比,Python是自动管理内存的,它采用“引用计数”的方式管理内存,即Python内部会记录对象有多少个引用,如果某个对象的引用计数大于0,该对象就会一直存放在内存中;当对象的引用计数为0时,就会被垃圾回收机制回收。这也是Python的方便之处,开发者无需考虑提前为对象分配内存,使用完后也无需手动释放内存。

但了解Python内部是如何管理内存,也很有必要。你考虑过is和==有什么不同吗?或者为什么要使用深拷贝?你有考虑过Python是如何操作对象的吗?这篇文章或许会给你一些启示。

什么是指针,存在于哪里?

首先,我们要理解命名空间的概念。Python中的命名空间指的是一个作用域中所有变量、关键字和函数组成的列表,用于避免命名冲突,不同命名空间中可以包含相同的变量名。例如,每个命名空间中都有内置函数(如print()和str()),关键字(如None和True)。

当你创建一个新变量时,变量名会被添加到其所在作用域的命名空间中。例如,下面的代码会把变量名my_string添加到全局命名空间中:

my_string = "Hello World!"

本文我们不考虑作用域的问题,所有的案例都基于全局命名空间。

指针就是变量名——也就是Python命名空间的入口——与Python内存中的对象相对应。在上面的例子中,指针就是my_string,内存中的对象是“Hello World!”字符串。通过在命名空间中使用指针,我们就能访问和操作内存中的对象了。就像一个人可能有多个名字一样,多个指针也可能会指向同一个对象。

需要注意的是: 本文中提到的“指针”并不是 C 或 C++ 中的指针(更像是 C++ 中的引用)。使用 C 的开发者可以看看这篇文章[1]

接下来,我们先创建一个列表,名称为my_list:my_list = ['string', 42]

变量名my_list指向的是列表对象。列表对象有2个指针,分别指向其中的2个元素。由此可以看出,当创建一个列表时,如果其中有元素,那么该列表也会包含指针,指向这些元素。因此,本文的许多例子将使用列表。

为什么要使用深拷贝?

初学者经常搞不清楚什么是指针重叠,简而言之就是两个指针指向了内存中的同一个对象。接下来,我们先创建一个列表a:

>>> a = ["string", 42]
>>> a
["string", 42]

随后,拷贝a,得到 b,并修改b的第一个元素:

>>> b = a
>>> b[0] = "some words"
>>> b
["some words", 42]

可以看到,修改成功了,然而a的结果也变了:

>>> a
["some words", 42]

a之所以会发生改变,是因为b = a并没有创建一个新的列表,只是创建了一个新的指针b,它与a指向了相同的列表。

假如我们想在不创建新列表的情况下修改b,该如何做呢?我们可以使用copy方法:

>>> c = a.copy()
>>> c[0] = "hello!"
>>> c
["hello!", 42]

>>> a
["some words", 42]

该方法会创建一个新的列表,同时包含新的指针,但这些指针指向的元素与原列表相同,如下图:

我们再来看看列表元素也是对象的情况:

>>> a = [["alex""beth"]]
>>> a
[["alex""beth"]]

接下来,使用copy()方法复制列表a,再往b的第一个元素["alex", "beth"]中添加元素:

>>> b = a.copy()
>>> b[0].append("charlie")
>>> b[0]
["alex""beth""charlie"]

看起来成功了,但是a[0]的元素也变了:

>>> a[0]
["alex""beth""charlie"]

原因在于,copy方法是浅拷贝。a[0]与b[0]指向了同一个对象,如下图:

那么,怎样实现深拷贝呢?可以使用deepcopy函数:

>>> from copy import deepcopy
>>> c = deepcopy(a)
>>> c[0].append("dan")
>>> c[0]
["alex""beth""charlie""dan"]

>>> a[0]
["alex""beth""charlie"]

deepcopy函数采用递归的方式,复制每一个对象,避免指针混叠。下图就是采用深拷贝方法生成的新对象c:

这样,当我们修改c中的元素时,就不会影响到源对象a了。

不可变对象(如元组)中的指针

到目前为止,我们例子使用的一直是列表,它是可变对象。接下来看看不可变对象元组。之所以说它不可变,是因为当创建a时,它的所有元素a[0]、a[1]都是固定的。假如元素是不可变的,比如字符串或整数,那还比较好理解。比如下面这个例子,我们无法修改a[0]:

>>> a = (42, 'beeblebrox')
>>> a[0] = 63
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

如果a[0]指向的是一个可变对象,例如列表,会出现什么情况呢?

>>> a = ([1, 2, 3], "hello")
>>> a[0].append(4)
>>> a
([1, 2, 3, 4], "hello")

结果显示,我们可以修改元组a!这是因为我们修改的不是a[0]本身,而是它指向的对象。我们把a[0]指向的对象重新命名,看起来就清晰多了:

>>> my_list = [1, 2, 3]
>>> a = (my_list, "hello")
>>> my_list.append(4)
>>> a
([1, 2, 3, 4], "hello")

然而,append方法不是唯一给列表添加元素的方式。我们还可以使用 += 操作符:

  1. 首先,该操作符会创建所需的对象。对于可变对象(如列表),它会直接修改对象;对于不可变对象(如字符串),它会创建一个新的对象。

  2. 其次,让my_list的指针指向创建的对象。

  • 可变对象
>>> my_list = [1, 2, 3, 4]
>>> my_list += [5, ]
>>> my_list
[1, 2, 3, 4, 5]

如果在可变对象上调用+=,那么第2步就相当多余了——毕竟,指针已经指向所需的对象。但是当它被不可变对象(比如字符串)调用时,它确实需要改变指针指向的位置。例如:

  • 不可变对象
>>> my_string = 'Hello'
>>> my_string += ', World!'
>>> my_string
'Hello, World!'

假如我们使用+=操作元组a的第一个元素,可以吗?来看看:

>>> a = ([1, 2, 3, 4], "hello")
>>> a[0] += [5, ]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

>>> a
([1, 2, 3, 4, 5], "hello")

看起来,出现了报错,但似乎+=强制执行了。

首先,a[0]指向的列表本身发生了改变,这是a[0]的值变成[1, 2, 3, 4, 5]的原因。然而,由于不能直接修改列表中元素的指向,当a[0]的指针指向新创建的对象时,就会产生报错。

后记

本文我们学习了名称空间、指针的概念,还提供了一些例子,用以解释不可变对象中指针的工作原理。限于篇幅,这篇文章并涉及 Python 对象。如果大家觉得本文内容有帮助,请点赞转发支持一下。后续再继续更新 Python 指针的其他内容。

参考资料

[1]

这篇文章: https://realpython.com/pointers-in-python/

[2]

参考原文: https://anvil.works/articles/pointers-in-my-python-1


- EOF -


加主页君微信,不仅Python技能+1

主页君日常还会在个人微信分享Python相关工具资源精选技术文章,不定期分享一些有意思的活动岗位内推以及如何用技术做业余项目

加个微信,打开一扇窗



推荐阅读  点击标题可跳转

1、涨知识!Python 的异常信息还能这样展现

2、Cython 是什么编程语言?为什么你有必要学习一下

3、6.6K Star!比 Pandas 快很多的数据处理库


觉得本文对你有帮助?请分享给更多人

推荐关注「Python开发者」,提升Python技能

点赞和在看就是最大的支持❤️

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存