Python 通过 ref 调用 使用 ctypes 按值调用

Tim*_*man 3 python ctypes

我正在尝试编写一个程序来向 A-level 学生说明使用 Python 进行引用调用和按值调用之间的区别。我成功地将可变对象作为变量传递给函数,但发现我也可以使用 ctypes 库执行相同的操作。

我不太明白它是如何工作的,因为byref()ctype 库中有一个函数,但它在我的示例中不起作用。然而,通过调用一个没有byref()它的函数确实有效!

我的工作代码:

"""
Program to illustrate call by ref
"""

from  ctypes import *  #allows call by ref

test = c_int(56)  #Python call by reference eg address
t = 67            #Python call by value eg copy


#expects a ctypes argument
def byRefExample(x):
    x.value= x.value + 2
    

#expects a normal Python variable
def byValueExample(x):
    x = x + 2
         

if __name__ == "__main__":

    print "Before call test is",test
    byRefExample(test)                
    print "After call test is",test

    print "Before call t is",t
    byValueExample(t)
    print "After call t is",t
Run Code Online (Sandbox Code Playgroud)

问题

当传递一个普通的 Python 变量时,byValueExample()它会按预期工作。函数参数的副本t发生变化,但标头中的变量t没有变化。但是,当我通过 ctypes 变量测试时,本地变量和标头变量都会发生变化,因此它的作用就像 C 指针变量。虽然我的程序可以工作,但我不确定该byref()函数在像这样使用时如何以及为什么不起作用:

byRefExample(byref(test))
Run Code Online (Sandbox Code Playgroud)

aba*_*ert 5

您实际上使用的术语并不完全正确,并且可能非常具有误导性。我会在最后解释。但首先我会根据你的措辞来回答。

\n\n
\n\n
\n

我成功地将可变对象作为变量传递给函数,但发现我也可以使用 ctypes 库执行相同的操作。

\n
\n\n

那是因为这些ctypes对象是可变对象,所以你只是在做你已经做过的同样的事情。特别是, actypes.c_int是一个保存整数值的可变对象,您可以通过设置其value成员来改变它。所以你已经在做与没有ctypes.

\n\n

更详细地比较这些:

\n\n
def by_ref_using_list(x):\n    x[0] += 1\nvalue = [10]\nby_ref_using_list(value)\nprint(value[0])\n\ndef by_ref_using_dict(x):\n    x[\'value\'] += 1\nvalue = {\'value\': 10}\nby_ref_using_list(value)\nprint(value[\'value\'])\n\nclass ValueHolder(object):\n    def __init__(self, value):\n        self.value = value\ndef by_ref_using_int_holder(x):\n    x.value += 1\nvalue = ValueHolder(10)\nby_ref_using_list(value)\nprint(value.value)\n
Run Code Online (Sandbox Code Playgroud)\n\n

您会期望所有这三个都打印出 11,因为它们只是传递不同类型的可变对象并改变它们的三种不同方式。

\n\n

这正是您正在做的事情c_int

\n\n

您可能想阅读常见问题解答如何编写带有输出参数的函数(通过引用调用)?,尽管您似乎已经知道那里的答案,并且只是想知道如何ctypes适合 \xe2\x80\xa6

\n\n
\n\n

那么,byref究竟是为了什么?

\n\n

它用于调用通过引用 C 风格获取值的 C 函数:通过使用显式指针类型。例如:

\n\n
void by_ref_in_c(int *x) {\n    *x += 1;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

您不能将其传递为c_int对象,因为它需要一个指向c_int. 并且您不能向其传递未初始化的值POINTER(c_int),因为那样它只会写入随机内存。您需要获取指向实际c_int. 你可以这样做:

\n\n
x = c_int(10)\nxp = pointer(x)\nby_ref_in_c(xp)\nprint(x)\n
Run Code Online (Sandbox Code Playgroud)\n\n

效果很好。但这是多余的,因为您已经创建了一个额外的 Pythonctypes对象,xp而您实际上并不需要任何东西。这就是它的用途byref:它为您提供了一个指向对象的轻量级指针,该指针只能用于通过引用传递该对象:

\n\n
x = c_int(10)\nby_ref_in_c(byref(x))\nprint(x)\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

这解释了为什么这不起作用:

\n\n
byRefExample(byref(test))\n
Run Code Online (Sandbox Code Playgroud)\n\n

该调用正在创建一个指向 的轻量级指针test,并将该指针传递给byRefExample。但byRefExample不想要一个指向 a 的指针c_int,它想要一个c_int.

\n\n

当然,这都是用 Python 编写的,而不是 C 语言,所以没有进行静态类型检查。函数调用工作得很好,并且您的代码不关心它得到什么类型,只要它有一个value可以递增的成员即可。但 aPOINTER没有成员value。(它有一个contents成员。)因此,您会尝试AttributeError访问x.value.

\n\n
\n\n

那么,你如何这种事情呢?

\n\n

嗯,使用单元素列表是一种众所周知的技巧,可以绕过这样一个事实:您需要共享一些可变的东西,但只有一些不可变的东西。如果您使用它,经验丰富的 Python 程序员就会知道您在做什么。

\n\n

话虽这么说,如果您认为需要这个,那么您通常是错的。通常正确的答案是返回新值。推理不改变任何东西的函数会更容易。您可以按照您想要的任何方式将它们串在一起,使用生成器和迭代器将它们从里到外翻转,将它们发送到子进程以利用 CPU 中的额外内核等。即使您不执行任何操作对于这些东西,返回一个新值通常比就地修改一个值更快,即使在您不希望发生的情况下(例如,删除列表中 75% 的值)。

\n\n

通常,当您确实需要可变值时,它们已经有一个明显的位置,例如类的实例属性。

\n\n

但有时您确实需要单元素列表 hack,所以它值得您拥有;只是当你不需要它时不要使用它。

\n\n
\n\n

那么,您的术语有什么问题吗?

\n\n

从某种意义上说(Ruby 和 Lisp 程序员使用的意义), Python 中的所有内容都是按引用传递的。从另一种意义上来说(许多 Java 和 VB 程序员使用的意义),它都是按值传递的。但实际上,最好也不要调用它。*您传递的既不是变量值的副本,也不是对变量的引用,而是对值的引用。当您调用该byValueExample(t)函数时,您不会67像在 C 中那样传递带有值的新整数,而是传递对绑定到 name 的同一整数的引用。如果你可以改变(你不能,因为整数是不可变的),调用者就会看到变化。67t67

\n\n

其次,Python 名称甚至不是您所想的意义上的变量。在 C 中,变量是一个lvalue. 它有一个类型,更重要的是,有一个地址。因此,您可以传递对变量本身的引用,而不是对其值的引用。在 Python 中,名称只是一个名称(通常是模块、本地或对象字典中的键)。它没有类型或地址。这不是你可以传递的东西。因此,无法x通过引用传递变量。**

\n\n

最后,=在 Python 中,不是将值复制到变量的赋值运算符;而是将值复制到变量的赋值运算符。它是一个绑定运算符,为值提供名称。因此,在 C 中,当您编写时x = x + 1,会将值复制x + 1到变量的位置x,但在 Python 中,当您编写时x = x + 1,只会重新绑定局部变量x以引用新值x + 1。这不会对x曾经绑定的任何值产生任何影响。(好吧,如果它是对该值的唯一引用,垃圾收集器可能会清理它\xe2\x80\xa6\xc2\xa0,但仅此而已。)

\n\n

如果您来自 C++,这实际上更容易理解,这确实迫使您了解右值和左值以及不同类型的引用以及复制构造与复制赋值等等\xe2\x80\xa6 在 C 中,它看起来很简单,这使得人们更难意识到它与同样简单的 Python 有多么不同。

\n\n
\n\n

* Python社区中有些人喜欢称之为“pass-by-sharing”。一些研究人员称之为“通过物体”。其他人选择在描述调用样式之前首先区分值语义和引用语义,因此您可以将其称为“引用语义传递副本”。但是,虽然至少这些名字并不含糊,但它们也不是很出名,因此它们不太可能对任何人有帮助。我认为描述它比试图找出它的最佳名称更好\xe2\x80\xa6

\n\n

** 当然,因为 Python 是完全反射的,所以您始终可以x直接或间接地传递字符串及其找到的上下文\xe2\x80\xa6 如果您byRefExample这样做了globals()[\'x\'] = x + 2,那影响全局x. 但\xe2\x80\xa6\xc2\xa0不这样做。

\n