Python 中的引用传递:背景和最佳实践
目录
定义按引用传递
对比引用传递和值传递
使用传递引用结构
避免重复对象
返回多个值
创建条件多返回函数
在 Python 中传递参数
理解 Python 中的赋值
探索函数参数
使用 Python 复制引用传递
最佳实践:返回和重新分配
最佳实践:使用对象属性
最佳实践:使用字典和列表
结论
在熟悉 Python 之后,您可能会注意到函数不会像预期的那样修改参数的情况,尤其是在您熟悉其他编程语言的情况下。某些语言将函数参数作为对现有变量的引用来处理,这称为通过引用传递。其他语言将它们作为独立值处理,这种方法称为按值传递。
如果您是一名中级 Python 程序员,希望了解 Python 处理函数参数的特殊方式,那么本教程适合您。您将在 Python 中实现按引用传递构造的实际用例,并学习几个最佳实践以避免函数参数中的陷阱。
在本教程中,您将学习:
通过引用传递意味着什么以及为什么要这样做
按引用传递与按值传递和Python 的独特方法有何不同
如何函数参数的行为在Python
如何使用某些可变类型在 Python 中按引用传递
在 Python 中复制引用传递的最佳实践是什么
定义按引用传递
在深入研究按引用传递的技术细节之前,通过将其分解为多个组件来仔细研究该术语本身会很有帮助:
Pass表示为函数提供参数。
引用意味着您传递给函数的参数是对内存中已存在的变量的引用,而不是该变量的独立副本。
由于您为函数提供了对现有变量的引用,因此对该引用执行的所有操作都将直接影响它所引用的变量。让我们看一些在实践中如何工作的例子。
下面,您将看到如何在 C# 中通过引用传递变量。请注意在突出显示的行中使用ref关键字:
using System; // Source: // https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/passing-parameters class Program { static void Main(string[] args) { int arg; // Passing by reference. // The value of arg in Main is changed. arg = 4; squareRef(ref arg); Console.WriteLine(arg); // Output: 16 } static void squareRef(ref int refParameter) { refParameter *= refParameter; } }
可以看到,refParameterofsquareRef()必须用ref关键字声明,调用函数时也必须使用关键字。然后参数将通过引用传入并且可以就地修改。
Python 没有ref关键字或与之等效的任何东西。如果您尝试在 Python 中尽可能接近地复制上述示例,那么您将看到不同的结果:
>>>
>>> def main(): ... arg = 4 ... square(arg) ... print(arg) ... >>> def square(n): ... n *= n ... >>> main() 4
在这种情况下,arg变量不会就地更改。Python 似乎将您提供的参数视为独立值,而不是对现有变量的引用。这是否意味着 Python 按值而不是按引用传递参数?
不完全的。Python 既不是通过引用也不是通过值传递参数,而是通过赋值。下面,在更仔细地研究 Python 的方法之前,您将快速探索按值传递和按引用传递的细节。之后,您将了解一些最佳实践,以在 Python 中实现等效于按引用传递的效果。
对比引用传递和值传递
当您通过引用传递函数参数时,这些参数只是对现有值的引用。相反,当您按值传递参数时,这些参数将成为原始值的独立副本。
让我们重温一下 C# 示例,这次不使用ref关键字。这将导致程序使用按值传递的默认行为:
using System; // Source: // https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/passing-parameters class Program { static void Main(string[] args) { int arg; // Passing by value. // The value of arg in Main is not changed. arg = 4; squareVal(arg); Console.WriteLine(arg); // Output: 4 } static void squareVal(int valParameter) { valParameter *= valParameter; } }
在这里,您可以看到squareVal()没有修改原始变量。相反,valParameter是原始变量的独立副本arg。虽然这与您在 Python 中看到的行为相匹配,但请记住,Python 并不完全按值传递。让我们证明一下。
Python 的内置id()返回一个整数,表示所需对象的内存地址。使用id(),您可以验证以下断言:
函数参数最初引用与其原始变量相同的地址。
在函数内重新分配参数会为其提供一个新地址,而原始变量保持不变。
在下面的例子中,注意 的地址x最初与的地址匹配,n但在重新分配后发生变化,而 的地址n从不改变:
>>>
>>> def main(): ... n = 9001 ... print(f"Initial address of n: {id(n)}") ... increment(n) ... print(f" Final address of n: {id(n)}") ... >>> def increment(x): ... print(f"Initial address of x: {id(x)}") ... x += 1 ... print(f" Final address of x: {id(x)}") ... >>> main() Initial address of n: 140562586057840 Initial address of x: 140562586057840 Final address of x: 140562586057968 Final address of n: 140562586057840
调用时n和的初始地址x相同的increment()事实证明该x参数不是按值传递的。否则,n并且x会有不同的内存地址。
在了解 Python 如何处理参数的细节之前,让我们先看一下按引用传递的一些实际用例。
使用传递引用结构
通过引用传递变量是您可以用来实现某些编程模式的几种策略之一。虽然很少需要,但通过引用传递可能是一个有用的工具。
在本节中,您将了解按引用传递是一种实用方法的三种最常见模式。然后,您将看到如何使用 Python 实现这些模式中的每一个。
避免重复对象
如您所见,按值传递变量将导致创建该值的副本并将其存储在内存中。在默认为按值传递的语言中,您可能会发现按引用传递变量会带来性能优势,尤其是当变量包含大量数据时。当您的代码在资源受限的机器上运行时,这一点会更加明显。
然而,在 Python 中,这从来都不是问题。您将在下一节中了解原因。
返回多个值
按引用传递的最常见应用之一是创建一个函数,该函数在返回不同值的同时更改引用参数的值。您可以修改通过引用传递的 C# 示例来说明此技术:
using System; class Program { static void Main(string[] args) { int counter = 0; // Passing by reference. // The value of counter in Main is changed. Console.WriteLine(greet("Alice", ref counter)); Console.WriteLine("Counter is {0}", counter); Console.WriteLine(greet("Bob", ref counter)); Console.WriteLine("Counter is {0}", counter); // Output: // Hi, Alice! // Counter is 1 // Hi, Bob! // Counter is 2 } static string greet(string name, ref int counter) { string greeting = "Hi, " + name + "!"; counter++; return greeting; } }
在上面的示例中,greet()返回一个问候字符串并修改 的值counter。现在尝试在 Python 中尽可能地重现这一点:
>>>
>>> def main(): ... counter = 0 ... print(greet("Alice", counter)) ... print(f"Counter is {counter}") ... print(greet("Bob", counter)) ... print(f"Counter is {counter}") ... >>> def greet(name, counter): ... counter += 1 ... return f"Hi, {name}!" ... >>> main() Hi, Alice! Counter is 0 Hi, Bob! Counter is 0
counter在上面的示例中没有递增,因为正如您之前所了解的,Python 无法通过引用传递值。那么如何才能获得与使用 C# 相同的结果呢?
本质上,C# 中的引用参数不仅允许函数返回值,还允许对附加参数进行操作。这相当于返回多个值!
幸运的是,Python 已经支持返回多个值。严格来说,返回多个值的 Python 函数实际上返回一个包含每个值的元组:
>>>
>>> def multiple_return(): ... return 1, 2 ... >>> t = multiple_return() >>> t # A tuple (1, 2) >>> # You can unpack the tuple into two variables: >>> x, y = multiple_return() >>> x 1 >>> y 2
如您所见,要返回多个值,您可以简单地使用return关键字后跟逗号分隔的值或变量。
这种技术的武装,你可以改变return声明中的greet()从以前的Python代码同时返回问候和计数器:
>>>
>>> def main(): ... counter = 0 ... print(greet("Alice", counter)) ... print(f"Counter is {counter}") ... print(greet("Bob", counter)) ... print(f"Counter is {counter}") ... >>> def greet(name, counter): ... return f"Hi, {name}!", counter + 1 ... >>> main() ('Hi, Alice!', 1) Counter is 0 ('Hi, Bob!', 1) Counter is 0
那看起来还是不对。尽管greet()现在返回多个值,但它们被打印为tuple,这不是您的意图。此外,原始counter变量保持在0。
清理你的输出,并得到想要的结果,你必须重新分配你的counter每次调用变量greet():
>>>
>>> def main(): ... counter = 0 ... greeting, counter = greet("Alice", counter) ... print(f"{greeting}\nCounter is {counter}") ... greeting, counter = greet("Bob", counter) ... print(f"{greeting}\nCounter is {counter}") ... >>> def greet(name, counter): ... return f"Hi, {name}!", counter + 1 ... >>> main() Hi, Alice! Counter is 1 Hi, Bob! Counter is 2
现在,通过调用 重新分配每个变量后greet(),您可以看到所需的结果!
将返回值分配给变量是实现与 Python 中按引用传递相同结果的最佳方式。您将在有关最佳实践的部分中了解原因以及一些其他方法。
创建条件多返回函数
这是返回多个值的特定用例,其中该函数可以在条件语句中使用,并且具有额外的副作用,例如修改作为参数传入的外部变量。
考虑C# 中的标准Int32.TryParse函数,它返回一个布尔值并同时操作对整数参数的引用:
public static bool TryParse (string s, out int result);
此函数尝试string使用out关键字将 a转换为 32 位有符号整数。有两种可能的结果:
如果解析成功,则输出参数将设置为结果整数,函数将返回true。
如果解析失败,则输出参数将设置为0,函数将返回false。
您可以在以下示例中在实践中看到这一点,该示例尝试转换许多不同的字符串:
using System; // Source: // https://docs.microsoft.com/en-us/dotnet/api/system.int32.tryparse?view=netcore-3.1#System_Int32_TryParse_System_String_System_Int32__ public class Example { public static void Main() { String[] values = { null, "160519", "9432.0", "16,667", " -322 ", "+4302", "(100);", "01FA" }; foreach (var value in values) { int number; if (Int32.TryParse(value, out number)) { Console.WriteLine("Converted '{0}' to {1}.", value, number); } else { Console.WriteLine("Attempted conversion of '{0}' failed.", value ?? "
上面的代码试图通过 将不同格式的字符串转换为整数TryParse(),输出如下:
Attempted conversion of '
要在 Python 中实现类似的功能,您可以使用之前看到的多个返回值:
def tryparse(string, base=10): try: return True, int(string, base=base) except ValueError: return False, None
这tryparse()将返回两个值。第一个值指示转换是否成功,第二个值保存结果(None如果失败,则为 )。
但是,使用此函数有点笨拙,因为您需要在每次调用时解压缩返回值。这意味着您不能在if语句中使用该函数:
>>>
>>> success, result = tryparse("123") >>> success True >>> result 123 >>> # We can make the check work >>> # by accessing the first element of the returned tuple, >>> # but there's no way to reassign the second element to `result`: >>> if tryparse("456")[0]: ... print(result) ... 123
尽管它通常通过返回多个值来工作,tryparse()但不能用于条件检查。这意味着您还有一些工作要做。
您可以利用 Python 的灵活性并简化函数以根据转换是否成功返回不同类型的单个值:
def tryparse(string, base=10): try: return int(string, base=base) except ValueError: return None
由于 Python 函数能够返回不同的数据类型,您现在可以在条件语句中使用此函数。但是如何?难道您不必先调用函数,分配其返回值,然后检查值本身吗?
通过利用 Python 在对象类型方面的灵活性,以及Python 3.8 中的新赋值表达式,您可以在条件if语句中调用这个简化的函数,并在检查通过时获取返回值:
>>>
>>> if (n := tryparse("123")) is not None: ... print(n) ... 123 >>> if (n := tryparse("abc")) is None: ... print(n) ... None >>> # You can even do arithmetic! >>> 10 * tryparse("10") 100 >>> # All the functionality of int() is available: >>> 10 * tryparse("0a", base=16) 100 >>> # You can also embed the check within the arithmetic expression! >>> 10 * (n if (n := tryparse("123")) is not None else 1) 1230 >>> 10 * (n if (n := tryparse("abc")) is not None else 1) 10
哇!这个 Python 版本tryparse()比 C# 版本更强大,允许您在条件语句和算术表达式中使用它。
通过一点点独创性,您已经复制了一个特定且有用的传递引用模式,而实际上并没有通过引用传递参数。事实上,在使用赋值表达式 operator( ) 并直接在 Python 表达式中使用返回值时,您又一次赋值了返回值:=。
到目前为止,您已经了解了按引用传递的含义,它与按值传递有何不同,以及 Python 的方法与两者有何不同。现在您已准备好仔细研究 Python 如何处理函数参数!
在 Python 中传递参数
Python 通过赋值传递参数。也就是说,当你调用一个 Python 函数时,每个函数参数都变成了一个变量,传递的值会被赋值给这个变量。
因此,您可以通过了解赋值机制本身的工作原理,甚至在函数外部,了解有关 Python 如何处理函数参数的重要细节。
理解 Python 中的赋值
Python 的赋值语句语言参考提供了以下详细信息:
如果赋值目标是标识符或变量名,则此名称绑定到对象。例如,在x = 2,x是名称,2是对象。
如果名称已经绑定到一个单独的对象,那么它会重新绑定到新对象。例如,如果x是2并且您发出x = 3,则变量名称x将重新绑定到3。
所有Python 对象都以特定结构实现。此结构的属性之一是一个计数器,用于跟踪已绑定到此对象的名称的数量。
注意:此计数器称为引用计数器,因为它会跟踪指向同一对象的引用或名称的数量。不要将引用计数器与通过引用传递的概念混淆,因为两者无关。
Python 文档提供了有关引用计数的更多详细信息。
让我们继续这个x = 2例子,看看当你给一个新变量赋值时会发生什么:
如果表示该值的对象2已经存在,则检索它。否则,它被创建。
此对象的引用计数器递增。
在当前命名空间中添加一个条目以将标识符绑定x到表示 的对象2。该条目实际上是存储在字典中的键值对!该字典的表示由locals()or返回globals()。
现在,如果您重新分配x给不同的值,会发生以下情况:
所代表对象的引用计数器2递减。
表示新值的对象的引用计数器递增。
当前命名空间的字典被更新以与x表示新值的对象相关。
Python 允许您使用函数获取任意值的引用计数sys.getrefcount()。您可以用它来说明分配如何增加和减少这些引用计数器。请注意,交互式解释器采用的行为会产生不同的结果,因此您应该从文件中运行以下代码:
from sys import getrefcount print("--- Before assignment ---") print(f"References to value_1: {getrefcount('value_1')}") print(f"References to value_2: {getrefcount('value_2')}") x = "value_1" print("--- After assignment ---") print(f"References to value_1: {getrefcount('value_1')}") print(f"References to value_2: {getrefcount('value_2')}") x = "value_2" print("--- After reassignment ---") print(f"References to value_1: {getrefcount('value_1')}") print(f"References to value_2: {getrefcount('value_2')}")
此脚本将显示分配前、分配后和重新分配后每个值的引用计数:
--- Before assignment --- References to value_1: 3 References to value_2: 3 --- After assignment --- References to value_1: 4 References to value_2: 3 --- After reassignment --- References to value_1: 3 References to value_2: 4
这些结果说明了标识符(变量名)和代表不同值的 Python 对象之间的关系。当您将多个变量分配给相同的值时,Python 会增加现有对象的引用计数器并更新当前命名空间,而不是在内存中创建重复的对象。
在下一节中,您将通过探索 Python 如何处理函数参数,以您当前对赋值操作的理解为基础。
探索函数参数
Python 中的函数参数是局部变量。这意味着什么?Local是 Python 的作用域之一。这些范围由上一节中提到的命名空间字典表示。您可以分别使用locals()和globals()来检索本地和全局命名空间字典。
执行时,每个函数都有自己的本地命名空间:
>>>
>>> def show_locals(): ... my_local = True ... print(locals()) ... >>> show_locals() {'my_local': True}
使用locals(),您可以证明函数参数成为函数本地命名空间中的常规变量。让我们my_arg向函数添加一个参数 , :
>>>
>>> def show_locals(my_arg): ... my_local = True ... print(locals()) ... >>> show_locals("arg_value") {'my_arg': 'arg_value', 'my_local': True}
您还可以使用sys.getrefcount()来显示函数参数如何增加对象的引用计数器:
>>>
>>> from sys import getrefcount >>> def show_refcount(my_arg): ... return getrefcount(my_arg) ... >>> getrefcount("my_value") 3 >>> show_refcount("my_value") 5
上面的脚本首先输出了"my_value"外部引用计数,然后是内部show_refcount()引用计数,显示引用计数增加的不是 1,而是 2!
那是因为,除了show_refcount()它本身之外,对sys.getrefcount()inside的调用show_refcount()也my_arg作为参数接收。这my_arg位于 for 的本地命名空间中sys.getrefcount(),添加了对 的额外引用"my_value"。
通过检查函数内部的命名空间和引用计数,您可以看到函数参数的工作方式与赋值完全一样:Python 在函数的本地命名空间中在标识符和表示参数值的 Python 对象之间创建绑定。这些绑定中的每一个都会增加对象的引用计数器。
现在你可以看到 Python 如何通过赋值来传递参数了!
使用 Python 复制引用传递
在上一节中检查了命名空间后,您可能会问为什么global没有提到作为一种修改变量的方法,就像它们通过引用传递一样:
>>>
>>> def square(): ... # Not recommended! ... global n ... n *= n ... >>> n = 4 >>> square() >>> n 16
使用该global语句通常会影响代码的清晰度。它可能会产生许多问题,包括:
自由变量,看似无关
所述变量没有显式参数的函数
不能与其他变量或参数一起使用的函数,因为它们依赖于单个全局变量
使用全局变量时缺乏线程安全性
将前面的示例与以下示例进行对比,该示例显式返回一个值:
>>>
>>> def square(n): ... return n * n ... >>> square(4) 16
好多了!您避免了全局变量的所有潜在问题,并且通过要求参数,您可以使您的函数更清晰。
尽管既不是按引用传递的语言也不是按值传递的语言,Python 在这方面没有任何缺点。它的灵活性足以应对挑战。
最佳实践:返回和重新分配
您已经接触到从函数返回值并将它们重新分配给变量。对于对单个值进行操作的函数,返回值比使用引用要清晰得多。此外,由于 Python 已经在幕后使用了指针,因此即使它能够通过引用传递参数,也不会有额外的性能优势。
旨在编写返回一个值的单一用途函数,然后(重新)将该值分配给变量,如下例所示:
def square(n): # Accept an argument, return a value. return n * n x = 4 ... # Later, reassign the return value: x = square(x)
返回和分配值还可以使您的意图明确并且您的代码更易于理解和测试。
对于对多个值进行操作的函数,您已经看到 Python 能够返回值元组。由于 Python 的灵活性,您甚至超越了C#中Int32.TryParse()的优雅!
如果您需要对多个值进行操作,那么您可以编写返回多个值的单一用途函数,然后(重新)将这些值分配给变量。下面是一个例子:
def greet(name, counter): # Return multiple values return f"Hi, {name}!", counter + 1 counter = 0 ... # Later, reassign each return value by unpacking. greeting, counter = greet("Alice", counter)
调用返回多个值的函数时,可以同时分配多个变量。
最佳实践:使用对象属性
对象属性在 Python 的赋值策略中占有一席之地。Python 的赋值语句语言参考指出,如果目标是支持赋值的对象属性,则将要求该对象对该属性执行赋值。如果将对象作为参数传递给函数,则可以就地修改其属性。
编写接受具有属性的对象的函数,然后直接对这些属性进行操作,如下例所示:
>>>
>>> # For the purpose of this example, let's use SimpleNamespace. >>> from types import SimpleNamespace >>> # SimpleNamespace allows us to set arbitrary attributes. >>> # It is an explicit, handy replacement for "class X: pass". >>> ns = SimpleNamespace() >>> # Define a function to operate on an object's attribute. >>> def square(instance): ... instance.n *= instance.n ... >>> ns.n = 4 >>> square(ns) >>> ns.n 16
注意square()需要写成直接对一个属性进行操作,修改后不需要重新赋值返回值。
值得重申的是,您应该确保该属性支持赋值!这是与 相同的示例namedtuple,其属性是只读的:
>>>
>>> from collections import namedtuple >>> NS = namedtuple("NS", "n") >>> def square(instance): ... instance.n *= instance.n ... >>> ns = NS(4) >>> ns.n 4 >>> square(ns) Traceback (most recent call last): File "
尝试修改不允许修改的属性会导致AttributeError.
此外,您应该注意类属性。它们将保持不变,并且将创建和修改实例属性:
>>>
>>> class NS: ... n = 4 ... >>> ns = NS() >>> def square(instance): ... instance.n *= instance.n ... >>> ns.n 4 >>> square(ns) >>> # Instance attribute is modified. >>> ns.n 16 >>> # Class attribute remains unchanged. >>> NS.n 4
由于类属性在通过类实例修改时保持不变,因此您需要记住引用实例属性。
最佳实践:使用字典和列表
Python中的字典是与所有其他内置类型不同的对象类型。它们被称为映射类型。Python 的映射类型文档提供了对该术语的一些了解:
甲映射对象映射可哈希值到任意对象。映射是可变对象。目前只有一种标准映射类型,即字典。(来源)
本教程不涉及如何实现自定义映射类型,但您可以使用简陋的字典复制通过引用传递。这是一个使用直接对字典元素进行操作的函数的示例:
>>>
>>> # Dictionaries are mapping types. >>> mt = {"n": 4} >>> # Define a function to operate on a key: >>> def square(num_dict): ... num_dict["n"] *= num_dict["n"] ... >>> square(mt) >>> mt {'n': 16}
由于您将值重新分配给字典键,因此对字典元素进行操作仍然是一种赋值形式。使用字典,您可以获得通过相同字典对象访问修改后的值的额外实用性。
虽然列表不是映射类型,但由于两个重要特征:下标性和可变性,您可以以类似于字典的方式使用它们。这些特性值得多解释一下,但让我们先来看看使用 Python 列表模拟按引用传递的最佳实践。
要使用列表复制引用传递,请编写一个直接对列表元素进行操作的函数:
>>>
>>> # Lists are both subscriptable and mutable. >>> sm = [4] >>> # Define a function to operate on an index: >>> def square(num_list): ... num_list[0] *= num_list[0] ... >>> square(sm) >>> sm [16]
由于您将值重新分配给列表中的元素,因此对列表元素进行操作仍然是一种赋值形式。与字典类似,列表允许您通过相同的列表对象访问修改后的值。
现在让我们探索可订阅性。当可以通过索引位置访问其结构的子集时,对象是可下标的:
>>>
>>> subscriptable = [0, 1, 2] # A list >>> subscriptable[0] 0 >>> subscriptable = (0, 1, 2) # A tuple >>> subscriptable[0] 0 >>> subscriptable = "012" # A string >>> subscriptable[0] '0' >>> not_subscriptable = {0, 1, 2} # A set >>> not_subscriptable[0] Traceback (most recent call last): File "
列表、元组和字符串是可下标的,但集合不是。尝试访问不可下标的对象元素将引发TypeError.
可变性是一个更广泛的主题,需要额外的探索和文档参考。简而言之,如果一个对象的结构可以就地更改而不是需要重新分配,则该对象是可变的:
>>>
>>> mutable = [0, 1, 2] # A list >>> mutable[0] = "x" >>> mutable ['x', 1, 2] >>> not_mutable = (0, 1, 2) # A tuple >>> not_mutable[0] = "x" Traceback (most recent call last): File "
列表和集合是可变的,字典和其他映射类型也是如此。字符串和元组是不可变的。尝试修改不可变对象的元素将引发TypeError.
结论
Python 与支持按引用或按值传递参数的语言不同。函数参数成为分配给传递给函数的每个值的局部变量。但这并不妨碍您获得在其他语言中通过引用传递参数时所期望的相同结果。
在本教程中,您学习了:
Python 如何处理给变量赋值
Python中如何通过赋值传递函数参数
为什么返回值是复制通过引用传递的最佳实践
如何使用属性、字典和列表作为替代最佳实践
您还学习了一些在 Python 中复制传递引用结构的其他最佳实践。您可以使用这些知识来实现 传统上需要支持按引用传递的模式。
为了继续您的 Python 之旅,我鼓励您深入研究您在这里遇到的一些相关主题,例如可变性、赋值表达式以及Python 命名空间和作用域。
保持好奇,我们下期再见!
Python
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。