首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Python的指针:有什么意义?

目录

为什么Python没有指针?

Python中的对象

不可变对象和可变对象

了解变量

C的变量

Python的名称

关于Python的预实现对象的注释

在Python中模拟指针

使用可变类型作为指针

使用Python对象

使用ctypes模块实现的真正指针

总结

如果您曾经使用过像C或者C++这样的低级语言,那么您可能听说过指针。指针允许您在部分代码上取得更高的效率。但它们也会给初学者带来困惑,而且还可能导致各种内存管理错误,即使对于专家来说也会如此。那么它们在Python的哪里?您该如何在Python中模拟指针?

指针在C和C++中广泛应用。从本质上来说,它们是保存另一个变量内存地址的变量。有关指针的复习,您可以考虑看一下这篇关于C语言指针的概述。

通过本文,您会更好地理解Python的对象模型,同时明白为什么Python中不存在真正的指针。对于需要模仿指针行为的情况,您将学习到如何在没有内存管理噩梦的情况下在Python中模拟指针。

在本文中,您将

· 了解为什么Python中的指针不存在

· 探索C变量和Python名称之间的区别

· 在Python中模拟指针

· 使用ctypes实现真正的指针

注意:在本文中,“Python”会涉及C中Python的参考实现,也称作CPython。当文章在讨论该语言的一些内部结构时,这些注释适用于CPython 3.7,但在将来或过去的语言迭代中可能不适用。

为什么Python没有指针?

事实上,我并不知道答案。Python中的指针本身可以存在吗?可能,但指针似乎违背了Python的禅宗。指针鼓励隐含的变化而不是明确的变化。通常,它们很复杂而不是简单,特别是对于初学者。更糟糕的是,他们会导致你自作自受,或者做一些非常危险的事情,比如从您不被允许的一段内存中读取数据。

Python倾向于尝试从用户那里抽象出内存地址等实现细节。Python通常关注可用性而不是速度。因此,Python中的指针并没有多大意义。但不要害怕,默认情况下,Python会为您提供使用指针的一些好处。

理解Python中的指针需要简要介绍Python的实现细节。具体来说,您需要了解:

1.不可变对象和可变对象

2.Python变量/名称

保留你的内存地址,让我们开始吧。

Python中的对象

在Python中,一切都是对象。为了证明,你可以打开一个REPL并尝试使用isinstance():

此代码向您显示Python中的所有内容确实是一个对象。每个对象至少包含三个数据:

•引用计数

•类型

•值

引用计数用于内存管理。要深入了解Python内存管理的内核,您可以阅读Python中的内存管理。

类型在CPython层使用,用于确保运行时的类型安全性。最后,值,即与对象关联的实际值。

但并非所有对象都是相同的。您还需要了解另一个重要的区别:不可变对象和可变对象。理解对象类型之间的差异确实有助于阐明Python中的指针的第一层。

不可变对象和可变对象

在Python中,有两种类型的对象;

1. 不可变对象无法更改

2. 可变对象可以更改

理解这种差异是认识Python指针的第一个关键。以下是常见类型的细分以及它们是否可变或不可变:

如您所见,许多常用的基元类型是不可变的。您可以通过编写一些Python来证明这一点。您需要Python标准库中的一些工具:

1.id() 返回对象的内存地址。

2.is 当且仅当两个对象具有相同的内存地址时才返回True。

再次,您可以在REPL环境中使用运行以下代码:

在上面的代码中,您已经将5赋给x。如果您尝试使用add修改此值,那么您将获得一个新对象:

上面的代码似乎修改了x的值,但您得到了一个新对象作为响应。

str类型也是不变的:

同样,经过“+=”操作后s最终会有不同的内存地址。

福利:“+=”操作会转换成不同的方法调用。

对于某些对象,如list对象,+=将转换为__iadd__()(就地添加)。这将修改self并返回相同的ID。但是,str和int对象没有这些方法,这就导致它们调用的是__add__()而不是__iadd__()。

有关更多详细信息,请查看Python 数据模型文档。

试图直接改变字符串s会导致错误:

上面的代码失败了,这表明str不支持这种突变,这与str类型是不可变的定义一致。

与可变对象作对比,例如list类型:

此代码显示了两种类型对象的主要区别。”my_list“最初有一个id。即使在4被附加到列表后,”my_list“也具有相同的 ID。这是因为list类型是可变的。

证明列表可变的另一种方法是赋值:

在此代码中,您可以改变“my_list”,将其第一个元素设置为0。但是,即使在赋值之后,它仍保持原有的ID。随着可变和不可变对象的出现,Python启蒙之旅的下一步是理解Python的变量生态系统。

