理解Python中的按对象传递函数参数的方式

15

我不确定我是否理解Python的按对象传递函数参数的概念(在此处讲解:http://effbot.org/zone/call-by-object.htm)。似乎没有足够的示例来很好地澄清这个概念(或者我的搜索能力可能很弱!:D)

我编写了这个小的假设Python程序,以尝试理解这个概念。

def foo( itnumber, ittuple,  itlist, itdict   ):
    itnumber +=1 
    print id(itnumber) , itnumber 

    print id(ittuple)  , ittuple

    itlist.append(3.4)
    print id(itlist)   , itlist

    itdict['mary']  = 2.3
    print id(itdict),    itdict



# Initialize a number, a tuple, a list and a dictionary
tnumber = 1
print id( tnumber ), tnumber 

ttuple  = (1, 2, 3)
print id( ttuple ) , ttuple

tlist   = [1, 2, 3]
print id( tlist ) , tlist

tdict = tel = {'jack': 4098, 'sape': 4139}
print '-------'
# Invoke a function and test it
foo(tnumber, ttuple, tlist , tdict)

print '-------'
#Test behaviour after the function call is over
print id(tnumber) , tnumber 
print id(ttuple)  , ttuple
print id(tlist)   , tlist
print id(tdict),  tdict

程序的输出是

146739376 1
3075201660 (1, 2, 3)
3075103916 [1, 2, 3]
3075193004 {'sape': 4139, 'jack': 4098}

---------

146739364 2
3075201660 (1, 2, 3)
3075103916 [1, 2, 3, 3.4]
3075193004 {'sape': 4139, 'jack': 4098, 'mary': 2.3}

---------

146739376 1
3075201660 (1, 2, 3)
3075103916 [1, 2, 3, 3.4]
3075193004 {'sape': 4139, 'jack': 4098, 'mary': 2.3}

从代码中可以看出,除了传递的整数之外,对象的ID(我理解为内存位置)保持不变。

因此,在整数的情况下,它被(有效地)按值传递,而其他数据结构则被(有效地)按引用传递。我尝试更改列表、数字和字典以测试数据结构是否会就地更改。数字没有被更改,但列表和字典被更改了。

我在上面使用"effectively"一词,因为"按对象调用"的参数传递方式似乎根据传递的数据结构而有不同的行为。

对于更复杂的数据结构,例如numpy数组等,有没有什么快速的经验法则来识别哪些参数将按引用传递,哪些按值传递?


1
由于您似乎了解C语言,Python的“对象传递”类似于通过值传递指针,其中一些值被指向(例如元组和整数)是不可变的。 - icktoofay
5
@icktoofay说的没错。但最好避免按值传递/按引用传递的范式,只需考虑“事物”和“名称”。 - Katriel
3个回答

14
关键区别在于,在C风格的语言中,变量是一个内存盒子,你可以将东西放入其中。而在Python中,变量是一个名字。

Python既不是按引用传递也不是按值传递。它是更加合理的东西!(事实上,在学习更常见的语言之前,我就学习了Python,因此按值传递和按引用传递对我来说似乎很奇怪。)

在Python中,有“东西”和有“名字”。列表、整数、字符串和自定义对象都是“东西”。x、y和z是“名字”。写成:

x = []

意思是“构造一个新的东西[]并给它命名为x”。写作:

x = []
foo = lambda x: x.append(None)
foo(x)

意思是 "用名称为x构造一个新的事物[],用名称为foo构造一个新的函数(另一个事物),并在名称为x的事物上调用foo"。现在,foo只是将None附加到它接收到的内容中,因此这就简化为“将None附加到空列表中”。

x = 0
def foo(x):
    x += 1
foo(x)

意思是“用名称 x 构造新物品 0,构造新函数 foo ,并在 x 上调用 foo”。 在 foo 中,赋值语句只是将 x 重命名为它原来的值加1,但这并不会改变物品 0


1
谢谢你的好回答。为了澄清你关于“things”和“names”的观点,我在ipython上进行了以下测试: >>> id(3.14) 得到145793796 >>> x=3.14 >>> id(x) 得到145793796 >>> >>> >>> id(2) 得到145756324 >>> x=2 >>> id(x) 得到145756324 所以,正如你所说,“x”是一个名称,它被重新绑定到不同的对象上。谢谢。 - smilingbuddha
2
我喜欢这个回答,除了你举的最后一个例子。问题在于+=对可变和不可变对象应该有所不同。可变对象很可能会在foo之外产生可见变化。 - mgilson
2
关于 += 的重要一点:a += b 实际上可能会调用 a.__iadd__(b),这与调用 a 的任何其他方法相同,它不执行任何重新赋值。如果 a 没有一个 __iadd__ 函数,那么 a += b 会简化为 a = a + b,进而简化为 a = a.__add__(b) 或者 a = b.__radd__(a) - Claudiu

10

其他人已经发表了很好的答案。我认为还有一件事会有所帮助:

 x = expr

评估expr并将x绑定到结果。 另一方面:

 x.operate()

该操作会对x进行处理,因此可能会改变它的值(导致相同的基础对象具有不同的值)。

一些有趣的情况涉及到:

 x += expr

这意味着可以翻译为要么x = x + expr(重新绑定),要么x.__iadd__(expr)(修改),有时会以非常奇特的方式进行:

>>> x = 1
>>> x += 2
>>> x
3

(因为整数是不可变的,所以x被重新绑定了)

>>> x = ([1], 2)
>>> x
([1], 2)
>>> x[0] += [3]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> x
([1, 3], 2)

这里x[0]本身是可变的,它被原地改变了;但随后Python也试图更改x本身(类似于x.__iadd__),但因为元组是不可变的而出现错误。但此时x[0]已经被改变了!


我应该承认,我最初是在comp.lang.python上看到这个的,也不知道是谁展示的。虽然它确实很奇特! - torek
你能分享更多关于为什么Python会抛出错误但仍然允许这种赋值的信息吗?我可以想象一些与此行为相关的原因,但我想知道实现细节。 - Arn
2
@Arn:这只是一个操作序列的问题:+= 运算符首先调用 x[0].__iadd__。然后,+= 的语义规定 Python 应该重新分配左侧的对象(请参见 https://docs.python.org/3/reference/simple_stmts.html#augmented-assignment-statements)。如果执行“评估左侧”操作时带有一个额外的标志“意图分配”,则可以避免该问题:在调用 __iadd__ 之前,Python 可以捕获错误。但它没有这样做。 - torek

7

在Python中,数字、字符串和元组是不可变的;使用增强赋值会重新绑定名称。

其他类型只是被“改变”,但仍然是同一个对象。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接