Python函数全攻略:参数传递、作用域LEGB及高阶特性详解

kayokoi 发布于 2025-05-28 50 次阅读


在Python中,函数不仅仅是代码的组织单元,它们被视为“一等公民”,这意味着函数可以像其他任何对象(如数字、字符串、列表)一样被赋值给变量、作为参数传递给其他函数,以及作为其他函数的返回值。这种特性为Python带来了强大的灵活性和表达力,是理解高阶函数、闭包和装饰器等高级概念的基础。

一、函数定义与调用基础

<a name="1-定义与调用"></a>

1.1 基本语法与 self

使用 def 关键字定义函数,后跟函数名、圆括号内的参数列表,以及一个冒号。函数体必须缩进。

def greet(name):
    # """可选的文档字符串 (Docstring)"""
    message = f"Hello, {name}!"
    return message

# 调用函数
print(greet("Python User"))

在类中定义的方法,其第一个参数按约定通常命名为 self,它显式地代表实例本身,类似于Java中隐式的 this 关键字。类的初始化方法(构造器)名为 __init__(self, ...)

1.2 返回值与文档字符串 (Docstrings)
  • 返回值: 函数可以使用 return 语句返回一个值。如果函数没有 return 语句,或者 return 后没有指定值,则隐式返回 None。若需返回多个值,可以直接用逗号分隔,Python会自动将它们打包成一个元组返回,例如 return val1, val2 实际上返回的是 (val1, val2)
  • 文档字符串 (Docstrings): 如果函数体的第一个未赋值的语句是一个三引号字符串,那么这个字符串就成为该函数的文档字符串。可以通过 help(func_name)func_name.__doc__ 来查看。Docstrings是Python运行时对象的属性,不仅用于生成API文档,还可以在程序中被访问和利用。

二、Python函数参数的灵活多变

<a name="2-参数-灵活多变"></a>

Python函数参数的处理方式极其灵活,这是其强大功能的一个重要体现。

2.1 位置参数 (Positional Arguments)

调用函数时,实参(arguments)按照它们在调用中出现的顺序,逐个传递给函数定义中的形参(parameters)。数量和顺序通常需要匹配(除非有默认值或可变参数)。

2.2 关键字参数 (Keyword Arguments)

调用时可以通过 parameter_name=value 的形式传递实参。这使得参数的顺序不再重要(但关键字参数必须在所有位置参数之后),并且能提高代码的可读性,明确指出哪个值赋给了哪个参数。

def describe_pet(animal_type, pet_name):
    print(f"I have a {animal_type}.")
    print(f"My {animal_type}'s name is {pet_name.title()}.")

describe_pet("hamster", "harry") # 位置参数
describe_pet(pet_name="willie", animal_type="dog") # 关键字参数,顺序可变
  • vs. Java: Java没有直接的关键字参数语法。Java的Builder设计模式在创建复杂对象时,可以通过链式调用setter方法达到类似提高可读性和灵活指定参数的效果。
2.3 默认参数值 (Default Argument Values) 与注意事项

定义函数时,可以为形参指定默认值,例如 def func(param='default_value'): ...。调用时若未给该形参传值,则使用其默认值。带默认值的形参必须在所有无默认值的位置形参之后。

重要警示:默认参数的可变对象陷阱

默认参数值在函数定义时被计算一次。如果默认值是一个可变对象(如列表、字典),并且在函数内部修改了这个可变对象,那么后续不传递该参数的调用将会共享这个被修改过的默认对象,可能导致意外行为。

推荐做法:用 None 作为可变对象的默认值,并在函数内部检查并创建新对象。

# 不推荐的做法 (潜在问题)
# def add_item_bad(item, my_list=[]):
#     my_list.append(item)
#     return my_list

# list1 = add_item_bad(1) # [1]
# list2 = add_item_bad(2) # [1, 2] <--- list1 也变成了 [1, 2]!

# 推荐做法
def add_item_good(item, my_list=None):
    if my_list is None:
        my_list = []
    my_list.append(item)
    return my_list

list1 = add_item_good(1) # [1]
list2 = add_item_good(2) # [2] (正确)
print(f"List1: {list1}, List2: {list2}")
  • vs. Java: Java通过方法重载 (overloading) 实现类似“可选参数”的功能。
