3

我正在尝试编写一个程序来向 A 级学生说明引用调用和使用 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

问题

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

byRefExample(byref(test))
4

2 回答 2

4

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


我成功地将可变对象作为变量传递给函数,但发现我也可以使用 ctypes 库来做同样的事情。

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

更详细地,比较这些:

def by_ref_using_list(x):
    x[0] += 1
value = [10]
by_ref_using_list(value)
print(value[0])

def by_ref_using_dict(x):
    x['value'] += 1
value = {'value': 10}
by_ref_using_list(value)
print(value['value'])

class ValueHolder(object):
    def __init__(self, value):
        self.value = value
def by_ref_using_int_holder(x):
    x.value += 1
value = ValueHolder(10)
by_ref_using_list(value)
print(value.value)

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

这正是你正在做的c_int

您可能想阅读常见问题解答如何编写带有输出参数的函数(通过引用调用)?,尽管您似乎已经知道那里的答案,只是想知道如何ctypes适应……</p>


那么,byref甚至是为了什么呢?

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

void by_ref_in_c(int *x) {
    *x += 1;
}

您不能将此c_int对象传递给它,因为它需要一个指向c_int. 而且您不能将它传递给 uninitialized POINTER(c_int),因为那样它只会写入随机存储器。您需要获取指向实际c_int. 你可以这样做:

x = c_int(10)
xp = pointer(x)
by_ref_in_c(xp)
print(x)

这工作得很好。但这太过分了,因为您已经创建了一个额外的 Pythonctypes对象,xp您实际上并不需要它。这就是它的byref用途:它为您提供了一个指向对象的轻量级指针,该指针只能用于通过引用传递该对象:

x = c_int(10)
by_ref_in_c(byref(x))
print(x)

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

byRefExample(byref(test))

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

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


那么,你是怎么这种事情的呢?

好吧,使用单元素列表是一种众所周知的技巧,可以解决您需要共享可变内容但您只有不可变内容的事实。如果你使用它,有经验的 Python 程序员会知道你在做什么。

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

通常,当您确实需要可变值时,已经有一个明显的地方可以让它们存在,例如类的实例属性。

但有时你确实需要单元素列表技巧,所以值得在你的曲目中使用;只是在不需要时不要使用它。


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

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

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

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

如果您来自 C++,这实际上更容易理解,这确实迫使您理解右值和左值以及不同类型的引用以及复制构造与复制分配等等……在 C 中,这一切都看似简单,这很难意识到它与同样简单的 Python 有多么不同。


* Python 社区中有些人喜欢称其为“传递共享”。一些研究人员称其为“通过对象”。其他人选择首先区分值语义和引用语义,然后再描述调用样式,因此您可以将此称为“引用语义传递复制”。但是,虽然至少这些名字不是模棱两可的,但它们也不是很知名,所以它们不太可能帮助任何人。我认为描述它比试图为它找出最好的名字更好......</p>

** 当然,因为 Python 是完全反射的,所以你总是可以x直接或间接地传递字符串和找到它的上下文……如果你byRefExample这样做了globals()['x'] = x + 2,那影响全局x. 但是……不要那样做。

于 2013-08-09T17:40:08.227 回答
2

Python 既不使用“call-by-reference”或“call-by-value”,而是使用“call-by-object”。赋值给对象命名。

test = c_int(56)
t = 67

test是给ctypes.c_int对象的名称,该名称在内部具有value分配给int对象的名称。

t是给int对象的名称。

调用时byRefExample(test)x是给ctypes.c_int引用的对象的另一个名称test

x.value = x.value + 2

上面将存储在ctypes.c_int对象中的“值”名称重新分配给具有不同值的全新对象。 int因为value是由名称和引用的同一 对象的属性,并且引用相同的值。ctypes.c_inttestxx.valuetest.value

调用时byValueExample(t)x是给int引用的对象的另一个名称t

x = x + 2

以上将名称重新分配x给具有不同值的全新 int对象。 x并且t不再引用同一个对象,所以t不会观察到变化。它仍然指的是原始int对象。

id()您可以通过在不同时间点打印对象来观察这一点:

from  ctypes import *

test = c_int(56)
t = 67

print('test id =',id(test))
print('t    id =',id(t))

#expects a ctypes argument
def byRefExample(x):
    print('ByRef x',x,id(x))
    print('ByRef x.value',x.value,id(x.value))
    x.value = x.value + 2
    print('ByRef x.value',x.value,id(x.value))
    print('ByRef x',x,id(x))

#expects a normal Python variable
def byValueExample(x):
    print('ByVal x',x,id(x))
    x = x + 2
    print('ByVal x',x,id(x))

print("Before call test is",test,id(test))
print("Before call test is",test.value,id(test.value))
byRefExample(test)                
print("After call test is",test.value,id(test.value))
print("After call test is",test,id(test))

print("Before call t is",t,id(t))
byValueExample(t)
print("After call t is",t,id(t))

输出(带注释):

test id = 80548680
t    id = 507083328
Before call test is c_long(56) 80548680
Before call test.value is 56 507082976
ByRef x c_long(56) 80548680                 # same id as test
ByRef x.value 56 507082976
ByRef x.value 58 507083040                  # x.value is new object!
ByRef x c_long(58) 80548680                 # but x is still the same.
After call test.value is 58 507083040       # test.value sees new object because...
After call test is c_long(58) 80548680      # test is same object as x.
Before call t is 67 507083328
ByVal x 67 507083328                        # same id as t
ByVal x 69 507083392                        # x is new object!
After call t is 67 507083328                # t id same old object.
于 2013-08-09T20:30:15.817 回答