社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

Python的指针:有什么意义?

Python程序员 • 6 年前 • 1118 次点击  


目录

  •  为什么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指针的第一个关键。以下是常见类型的细分以及它们是否可变或不可变:

类型

不可变?

int

Yes

float

Yes

bool

Yes

complex

Yes

tuple

Yes

frozenset

Yes

str

Yes

list

No

set

No

dict

No

如您所见,许多常用的基元类型是不可变的。您可以通过编写一些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


Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/37382