MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python函数的参数传递方式详解

2022-02-057.7k 阅读

Python函数参数传递基础概念

在Python中,函数是组织代码的重要方式,而参数传递则是函数与外界交互的关键环节。理解Python函数的参数传递方式,对于编写高效、健壮的代码至关重要。

形式参数与实际参数

首先要明确形式参数(形参)和实际参数(实参)的概念。形参是在函数定义时列出的参数,它们定义了函数接受数据的形式。例如:

def add_numbers(a, b):
    return a + b

这里的ab就是形参。

而实参是在函数调用时传递给函数的值。比如:

result = add_numbers(3, 5)

这里的35就是实参,它们被传递给了add_numbers函数的形参ab

参数传递的本质

Python中的参数传递本质上是“赋值传递”。当函数被调用时,实参的值被赋值给对应的形参。这意味着在函数内部,形参是实参值的一个副本。但由于Python中一切皆对象,所以这里的“值”实际上是对象的引用。

不可变对象的参数传递

整数类型参数传递示例

以整数类型为例,整数在Python中是不可变对象。看下面的代码:

def modify_number(num):
    num = num + 1
    return num

original_num = 5
new_num = modify_number(original_num)
print(f"Original number: {original_num}, New number: {new_num}")

在上述代码中,original_num是实参,传递给modify_number函数的形参num。函数内部对num进行num = num + 1操作,这实际上是创建了一个新的整数对象,并将num指向这个新对象。而original_num所指向的对象并没有改变。所以输出结果为:

Original number: 5, New number: 6

字符串类型参数传递示例

字符串同样是不可变对象。例如:

def modify_string(s):
    s = s + " world"
    return s

original_string = "Hello"
new_string = modify_string(original_string)
print(f"Original string: {original_string}, New string: {new_string}")

在函数modify_string中,对string进行拼接操作string = string + " world",这会创建一个新的字符串对象,并让string指向它。而original_string所指向的原始字符串对象并没有改变。输出为:

Original string: Hello, New string: Hello world

元组类型参数传递示例

元组也是不可变对象。如下代码:

def modify_tuple(tup):
    new_tup = tup + (3,)
    return new_tup

original_tuple = (1, 2)
new_tuple = modify_tuple(original_tuple)
print(f"Original tuple: {original_tuple}, New tuple: {new_tuple}")

这里函数modify_tuple对传入的元组进行拼接,创建了一个新的元组对象并返回。original_tuple所指向的元组对象本身并没有改变。输出是:

Original tuple: (1, 2), New tuple: (1, 2, 3)

可变对象的参数传递

列表类型参数传递示例

列表是可变对象。来看下面这个例子:

def modify_list(lst):
    lst.append(4)
    return lst

original_list = [1, 2, 3]
new_list = modify_list(original_list)
print(f"Original list: {original_list}, New list: {new_list}")

在函数modify_list中,通过lst.append(4)对列表进行修改。由于列表是可变对象,这里的修改直接作用于original_list所指向的列表对象。所以输出为:

Original list: [1, 2, 3, 4], New list: [1, 2, 3, 4]

可以看到,original_listnew_list都指向了同一个被修改后的列表对象。

字典类型参数传递示例

字典同样是可变对象。例如:

def modify_dict(dict_obj):
    dict_obj['new_key'] = 'new_value'
    return dict_obj

original_dict = {'key1': 'value1'}
new_dict = modify_dict(original_dict)
print(f"Original dict: {original_dict}, New dict: {new_dict}")

在函数modify_dict中,通过dict_obj['new_key'] = 'new_value'向字典中添加新的键值对。这一操作直接修改了original_dict所指向的字典对象。输出结果是:

Original dict: {'key1': 'value1', 'new_key': 'new_value'}, New dict: {'key1': 'value1', 'new_key': 'new_value'}

original_dictnew_dict指向的是同一个被修改后的字典对象。

集合类型参数传递示例

集合也是可变对象。代码如下:

def modify_set(set_obj):
    set_obj.add(4)
    return set_obj

original_set = {1, 2, 3}
new_set = modify_set(original_set)
print(f"Original set: {original_set}, New set: {new_set}")

