Python中的指针:有什么意义?

Posted sha777

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python中的指针:有什么意义?相关的知识,希望对你有一定的参考价值。

如果您曾经使用过C或C ++等低级语言,那么您可能已经听说过指针。指针允许您在部分代码中创建高效率。它们也会给初学者带来困惑,并且可能导致各种内存管理错误,即使对于专家也是如此。那么它们在Python中的位置,以及如何在Python中模拟指针?

技术图片

 

为什么Python没有指针?

事实是我不知道。Python中的指针本身可以存在吗?可能,但指针似乎违背了Python的禅宗。指针鼓励隐含的变化而不是明确的变化。通常,它们很复杂而不是简单,特别是对于初学者。更糟糕的是,他们乞求用脚射击自己的方法,或做一些非常危险的事情,比如从你不应该的一段记忆中读取。

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

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

  1. 不可变对象和可变对象
  2. 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中,有两种类型的对象:

  1. 不可变对象无法更改。
  2. 可以改变可变对象。

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

类型不可变?int是float是bool是complex是tuple是frozenset是str是list没有set没有dict没有

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

  1. id() 返回对象的内存地址。
  2. 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 ;

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

  1. 为整数分配足够的内存
  2. 将值分配2337给该内存位置
  3. 指示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类似,上面的代码在执行过程中分解为几个不同的步骤:

  1. 创建一个 PyObject
  2. 将typecode设置为整数 PyObject
  3. 将值设置2337为PyObject
  4. 创建一个名为 x
  5. 指向x新的PyObject
  6. 增加的引用计数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。如果这令人困惑,那么别担心。以下是执行此代码时发生的步骤:

  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开发人员也注意到了这种浪费,并决定进行一些优化。这些优化会导致新手的行为令人惊讶:

>>> x = 20 
>>> y = 19 + 1
>>> x is y
True

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

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

  1. -5和之间的整数256
  2. 仅包含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中模拟指针。您将在本节中学习两个:

  1. 使用可变类型作为指针
  2. 使用自定义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

您介绍了两种新方法:

  1. inc_func_calls()
  2. 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中的指针:有什么意义?的主要内容,如果未能解决你的问题,请参考以下文章

直观地解释指针及其意义?

一个片段的活动有啥意义吗?

片段中的ArrayAdapter空指针

片段中的 EditText 上的空指针异常 [重复]

活动到片段通信:当我尝试从活动更新片段中的文本视图时,出现空指针异常

如果 GIL 存在,Python 中的多线程有啥意义?