2.4 可变长度参数 (*args**kwargs)
  • *args (名称 args 是约定,星号是关键): 收集多余的未命名位置参数,打包成一个元组 (tuple)

    def print_all(first_arg, *remaining_args):
        print("First argument:", first_arg)
        print("Remaining arguments (tuple):", remaining_args)
    print_all(1, 2, 3, "hello") # First: 1, Remaining: (2, 3, 'hello')
    
    • vs. Java: 类似Java的可变参数 (varargs) Type... name,在Java方法内部 name 是一个数组。
  • **kwargs (名称 kwargs 是约定,双星号是关键): 收集多余的关键字参数,打包成一个字典 (dictionary)

    def print_details(**details):
        for key, value in details.items():
            print(f"{key}: {value}")
    print_details(name="Alice", age=30, city="New York")
    # 输出:
    # name: Alice
    # age: 30
    # city: New York
    
    • vs. Java: Java没有直接的语法等价物来收集任意命名参数到Map中。
2.5 仅限关键字参数 (Keyword-Only Arguments)

在函数定义时,如果某些参数出现在 *args 之后,或者单独用一个 * (不带名称) 之后的参数,那么这些参数在调用时必须以关键字参数的形式提供。

def kwo_example(a, b, *, c, d='default_d'): # c 和 d 必须用关键字参数
    print(f"a={a}, b={b}, c={c}, d={d}")

kwo_example(1, 2, c=3) # 正确: a=1, b=2, c=3, d=default_d
# kwo_example(1, 2, 3) # 错误: TypeError, c 必须是关键字参数
2.6 参数顺序总结

函数定义时参数的一般顺序是:

  1. 标准位置参数
  2. 带默认值的位置参数
  3. *args
  4. 仅关键字参数 (可带默认值)
  5. **kwargs
2.7 参数解包 (Unpacking Arguments)

在函数调用时,可以使用 *** 操作符进行参数解包:

  • 使用 *iterable:将序列或可迭代对象解包成独立的位置参数。
  • 使用 **dictionary:将字典解包成独立的关键字参数。
def add(x, y, z): return x + y + z

nums_list = [1, 2, 3]
print(add(*nums_list)) # 等价于 add(1, 2, 3) -> 输出 6

params_dict = {'x': 10, 'y': 20, 'z': 30}
print(add(**params_dict)) # 等价于 add(x=10, y=20, z=30) -> 输出 60

三、lambda 匿名函数:简洁的单行函数

&lt;a name="3-lambda-匿名函数">&lt;/a>

语法: lambda arguments: expression。

  • 核心特性:
    • 匿名: 没有正式名称。
    • 单一表达式: 函数体只能是一个表达式,其计算结果自动返回。
  • 用途: 常用于需要一个简单函数作为参数的场景,如 list.sort()key 参数、map()filter() 等。
points = [(1, 2), (3, 1), (5, -4), (2, 0)]
# 按每个元组的第二个元素 (y值) 排序
points.sort(key=lambda point: point[1])
print(points) # 输出: [(5, -4), (2, 0), (3, 1), (1, 2)]

numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers)) # [1, 4, 9, 16, 25]
  • vs. Java: 类似Java 8+ 中的Lambda表达式。但Python的 lambda 更受限,只能是单一表达式。

四、理解Python作用域:LEGB规则

&lt;a name="4-作用域-legb--">&lt;/a>

Python解释器查找变量名时遵循 LEGB 规则顺序:

  • Local: 函数内部定义的变量,包括参数。
  • Enclosing function locals: 嵌套函数中,对外层非全局函数的局部变量的查找。
  • Global: 在模块顶层定义的变量。
  • Built-in: Python 内置的名称,如 len(), print()
4.1 LEGB规则详解

当代码引用变量时,Python从当前作用域 (Local) 开始查找,逐级向上。若最终找不到,抛出 NameError。在函数内对变量赋值 (x = value) 时,默认在当前函数的局部作用域 (Local) 创建或更新该变量。

4.2 globalnonlocal 关键字
  • global variable_name: 在函数内部使用,声明 variable_name 指的是模块顶层的全局变量。这样,函数内对该变量的赋值会修改全局变量。 Python

    count = 0 # 全局变量
    def increment_global_counter():
        global count # 声明要修改的是全局的count
        count += 1
    increment_global_counter()
    print(count) # 输出: 1
    
  • nonlocal variable_name: 在嵌套函数的内部函数中使用,声明 variable_name 指的是其直接外层 (非全局) 函数中定义的变量(Enclosing作用域)。 Python

    def outer_function():
        message = "I am from outer"  # 外层函数的局部变量
        def inner_function():
            nonlocal message  # 声明 'message' 是外层函数的
            message = "Message changed by inner!"
            print(f"  Inner: {message}")
        inner_function()
        print(f"Outer (after inner call): {message}")
    outer_function()
    # 输出:
    #   Inner: Message changed by inner!
    # Outer (after inner call): Message changed by inner!
    