在函数modify_set中,使用set_obj.add(4)向集合中添加元素。这一操作直接修改了original_set所指向的集合对象。输出为:

Original set: {1, 2, 3, 4}, New set: {1, 2, 3, 4}

original_setnew_set指向的是同一个被修改后的集合对象。

传递可变对象时的复制策略

浅拷贝

有时候,我们希望在函数内部对可变对象进行操作,但又不想影响原始对象。这时可以使用浅拷贝。以列表为例,使用list.copy()方法可以进行浅拷贝。

def modify_list_safely(lst):
    copied_lst = lst.copy()
    copied_lst.append(4)
    return copied_lst

original_list = [1, 2, 3]
new_list = modify_list_safely(original_list)
print(f"Original list: {original_list}, New list: {new_list}")

这里通过lst.copy()创建了lst的一个浅拷贝copied_lst。对copied_lst的修改不会影响original_list。输出为:

Original list: [1, 2, 3], New list: [1, 2, 3, 4]

深拷贝

对于嵌套的可变对象,浅拷贝可能无法满足需求,因为浅拷贝只复制一层对象,内部的嵌套对象仍然是引用。这时需要使用深拷贝。可以通过copy模块的deepcopy函数实现。

import copy

def modify_nested_list_safely(nested_lst):
    copied_nested_lst = copy.deepcopy(nested_lst)
    copied_nested_lst[0].append(4)
    return copied_nested_lst

original_nested_list = [[1, 2, 3], [4, 5]]
new_nested_list = modify_nested_list_safely(original_nested_list)
print(f"Original nested list: {original_nested_list}, New nested list: {new_nested_list}")

在这个例子中,original_nested_list是一个嵌套列表。使用copy.deepcopy进行深拷贝,对copied_nested_lst内部列表的修改不会影响original_nested_list。输出为:

Original nested list: [[1, 2, 3], [4, 5]], New nested list: [[1, 2, 3, 4], [4, 5]]

位置参数与关键字参数

位置参数

位置参数是最常见的参数传递方式,实参按照形参定义的顺序依次传递。例如:

def print_info(name, age):
    print(f"Name: {name}, Age: {age}")

print_info("Alice", 25)

这里"Alice"传递给name25传递给age,是按照位置进行匹配的。

关键字参数

关键字参数允许我们在调用函数时通过参数名指定实参的值,而不必按照顺序。例如:

def print_info(name, age):
    print(f"Name: {name}, Age: {age}")

print_info(age = 25, name = "Alice")

这样即使实参的顺序与形参定义的顺序不同,也能正确传递参数。关键字参数可以提高代码的可读性,特别是当函数有多个参数时。

默认参数

默认参数的定义与使用

默认参数是在函数定义时为形参指定一个默认值。当函数调用时如果没有传递对应的实参,则使用默认值。例如:

def greet(name, message = "Hello"):
    print(f"{message}, {name}!")

greet("Bob")
greet("Charlie", "Hi")

在第一个调用greet("Bob")中,没有传递message的实参,所以使用默认值"Hello"。在第二个调用greet("Charlie", "Hi")中,传递了"Hi"作为message的实参,所以使用传递的值。输出为:

Hello, Bob!
Hi, Charlie!

默认参数的注意事项

默认参数的值在函数定义时就确定了,而不是在函数调用时。这在默认参数是可变对象时需要特别注意。例如:

def append_to_list(lst = []):
    lst.append(1)
    return lst

result1 = append_to_list()
result2 = append_to_list()
print(result1)
print(result2)

这里lst的默认值是一个空列表。由于默认值在函数定义时就确定了,所以result1result2操作的是同一个列表对象。输出为:

[1]
[1, 1]

如果想要每次调用都使用新的列表对象,可以这样修改代码:

def append_to_list(lst = None):
    if lst is None:
        lst = []
    lst.append(1)
    return lst

result1 = append_to_list()
result2 = append_to_list()
print(result1)
print(result2)

这样每次调用时,如果lstNone,就创建一个新的空列表。输出为:

[1]
[1]

可变参数

不定长位置参数(*args)

