Python中的指针:有什么意义?
Posted sha777
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python中的指针:有什么意义?相关的知识,希望对你有一定的参考价值。
如果您曾经使用过C或C ++等低级语言,那么您可能已经听说过指针。指针允许您在部分代码中创建高效率。它们也会给初学者带来困惑,并且可能导致各种内存管理错误,即使对于专家也是如此。那么它们在Python中的位置,以及如何在Python中模拟指针?
为什么Python没有指针?
事实是我不知道。Python中的指针本身可以存在吗?可能,但指针似乎违背了Python的禅宗。指针鼓励隐含的变化而不是明确的变化。通常,它们很复杂而不是简单,特别是对于初学者。更糟糕的是,他们乞求用脚射击自己的方法,或做一些非常危险的事情,比如从你不应该的一段记忆中读取。
Python倾向于尝试从用户那里抽象出内存地址等实现细节。Python通常关注可用性而不是速度。因此,Python中的指针并没有多大意义。但不要害怕,默认情况下,Python会为您提供使用指针的一些好处。
理解Python中的指针需要简要介绍Python的实现细节。具体来说,您需要了解:
- 不可变对象和可变对象
- Python变量/名称
保留你的内存地址,让我们开始吧。
Python中的对象
在Python中,一切都是对象。为了证明,你可以打开一个REPL并探索使用isinstance():
>>> isinstance(1, object)
True
>>> isinstance(list(), object)
True
>>> isinstance(True, object)
True
>>> def foo():
... pass
...
>>> isinstance(foo, object)
True
此代码向您显示Python中的所有内容确实是一个对象。每个对象至少包含三个数据:
- 参考计数
- 类型
- 值
该引用计数为内存管理。要深入了解Python中的内存管理内部,您可以阅读Python中的内存管理。
该类型在CPython层使用,以确保运行时的类型安全性。最后,有值,即与对象关联的实际值。
但并非所有对象都是相同的。您还需要了解另一个重要的区别:不可变对象和可变对象。理解对象类型之间的差异确实有助于阐明洋葱的第一层是Python中的指针。
不可变对象和可变对象
在Python中,有两种类型的对象:
- 不可变对象无法更改。
- 可以改变可变对象。
理解这种差异是在Python中导航指针的第一个关键。以下是常见类型的细分以及它们是否可变或不可变:
类型不可变?int是float是bool是complex是tuple是frozenset是str是list没有set没有dict没有
如您所见,许多常用的基元类型是不可变的。您可以通过编写一些Python来证明这一点。您需要Python标准库中的一些工具:
- id() 返回对象的内存地址。
- isTrue当且仅当两个对象具有相同的内存地址时才返回。
再一次,您可以在REPL环境中使用它们:
>>>
>>> x = 5
>>> id (x )
94529957049376
在上面的代码中,已分配的值5来x。如果您尝试使用add修改此值,那么您将获得一个新对象:
>>>
>>> x + = 1
>>> x
6
>>> id (x )
94529957049408
即使上面的代码似乎修改了值x,你也会得到一个新对象作为响应。
该str类型也是不变的:
>>> s = "real_python"
>>> id(s)
140637819584048
>>> s += "_rocks"
>>> s
‘real_python_rocks‘
>>> id(s)
140637819609424
同样,操作后s最终会有不同的内存地址+=。
奖励:在+=运营商转换到不同的方法调用。
对于某些对象list,+=将转换为__iadd__()(就地添加)。这将修改self并返回相同的ID。但是,str并int没有这些方法而导致__add__()调用而不是__iadd__()。
有关更多详细信息,请查看Python 数据模型文档。
试图直接改变字符串会s导致错误:
>>> s[0] = "R"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: ‘str‘ object does not support item assignment
上面的代码失败了,Python表明它str不支持这种突变,这与该str类型是不可变的定义一致。
与可变对象形成对比,例如list:
>>> my_list = [ 1 , 2 , 3 ]
>>> ID (my_list )
140637819575368
>>> my_list 。append (4 )
>>> my_list
[1,2,3,4]
>>> id (my_list )
140637819575368
此代码显示了两种类型对象的主要区别。my_list最初有一个id。即使在4附加到列表后,my_list也具有相同的 ID。这是因为list类型是可变的。
证明列表可变的另一种方法是赋值:
>>> my_list [ 0 ] = 0
>>> my_list
[0,2,3,4 ] >>> id (my_list )
140637819575368
在此代码中,您my_list将其第一个元素变异并设置为0。但是,即使在此分配之后,它仍保持相同的ID。随着可变和不可变对象的出现,Python启蒙之旅的下一步是理解Python的变量生态系统。
了解变量
Python变量与C或C ++中的变量根本不同。事实上,Python甚至没有变量。Python有名字,而不是变量。
这可能看起来很迂腐,而且大多数情况下都是如此。大多数时候,将Python名称视为变量是完全可以接受的,但理解差异很重要。当您在Python中导航棘手的指针主题时尤其如此。
为了帮助推动差异,您可以了解变量如何在C中工作,它们代表什么,然后将其与名称在Python中的工作方式进行对比。
C中的变量
假设您有以下代码来定义变量x:
int x = 2337 ;
这一行代码在执行时有几个不同的步骤:
- 为整数分配足够的内存
- 将值分配2337给该内存位置
- 指示x指向该值
以简化的内存视图显示,它可能如下所示:
在这里,您可以看到该变量x具有伪内存位置0x7f1和值2337。如果在程序中稍后要更改其值x,则可以执行以下操作:
x = 2338 ;
上面的代码为2338变量分配了一个新的value()x,从而覆盖了以前的值。这意味着变量x是可变的。更新的内存布局显示新值:
请注意,位置x没有改变,只是值本身。这是一个重要的观点。这意味着它x 是内存位置,而不仅仅是它的名称。
另一种思考这个概念的方法是在所有权方面。从某种意义上说,x拥有内存位置。x首先,它是一个空盒子,它可以恰好适合一个可以存储整数值的整数。
为值分配值时x,您将在所x拥有的框中放置一个值。如果你想引入一个新的变量(y),你可以添加这行代码:
int y = x ;
此代码创建一个名为的新框y,并将值复制x到框中。现在内存布局将如下所示:
注意新的位置0x7f5的y。即使将值x复制到y,变量也会y在内存中拥有一些新地址。因此,您可以覆盖y不影响的值x:
y = 2339 ;
现在内存布局将如下所示:
同样,你修改的值y,但不是它的位置。此外,您根本没有影响原始x变量。这与Python名称的工作方式形成鲜明对比。
Python中的名称
Python没有变量。它有名字。是的,这是一个迂腐点,你当然可以随意使用术语变量。重要的是要知道变量和名称之间存在差异。
让我们从上面的C示例中获取等效代码并将其写在Python中:
>>> x = 2337
与C类似,上面的代码在执行过程中分解为几个不同的步骤:
- 创建一个 PyObject
- 将typecode设置为整数 PyObject
- 将值设置2337为PyObject
- 创建一个名为 x
- 指向x新的PyObject
- 增加的引用计数PyObject被1
注意:这PyObject与Python不同object。它特定于CPython并表示所有Python对象的基本结构。
PyObject被定义为C结构,所以,如果你想知道为什么你不能打电话typecode或refcount直接,其因为你没有进入结构直接。方法调用sys.getrefcount()可以帮助获得一些内部。
在内存中,它可能看起来像这样:
您可以看到内存布局与之前的C布局截然不同。新创建的Python对象拥有生命中的内存,而不是x拥有值2337所在的内存块2337。Python名称x不像C变量在内存中拥有静态插槽那样直接拥有任何内存地址x。
如果您尝试为其分配新值x,可以尝试以下操作:
>>> x = 2338
这里发生的事情与C等价物不同,但与Python中的原始绑定没有太大区别。
这段代码:
- 创建一个新的 PyObject
- 将typecode设置为整数 PyObject
- 设置值2338的PyObject
- 点x新PyObject
- 将new的refcount增加PyObject1
- 将旧的refcount减少PyObject1
现在在内存中,它看起来像这样:
此图有助于说明x指向对象的引用,并且不像以前那样拥有内存空间。它还显示该x = 2338命令不是赋值,而是将名称绑定x到引用。
此外,前一个对象(持有该2337值)现在位于内存中,引用计数为0,并将被垃圾收集器清理。
您可以y在C示例中引入一个新名称:
>>> y = x
在内存中,您将拥有一个新名称,但不一定是新对象:
现在,你可以看到一个新的Python对象没有被创建,只是指向同一个对象的新名称。此外,对象的refcount增加了一个。您可以检查对象标识是否相同,以确认它们是否相同:
>>> y is x
True
上面的代码表明x和y是相同的对象。不过没错:y它仍然是不可改变的。
例如,您可以执行以下操作y:
>>>
>>> y + = 1
>>> y is x
False
添加调用后,将返回一个新的Python对象。现在,内存看起来像这样:
已创建新对象,y现在指向新对象。有趣的是,这是如果你已经绑定到的最终状态y,以2339直接:
>>> y = 2339
上述语句导致与添加相同的结束内存状态。回顾一下,在Python中,您不需要分配变量。而是将名称绑定到引用。
关于Python中实习生对象的注释
现在您已经了解了如何创建Python对象并将名称绑定到这些对象,现在是时候在机器中抛出一把扳手了。该扳手以实习对象的名称命名。
假设您有以下Python代码:
>>> x = 1000
>>> y = 1000
>>> x is y
True
如上所述,x并且y都指向同一个Python对象这两个名字。但是保存该值的Python对象1000并不总是保证具有相同的内存地址。例如,如果要将两个数字相加以获得1000,则最终会得到不同的内存地址:
>>> x = 1000
>>> y = 499 + 501
>>> x is y
False
这一次,该行x is y返回False。如果这令人困惑,那么别担心。以下是执行此代码时发生的步骤:
- 创建Python对象(1000)
- 将名称分配x给该对象
- 创建Python对象(499)
- 创建Python对象(501)
- 将这两个对象一起添加
- 创建一个新的Python对象(1000)
- 将名称分配y给该对象
技术说明:只有在REPL中执行此代码时,才会执行上述步骤。如果您采用上面的示例,将其粘贴到一个文件中,然后运行该文件,那么您会发现该x is y行将返回True。
这是因为编译器很聪明。CPython编译器尝试进行称为窥孔优化的优化,这有助于尽可能地保存执行步骤。有关详细信息,您可以查看CPython的窥孔优化器源代码。
这不是浪费吗?嗯,是的,但这是你为Python的所有巨大好处付出的代价。您永远不必担心清理这些中间对象,甚至需要知道它们存在!令人高兴的是,这些操作相对较快,直到现在你都不必知道任何这些细节。
他们的智慧核心Python开发人员也注意到了这种浪费,并决定进行一些优化。这些优化会导致新手的行为令人惊讶:
>>> x = 20
>>> y = 19 + 1
>>> x is y
True
在此示例中,您看到的代码几乎与以前相同,但这次结果是True。这是实习对象的结果。Python在内存中预先创建了某个对象子集,并将它们保存在全局命名空间中以供日常使用。
哪些对象依赖于Python的实现。CPython 3.7实习生如下:
- -5和之间的整数256
- 仅包含ASCII字母,数字或下划线的字符串
这背后的原因是这些变量很可能在许多程序中使用。通过实习这些对象,Python可以防止对一致使用的对象进行内存分配调用。
将使用小于20个字符且包含ASCII字母,数字或下划线的字符串。这背后的原因是假设这些是某种身份:
>>> s1 = “realpython”
>>> id (s1 )
140696485006960
>>> s2 = “realpython”
>>> id (s2 )
140696485006960
>>> s1 is s2
True
在这里你可以看到它s1并且s2都指向内存中的相同地址。如果您要引入非ASCII字母,数字或下划线,那么您将得到不同的结果:
>>> s1 = “真正的Python!”
>>> s2 = “真正的Python!”
>>> s1 is s2
False
因为此示例中包含感叹号(!),所以这些字符串不会被实现,并且是内存中的不同对象。
额外奖励:如果您真的希望这些对象引用相同的内部对象,那么您可能需要查看sys.intern()。文档中概述了此功能的一个用例:
实习字符串对于在字典查找中获得一点性能很有用 - 如果字典中的键被中断,并且查找键被实现,则可以通过指针比较而不是字符串比较来进行键比较(在散列之后)。(来源)
实体对象通常是混乱的来源。请记住,如果您有任何疑问,可以随时使用id()并is确定对象相等性。
在Python中模拟指针
仅仅因为Python中的指针本身不存在并不意味着你无法获得使用指针的好处。实际上,有多种方法可以在Python中模拟指针。您将在本节中学习两个:
- 使用可变类型作为指针
- 使用自定义Python对象
好的,让我们谈谈。
使用可变类型作为指针
您已经了解了可变类型。因为这些对象是可变的,所以您可以将它们视为指向模拟指针行为的指针。假设您要复制以下c代码:
void add_one (int * x )
* x + = 1 ;
此代码采用指向整数(*x)的指针,然后将该值递增1。这是一个练习代码的主要功能:
#include <stdio.h>
int main (void )
int y = 2337 ;
printf (“y =%d n ” , y );
add_one (&y );
printf (“y =%d n ” , y );
返回 0 ;
在上面的代码中,分配2337给y,打印出当前值,将值递增1,然后打印出修改后的值。执行此代码的输出如下:
y = 2337
y = 2338
在Python中复制此类行为的一种方法是使用可变类型。考虑使用列表并修改第一个元素:
>>> def add_one (x ):
... x [ 0 ] + = 1
...
>>> y = [ 2337 ]
>>> add_one (y )
>>> y [ 0 ]
2338
在这里,add_one(x)访问第一个元素并将其值增加1。使用一种list方法,最终结果似乎已修改了该值。那么Python中的指针确实存在吗?好吧,不。这是唯一可能的,因为它list是一种可变类型。如果您尝试使用a tuple,则会收到错误消息:
>>> z = (2337,)
>>> add_one(z)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in add_one
TypeError: ‘tuple‘ object does not support item assignment
上面的代码演示了这tuple是不可变的。因此,它不支持项目分配。list不是唯一可变的类型。在Python中模仿指针的另一种常见方法是使用a dict。
假设您有一个应用程序,您希望每次发生有趣事件时都要跟踪它。实现此目的的一种方法是创建一个dict并使用其中一个项目作为计数器:
>>> counters = "func_calls": 0
>>> def bar():
... counters["func_calls"] += 1
...
>>> def foo():
... counters["func_calls"] += 1
... bar()
...
>>> foo()
>>> counters["func_calls"]
2
在此示例中,counters字典用于跟踪函数调用的数量。打电话后foo(),计数器已2按预期增加。所有因为dict是可变的。
请记住,这只是模拟指针行为,并不直接映射到C或C ++中的真指针。也就是说,这些操作比在C或C ++中更昂贵。
使用Python对象
该dict选项是在Python中模拟指针的好方法,但有时记住您使用的密钥名称会很繁琐。如果您在应用程序的各个部分使用字典,则尤其如此。这是自定义Python类可以真正帮助的地方。
要构建最后一个示例,假设您要跟踪应用程序中的指标。创建一个类是抽象讨厌细节的好方法:
class Metrics (object ):
def __init __ (self ):
self 。_metrics =
“func_calls” : 0 ,
“cat_pictures_served” : 0 ,
此代码定义了一个Metrics类。该类仍然使用a dict来保存实际数据,该数据位于_metrics成员变量中。这将为您提供所需的可变性。现在您只需要能够访问这些值。一个很好的方法是使用属性:
class Metrics(object):
# ...
@property
def func_calls(self):
return self._metrics["func_calls"]
@property
def cat_pictures_served(self):
return self._metrics["cat_pictures_served"]
这段代码利用了@property。如果您不熟悉装饰器,可以在Python装饰器上查看这个Primer。@property这里的装饰器允许您访问func_calls,cat_pictures_served就像它们是属性一样:
>>> metrics = Metrics()
>>> metrics.func_calls
0
>>> metrics.cat_pictures_served
0
您可以将这些名称作为属性访问这一事实意味着您抽象出这些值在a中的事实dict。您还可以更明确地了解属性的名称。当然,您需要能够增加这些值:
class Metrics (object ):
#...
def inc_func_calls (self ):
self 。_metrics [ “func_calls” ] + = 1
def inc_cat_pics (self ):
self 。_metrics [ “cat_pictures_served” ] + = 1
您介绍了两种新方法:
- inc_func_calls()
- inc_cat_pics()
这些方法会修改指标中的值dict。您现在有一个类可以修改,就像您正在修改指针一样:
>>> metrics = Metrics()
>>> metrics.inc_func_calls()
>>> metrics.inc_func_calls()
>>> metrics.func_calls
2
在这里,您可以在应用程序中的各个位置访问func_calls和调用inc_func_calls(),并在Python中模拟指针。当您需要在应用程序的各个部分中频繁使用和更新指标时,这非常有用。
注意:特别是在这个类中,make inc_func_calls()和inc_cat_pics()explicit而不是使用@property.setter阻止用户将这些值设置为任意int或无效的值,如dict。
完整代码:
class Metrics(object):
def __init__(self):
self._metrics =
"func_calls": 0,
"cat_pictures_served": 0,
@property
def func_calls(self):
return self._metrics["func_calls"]
@property
def cat_pictures_served(self):
return self._metrics["cat_pictures_served"]
def inc_func_calls(self):
self._metrics["func_calls"] += 1
def inc_cat_pics(self):
self._metrics["cat_pictures_served"] += 1
真实指针 ctypes
好吧,也许Python中有指针,特别是CPython。使用内置ctypes模块,您可以在Python中创建真正的C风格指针。如果您不熟悉ctypes,那么您可以查看使用C库扩展Python和“ctypes”模块。
你要使用它的真正原因是你需要对需要指针的C库进行函数调用。让我们回到add_one()之前的C函数:
void add_one (int * x )
* x + = 1 ;
同样,这段代码将值递增x1。要使用它,首先将其编译为共享对象。假设存储了上述文件add.c,您可以通过以下方式完成此操作gcc:
$ gcc -c -Wall -Werror -fpic add.c
$ gcc -shared -o libadd1.so add.o
第一个命令将C源文件编译为一个名为的对象add.o。第二个命令获取该未链接的目标文件并生成一个名为的共享对象libadd1.so。
libadd1.so应该在您当前的目录中。您可以使用ctypes以下命令将其加载到Python :
>>> import ctypes
>>> add_lib = ctypes 。CDLL (“./ libadd1.so” )
>>> add_lib 。add_one
<_FuncPtr对象位于0x7f9f3b8852a0>
的ctypes.CDLL代码返回一个表示一个对象libadd1的共享对象。因为您add_one()在此共享对象中定义,所以您可以像访问任何其他Python对象一样访问它。在调用该函数之前,您应该指定函数签名。这有助于Python确保将正确的类型传递给函数。
在这种情况下,函数签名是指向整数的指针。ctypes将允许您使用以下代码指定:
>>> add_one = add_lib 。add_one
>>> add_one 。argtypes = [ ctypes 。POINTER (ctypes的。c_int的)]
在此代码中,您将设置函数签名以匹配C期望的内容。现在,如果您尝试使用错误的类型调用此代码,那么您将得到一个很好的警告而不是未定义的行为:
>>> add_one(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ctypes.ArgumentError: argument 1: <class ‘TypeError‘>:
expected LP_c_int instance instead of int
Python抛出一个错误,解释说add_one()想要一个指针而不是一个整数。幸运的是,ctypes有一种方法可以将指针传递给这些函数。首先,声明一个C风格的整数:
>>> x = ctypes 。c_int ()
>>> x
c_int(0)
上面的代码创建了一个C样式的整数x,其值为0。ctypes提供方便byref()允许通过引用传递变量。
注意:引用的术语与按值传递变量相反。
通过引用传递时,您将引用传递给原始变量,因此修改将反映在原始变量中。按值传递会生成原始变量的副本,并且修改不会反映在原始变量中。
你可以用它来打电话add_one():
>>> add_one (ctypes的。按地址(X ))
998793640
>>> X
c_int的(1)
太好了!你的整数加1。恭喜,您已成功使用Python中的实际指针。
结论
您现在可以更好地理解Python对象和指针之间的交集。尽管名称和变量之间的某些区别似乎很迂腐,但从根本上理解这些关键术语可以扩展您对Python如何处理变量的理解。
您还学习了一些在Python中模拟指针的好方法:
- 利用可变对象作为低开销指针
- 创建自定义Python对象以便于使用
- 解锁ctypes模块的真实指针
这些方法允许您在Py??thon中模拟指针,而不会牺牲Python提供的内存安全性。
以上是关于Python中的指针:有什么意义?的主要内容,如果未能解决你的问题,请参考以下文章