4.3 if/for 等代码块与作用域的区别 (vs. Java)

在Python中,ifforwhiletry等代码块不创建新的作用域。在这些块内定义的变量(若块被执行)属于包含它们的函数作用域或模块作用域。这是与Java(具有严格的花括号 {} 块级作用域)的一个显著区别。

def scope_test():
    if True:
        x = 10 # x 定义在 scope_test 函数的局部作用域内
    print(f"x inside function: {x}") # 在 if 块外依然可以访问 x,输出 10
scope_test()
# print(x) # 若在函数外访问x,则会 NameError

五、函数作为一等公民:高阶函数的基石

&lt;a name="5-函数作为一等公民-高阶函数">&lt;/a>

在Python里,函数也是一种对象,可以被平等对待:

  • 赋值给变量: my_func = original_func
  • 作为参数传递 (回调函数/高阶函数): 如 list.sort(key=my_comp_func)
  • 作为其他函数的返回值 (工厂函数/闭包)
  • vs. Java: Java 8+ 通过函数式接口和Lambda表达式实现了类似能力。

六、闭包 (Closures):捕获自由变量的函数

&lt;a name="6-闭包-closures">&lt;/a>

6.1 闭包的定义与条件

当一个嵌套的内部函数引用了其外部(包围)函数作用域中的变量(自由变量),并且这个外部函数返回了这个内部函数的引用时,就形成了闭包。闭包使得内部函数即使在其外部函数执行完成后,仍可访问那些被捕获的自由变量。

  • 条件: 1. 函数嵌套; 2. 内部函数引用外部变量; 3. 外部函数返回内部函数对象。
6.2 nonlocal 在闭包中的关键作用

如果闭包中的内部函数需要修改其捕获的外部函数的变量,必须使用 nonlocal 关键字。

def make_counter():
    count = 0  # 自由变量,被闭包捕获
    def counter():
        nonlocal count # 声明 count 不是局部变量
        count += 1
        return count
    return counter # 返回内部函数,形成闭包

counter1 = make_counter()
print(counter1())  # 输出: 1
print(counter1())  # 输出: 2 (count 状态被保持和修改)
  • 用途: 状态保持、延迟计算、装饰器基础。

七、装饰器 (Decorators):优雅地增强函数功能

&lt;a name="7-装饰器-decorators">&lt;/a>

7.1 装饰器的本质与 @ 语法糖

装饰器本质上是一个接收函数作为参数并返回一个新函数(或可调用对象)的函数。它用于在不修改被装饰函数源代码和调用方式的前提下,为其动态添加额外功能。Python的 @decorator_name 是应用装饰器的语法糖。

def log_calls(func): # 装饰器函数
    def wrapper(*args, **kwargs): # 内部包装函数
        print(f"Calling function '{func.__name__}' with args {args}, kwargs {kwargs}")
        result = func(*args, **kwargs) # 调用原始函数
        print(f"Function '{func.__name__}' returned {result}")
        return result
    return wrapper # 返回包装后的函数

@log_calls # 相当于 greet = log_calls(greet)
def greet(name, message="Hello"):
    return f"{message}, {name}!"

print(greet("Alice"))
# 输出:
# Calling function 'greet' with args ('Alice',), kwargs {}
# Function 'greet' returned Hello, Alice!
# Hello, Alice!
7.2 使用 functools.wraps 保留元信息

为避免被装饰函数的元信息(如 __name__, __doc__)被包装函数替换,应使用 functools.wraps 装饰器来装饰 wrapper 函数。

import functools

def log_calls_with_wraps(func):
    @functools.wraps(func) # 保留原函数的元信息
    def wrapper(*args, **kwargs):
        print(f"Calling function '{func.__name__}'...")
        result = func(*args, **kwargs)
        print(f"Function '{func.__name__}' finished.")
        return result
    return wrapper

@log_calls_with_wraps
def add(a, b):
    """This function adds two numbers."""
    return a + b

print(add(5, 3))
print(add.__name__) # 输出: add (而不是 wrapper)
print(add.__doc__)  # 输出: This function adds two numbers.
7.3 带参数的装饰器

如果装饰器本身需要接收参数,需要再额外嵌套一层函数。

def repeat(num_times): # 最外层,接收装饰器参数
    def decorator_repeat(func): # 标准装饰器
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs): # 包装函数
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result # 通常返回最后一次调用的结果
        return wrapper_repeat
    return decorator_repeat

@repeat(num_times=3)
def say_hello(name):
    print(f"Hello, {name}!")

say_hello("World")
# 输出三次 "Hello, World!"