在Python中,可以使用*args来接受不定数量的位置参数。args是一个元组,包含了所有传递进来的位置参数。例如:

def sum_numbers(*args):
    total = 0
    for num in args:
        total += num
    return total

result = sum_numbers(1, 2, 3, 4)
print(result)

这里*args接受了1234这些位置参数,并将它们组成一个元组。函数内部通过遍历这个元组来计算总和。输出为:

10

不定长关键字参数(**kwargs)

**kwargs用于接受不定数量的关键字参数。kwargs是一个字典,键是参数名,值是对应的参数值。例如:

def print_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name = "Alice", age = 25, city = "New York")

这里**kwargs接受了name = "Alice"age = 25city = "New York"这些关键字参数,并将它们组成一个字典。函数内部通过遍历字典来打印信息。输出为:

name: Alice
age: 25
city: New York

结合使用*args和**kwargs

在函数定义中,可以同时使用*args**kwargs,但*args必须在**kwargs之前。例如:

def process_data(*args, **kwargs):
    print("Positional arguments:", args)
    print("Keyword arguments:", kwargs)

process_data(1, 2, name = "Bob", age = 30)

输出为:

Positional arguments: (1, 2)
Keyword arguments: {'name': 'Bob', 'age': 30}

参数传递中的作用域问题

局部作用域与全局作用域

在Python中,函数内部定义的变量具有局部作用域,函数外部定义的变量具有全局作用域。例如:

global_variable = 10

def test_scope():
    local_variable = 5
    print(f"Local variable: {local_variable}")
    print(f"Global variable: {global_variable}")

test_scope()
print(f"Global variable outside: {global_variable}")
# 下面这行代码会报错,因为local_variable只在函数内部有效
# print(f"Local variable outside: {local_variable}")

在函数test_scope中,可以访问全局变量global_variable,同时定义了局部变量local_variable。函数外部可以访问全局变量,但不能访问局部变量。输出为:

Local variable: 5
Global variable: 10
Global variable outside: 10

修改全局变量

如果要在函数内部修改全局变量,需要使用global关键字。例如:

global_variable = 10

def modify_global():
    global global_variable
    global_variable = global_variable + 5
    return global_variable

new_value = modify_global()
print(f"New value of global variable: {new_value}")

这里通过global global_variable声明要修改全局变量global_variable。函数内部对其进行修改后,全局变量的值也相应改变。输出为:

New value of global variable: 15

闭包与非局部变量

闭包是一种特殊的函数,它可以访问其定义时所在的外部作用域的变量,即使外部函数已经返回。在闭包中,如果要修改外部作用域(但不是全局作用域)的变量,需要使用nonlocal关键字。例如:

def outer_function():
    outer_variable = 10
    def inner_function():
        nonlocal outer_variable
        outer_variable = outer_variable + 5
        return outer_variable
    return inner_function()

result = outer_function()
print(f"Result: {result}")

在这个例子中,inner_function是一个闭包。通过nonlocal outer_variable声明要修改外部作用域的outer_variable。输出为:

Result: 15

总结参数传递方式对代码设计的影响

代码的可维护性

理解参数传递方式有助于编写可维护的代码。例如,在使用可变对象作为参数时,如果不注意可能会导致意外的副作用,影响代码的可维护性。通过合理使用浅拷贝或深拷贝,可以避免这种情况,使得代码逻辑更加清晰,易于维护。

代码的灵活性

位置参数、关键字参数、默认参数、可变参数等不同的参数传递方式,为代码提供了很大的灵活性。可以根据具体需求选择合适的方式,使函数能够适应不同的调用场景,提高代码的复用性。

性能方面的考虑

在传递大型可变对象时,浅拷贝和深拷贝会带来不同的性能开销。深拷贝由于要递归复制所有嵌套对象,性能开销较大。所以在实际应用中,需要根据对象的复杂程度和性能要求,选择合适的复制策略,以平衡代码的功能和性能。

通过深入理解Python函数的参数传递方式,开发者可以编写出更高效、更健壮、更灵活的代码,提升整个项目的质量和可维护性。无论是小型脚本还是大型应用程序,对参数传递的准确把握都是至关重要的。