《Python编程的术与道:Python语言入门》视频课程
《Python编程的术与道:Python语言入门》视频课程链接:https://edu.csdn.net/course/detail/27845
如何将函数调用中的参数传递给函数的参数(即函数参数的自变量的评估策略),因编程语言而异。
最常见的评估策略是“按值调用”(“Call by Value” )和“按引用调用”(“Call by Reference”):
按值调用(Call by Value )
最常见的策略是按值调用评估,有时也称为按值传递。例如,用于C和C ++。按值传递中,对参数表达式进行求值,并将求值结果绑定到函数中的相应变量。因此,如果表达式是一个变量,则其值将赋值(复制)到相应的参数。这样可以确保在函数返回时,调用者作用域内的变量将保持不变。
按引用调用(Call by Reference)
在按引用调用评估(也称为按引用传递)中,函数获取对参数的隐式引用,而不是其值的副本(copy)。函数可以修改自变量,即可以更改调用者作用域内的变量值。
通过按引用传递,不需要复制参数,可以节省计算时间和内存空间。
另一方面,这具有以下缺点:在函数调用中会意外地更改变量。因此,为不更改这些值,必须特别注意保护这些值。
许多编程语言都支持按引用传递,例如C或C++,但是Perl将其用作默认调用。
Python使用一种机制,称为"Call-by-Object"(“按对象调用”),有时也称为"Call by Object Reference"(“按对象引用调用”)、或"Call by Sharing"(“按共享调用”)、或赋值传递 (pass by assignment)、或按对象传递。
如果将不可变(immutable)的参数(例如整数、字符串或元组)传递给函数,则传递的行为类似于按值调用。对象引用传递给函数参数,但它们无法在函数中更改,因为它们根本无法更改。
如果传递可变(mutable)参数,则有所不同。它们也通过对象引用传递,但是可以在函数中对其进行更改。如果我们将一个列表传递给函数,则必须考虑两种情况:
首先,让我们看一下整数变量。只要不更改参数,函数内部的参数仍是对arguments变量的引用。一旦为其赋值了新值,Python就会创建一个单独的局部变量。调用者的变量不会以这种方式被更改:
def ref_demo(x):
print("x=",x," id=",id(x))
x=35
print("x=",x," id=",id(x))
x = 9
id(x)
140730833478288
ref_demo(x)
x= 9 id= 140730833478288
x= 35 id= 140730833479120
id(x)
140730833478288
x
9
在上面的示例中,使用了id()
函数,该函数将对象作为参数。 id(obj)返回对象“ obj”的"identity"(“标识”)。此标识,即函数的返回值,是一个整数,对于该对象在其生存期内是唯一且恒定的。
如果调用函数ref_demo():可以看到在主作用域中,x具有一个标识。在ref_demo()函数的第一个print语句中,使用了主作用域中的x,因为可以看到有相同的标识。在将值35赋值给x之后,x获得一个新的标识,即与全局x分开的存储位置。因此,当回到主作用域时,x仍具有原始值9。
这意味着Python最初的行为类似于按引用调用,但是一旦更改了此类变量的值,即,一旦为其赋值了新对象,Python就会切换为按值调用。这意味着将创建局部变量x并将全局变量x的值复制到其中。
那么对于上面的例子,是不是就没有办法改变 x 的值了呢?答案当然是否定的,我们只需稍作改变,让函数返回一个新变量,赋值给 x。这样,x 就指向了一个新的值为 35 的对象,x 的值也因此变为 35。
def ref_demo(x):
print("x=",x," id=",id(x))
x=35
print("x=",x," id=",id(x))
return x
x = 9
id(x)
140730833478288
x=ref_demo(x)
x= 9 id= 140730833478288
x= 35 id= 140730833479120
print(x)
id(x)
35
140730833479120
如果该函数除了产生返回值之外,还以其他方式修改了调用者的环境,则该函数具有副作用。 例如,一个函数可能会修改全局或静态变量、修改其参数之一、抛出异常、将数据显示或写入文件等。
在某些情况下,这些副作用是预期的,即它们是所需功能的一部分。 但是在其他情况下,则不需要它们,它们是隐藏的副作用。
在本节中,我们关注的副作用是:对于作为参数传递给函数的变量,副作用会更改一个或多个全局变量。
假设,将一个列表传递给函数。 我们希望该函数不更改此列表。
首先,让我们看一个没有副作用的函数。 将一个新列表分配给func1()中的参数列表后,将为列表创建一个新的存储位置,并且列表成为一个局部变量。
def no_side_effects(cities):
print(cities)
cities = cities + ["Birmingham", "Bradford"]
print(cities)
locations = ["London", "Leeds", "Glasgow", "Sheffield"]
no_side_effects(locations)
print(locations)
['London', 'Leeds', 'Glasgow', 'Sheffield']
['London', 'Leeds', 'Glasgow', 'Sheffield', 'Birmingham', 'Bradford']
['London', 'Leeds', 'Glasgow', 'Sheffield']
如果使用增强的赋值运算符+=
来添加列表,则情况将发生大变化。
def side_effects(cities):
print(cities)
cities += ["Birmingham", "Bradford"]
print(cities)
locations = ["London", "Leeds", "Glasgow", "Sheffield"]
side_effects(locations)
print(locations)
['London', 'Leeds', 'Glasgow', 'Sheffield']
['London', 'Leeds', 'Glasgow', 'Sheffield', 'Birmingham', 'Bradford']
['London', 'Leeds', 'Glasgow', 'Sheffield', 'Birmingham', 'Bradford']
我们可以看到Birmingham
和Bradford
也包括在全局列表locations中,因为+=
用作一个就地操作(in-place operation)执行。
可以通过将列表的副本传递给该函数来防止这种副作用。 因为列表中没有嵌套结构,浅拷贝就足够了。
def side_effects(cities):
print(cities)
cities += ["Paris", "Marseille"]
print(cities)
locations = ["Lyon", "Toulouse", "Nice", "Nantes", "Strasbourg"]
side_effects(locations[:])
['Lyon', 'Toulouse', 'Nice', 'Nantes', 'Strasbourg']
['Lyon', 'Toulouse', 'Nice', 'Nantes', 'Strasbourg', 'Paris', 'Marseille']
print(locations)
['Lyon', 'Toulouse', 'Nice', 'Nantes', 'Strasbourg']
我们可以看到,全局列表locations没有受到函数执行的影响。
总结:
Python 中参数的传递既不是按值传递,也不是按引用传递,而是按对象传递(或称按对象引用传递)。
这里的按对象传递,不是指向一个具体的内存地址,而是指向一个具体的对象。如果对象不可变,简单的赋值只能改变其中一个变量的值,其余变量则不受影响。如果对象是可变的,当其改变时,指向这个对象的变量也会改变。
如果想通过函数来改变某个变量的值,通常有两种方法:
直接将可变数据类型(比如列表,字典,集合)当作参数传入,直接在其上修改
创建一个新变量,来保存修改后的值,然后将其返回给原变量。 这种方法对于不可变对象也是可以的。
如果想通过函数而不改变一个可变对象的变量的值,可将可变对象的拷贝传入函数。