了解变量

Python变量在根本上与C或C ++中的变量不同。事实上,Python甚至没有变量。Python有名称,而不是变量。

这可能看起来很迂腐,而且在大多数情况下就是迂腐。大多数时候,将Python名称视为变量是完全可以接受的,但理解差异很重要。当您在Python中探寻棘手的指针主题时尤为重要。

为了帮助理解差异,您可以了解变量如何在C中工作,它们代表什么,然后将其与名称在Python中的工作方式进行对比。

C中的变量

假设您用以下代码来定义变量x:

这一行代码在执行时有几个不同的步骤:

1. 为整数分配足够的内存

2. 将值分配2337给该内存位置

3. 指示x指向该值

以简化的内存视图显示,它可能如下所示:

在这里,您可以看到该变量x具有伪内存位置0x7f1和值2337。如果在程序中稍后要更改其x的值,则可以执行以下操作:

上面的代码给变量x分配了一个新的值2338,从而覆盖了以前的值。这意味着变量x是可变的。更新的内存布局显示新值:

请注意,x的位置没有改变,只是改变了值。这是一个重要的观点。这意味着x 是内存位置,而不仅仅是名称。

另一种思考这个概念的方法是在所有权方面。从某种意义上说,x拥有内存位置。首先,x恰好是一个可以存储整数的空盒子,可以用来存储整数值。

当您给x赋值时,您将向x拥有的盒子中放入一个值。如果你想引入一个新的变量(y),你可以添加这行代码:

此代码创建一个名为y的盒子,并将x的值复制到y盒子中。现在内存布局将如下所示:

注意新位置0x7f5的y。即使将x的值复制到y,但是变量y在内存中拥有新地址。因此,您可以覆盖y的值而不影响x的值:

现在内存布局将如下所示:

同样,你修改的是y的值,而不是它的位置。此外,您始终没有影响原始的x变量。这与Python名称的工作方式形成鲜明对比。

Python中的名称

Python没有变量。它有名字。是的,这是一个迂腐点,你当然可以随意使用术语变量。重要的是要知道变量和名称之间存在差异。

让我们根据上面的C示例获取等效代码并将其写在Python中:

与C类似,上面的代码在执行过程中分解为几个不同的步骤:

1.创建一个 PyObject

2.将PyObject的typecode设置为整数 PyObject

3.将PyObject的值设置为2337

4.创建一个名称 x

5.将x指向新的PyObject

6.将PyObject引用计数增加1

注意:这里的PyObject与Python的对象不一样。它于CPython特有的并表示所有Python对象的基本结构。

PyObject被定义为C结构,所以,如果你想知道为什么你不能调用typecode或refcount,这是因为你没有权限直接进入结构。方法调用如sys.getrefcount()可以帮助您获得一些内部情况。

在内存中,它可能看起来像这样:

您可以看到内存布局与之前的C布局截然不同。在这里,新创建的Python对象拥有值2337所在的内存,而不是x拥有值2337所在的内存。Python名称x不直接拥有任何内存地址,不像C变量在内存中拥有静态插槽。

如果您尝试为x赋新的值,可以尝试以下操作:

这里发生的事情与C的同样操作不同,但与Python中的原始绑定没有太大区别。

这行代码:

· 创建一个新的 PyObject

· 将PyObject的typecode设置为整数

· 将PyObject的值设置为2338

· 将x指向新的PyObject

· 将新的PyObject引用计数增加1

· 将旧的PyObject引用计数减少1

现在在内存中,它看起来像这样:

此图有助于说明x指向对象的引用,并不像以前那样拥有内存空间。它还表明命令“x = 2338”不是赋值,而是将名称x绑定到一个引用。

此外,前一个对象(拥有值2337)现在位于内存中,引用计数为0,并将被垃圾收集器清理。

您可以引入一个新名称y,就如C的示例一样:

在内存中,您将拥有一个新名称,但不一定是新对象:

现在,你可以看到并没有创建一个新的Python对象,只是创建指向同一个对象的新名称。此外,对象的引用参数增加了1。您可以检查对象标识来确认它们是否相同:

上面的代码表明x和y是相同的对象。没错:y仍然是不可改变的。

例如,您可以对有y执行以下操作:

添加调用后,将返回一个新的Python对象。现在,内存看起来像这样:

一个新对象被创建,y现在指向新对象。有趣的是,如果你已经将2339绑定到y,结束状态也是如此:

上述语句导致与添加相同的结束内存状态。回顾一下,在Python中,您不需要分配变量。而是将名称绑定到引用。

关于Python中的预实现对象的注释

现在您已经了解了如何创建Python对象并将名称绑定到这些对象,现在是时候在机器中抛出一把扳手了。该扳手叫做预实现对象。

假设您有以下Python代码:

如上所述,x和y这两个名字都指向同一个Python对象。但是保存1000的Python对象并不能保证总是具有相同的内存地址。例如,如果将两个数字相加以获得1000,则最终会得到一个不同的内存地址:

这一次,"x is y"返回False。如果这令人困惑,别担心。以下是执行此代码时发生的步骤:

1.创建Python对象(1000)

2.将名称分配x给该对象

3.创建Python对象(499)

4.创建Python对象(501)

5.将这两个对象一起添加

6.创建一个新的Python对象(1000)

7.将名称分配y给该对象

技术说明:只有在REPL中执行此代码时,才会执行上述步骤。如果您采用上面的示例,将其粘贴到一个文件中,然后运行该文件,那么您会发现"x is y"将返回True。

这是因为编译器很聪明。CPython编译器尝试进行称为窥孔优化的优化,这有助于尽可能地保存执行步骤。有关详细信息,您可以查看CPython的窥孔优化器源代码。

这不是浪费吗?嗯,是的,这是你为Python所有巨大好处付出的代价。您永远不必担心如何清理这些中间对象,甚至都不需要知道它们存在!令人高兴的是,这些操作相对较快,并且直到现在你都不需要去理解这些细节。

Python核心开发人员也睿智地注意到了这种浪费,并决定进行一些优化。这些优化产生了令新手感到惊讶的行为:

在此示例中,您看到的代码几乎与以前相同,除了这次结果是True。这是预实现对象的结果。Python在内存中预先创建了某个对象子集,并将它们保存在全局命名空间中以供日常使用。

哪些对象依赖于Python的预实现。CPython 3.7预实现对象如下:

1.-5到256之间的整数

2.仅包含ASCII字母,数字或下划线的字符串

这背后的原因是这些变量很可能在许多程序中使用。通过预先实现些对象,Python可以防止对一致使用的对象进行内存分配调用。

预先实现小于20个字符且包含ASCII字母,数字或下划线的字符串。背后的原因是假设这些字符串是某种身份:

在这里您可以看到s1和s2都指向相同的内存地址。如果您要引入非ASCII字母,数字或下划线组成的字符串,那么您将得到不同的结果:

因为此示例中包含感叹号“!”,所以这些字符串不会被预先实现,并且s1和s2是内存中的不同对象。

福利:如果您真的希望这些对象引用相同的内部对象,那么您可能需要查看sys.intern()。文档中概述了此功能的一个用例:

预先实现的字符串对于在字典查找中获得一点性能很有用 - 如果字典中的键被预先实现,并且查找键被预先实现,则键比较(完成在散列之后)就可以通过指针来比较而不是用字符串来比较。(来源)

预实现对象通常是混乱的来源。请记住,如果您有任何疑问,可以随时使用id()和is确定对象是否相同。

在Python中模拟指针

仅仅因为Python中的指针本身不存在并不意味着你无法获得使用指针的好处。实际上,可以有多种方法在Python中模拟指针。您将在本节中学习到两种:

1.使用可变类型作为指针

2.使用自定义Python对象

好的,让我们进入正题。

使用可变类型作为指针

您已经了解过可变类型。因为这些对象是可变的,所以您可以将它们视作指针,以此来模拟指针行为。假设您复制了以下c代码:

此代码将一个指针指向一个整数(*x),然后将其值增加1。这有一个运行代码的主函数:

在上面的代码中,将值2337赋给y,打印出当前值,将值增加1,然后打印出修改后的值。执行此代码的输出如下:

在Python中模仿此类行为的一种方法是使用可变类型。考虑使用列表并修改第一个元素:

在这里,add_one(x)访问第一个元素并将其值增加1。通过使用列表,最终似乎已修改了该值。那么Python中的指针确实存在吗?好吧,不。唯一的可能是:因为列表是一种可变类型。如果您尝试使用一个元组,则会收到错误消息:

上面的代码演示了元组是不可变的。因此,它不支持项目赋值。列表不是唯一可变的类型。在Python中模仿指针的另一种常见方法是创建字典。

假设您有一个应用程序,您希望每次发生有趣事件时都要跟踪。实现此目的的另一种方法是创建一个字典 并使用其中的一项作为计数器:

在此示例中,counters字典用于跟踪函数调用的数量。调用foo()函数后,计数器按预期增加到2。这都是因为字典是可变类型。

请记住,这只是模拟指针行为,并不直接映射到C或C ++中的真指针。也就是说,这些操作在Python中会比在C或C ++中付出更多代价。

使用Python对象

使用字典是在Python中模拟指针的一种好方法,但有时您需要记住使用的密钥名称,这会很繁琐。如果您在应用程序的各个部分都使用字典,则尤其如此。这就是自定义Python类可以真正起到作用的地方。

构建最后一个示例,假设您要跟踪应用程序中的指标。创建一个类是解决那些讨厌的抽象细节的好方法:

此代码定义了一个Metrics类。该类仍然使用字典来保存实际数据,该数据位于_metrics成员变量中。这将为您提供所需的可变性。现在您只需要能够访问这些值。一个很好的方法是使用属性:

这段代码利用了@property。如果您不熟悉装饰器,可以查看Python装饰器入门。@property装饰器允许您访问func_calls,cat_pictures_served,它们就像属性一样:

您可以把名称当作属性访问这一事实,意味着您已抽象了一个事实:这些值在字典中。您还可以更明确地指出属性的名称是什么。当然,您应该能够增加这些值:

您已了解了两种新方法:

1.inc_func_calls()

2.inc_cat_pics()

这些方法能够修改类中字典的值。您现在有一个类可以修改,就像您正在修改指针一样:

这样,您就可以在应用程序中的各个位置访问func_calls和调用inc_func_calls(),并在Python中模拟指针。当您需要在应用程序的各个部分中频繁使用和更新指针时,这非常有用。

注意:特别是在这个类中,使用inc_func_calls()和inc_cat_pics()更为清楚明白,而不是使用@property.setter,这能阻止用户将这些值设置为任意的整型或无效的值,如字典。

这是Metrics类的完整代码:

使用ctypes模块实现真实指针

好吧,也许Python中有指针,特别是CPython。使用内置ctypes模块,您可以在Python中创建真正的C风格指针。如果您不熟悉ctypes,那么您可以查看使用C库扩展Python和“ctypes”模块。

你要使用它的真正原因是你需要对C库创建一个需要指针的函数调用。让我们回到之前的C函数add_one():

同样,这段代码将x的值增加1。要使用它,首先将其编译为共享对象。假设上述代码存储在add.c文件中,您可以通过gcc来完成以下操作:

第一个命令将C源文件编译为一个名为add.o的对象。第二个命令获取该未链接的目标文件并生成一个名为libadd1.so的共享对象。

libadd1.so应该在您当前的目录中。您可以使用ctypes的命令将其加载到Python:

代码ctypes.CDLL返回一个代表libadd1的共享对象。因为您add_one()在此共享对象中定义,所以您可以像访问其他任何Python对象一样访问它。在调用该函数之前,您应该指定函数签名。这有助于Python确保将正确的类型传递给函数。

在这种情况下,函数签名是指向整数的指针。ctypes允许您使用以下代码来指定:

在此代码中,您设置函数签名以匹配C所期望的内容。现在,如果您尝试使用错误的类型调用此代码,那么您将得到一个很好的警告而不是未定义的行为:

Python抛出一个错误,解释说add_one()想要一个指针而不是一个整数。幸运的是,ctypes有一种方法可以将指针传递给这些函数。首先,声明一个C风格的整数:

上面的代码创建了一个C风格的整数x,其值为0。ctypes提供方便的byref()方法,它允许通过引用来传递变量。

注意:传递变量时,术语"通过引用"与"通过值"相反。

通过引用传递时,您将引用传递给原始变量,因此修改将反映在原始变量中。按值传递会生成原始变量的副本,并且修改不会反映在原始变量中。

你可以用下面的代码来调用add_one():

太好了!你的整数加1。恭喜,您已成功使用Python的真实指针。

总结

您现在对Python对象和指针之间的关系有了更好的理解。尽管名称和变量之间的某些区别似乎很迂腐,但从根本上理解这些关键术语可以扩展您对Python如何处理变量的理解。

您还学习了一些在Python中模拟指针的好方法:

· 利用可变对象作为低开销指针

· 创建自定义Python对象以便于使用

· 使用ctypes模块的解锁真实指针

这些方法允许您在Python中模拟指针,而且不会牺牲Python提供的内存安全性。

感谢您的阅读。如果您仍有疑问,请随时在评论部分或Twitter上与我联系。

英文原文:https://realpython.com/pointers-in-python

译者:ZH

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20190810A03TXA00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券