在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
是一个数组。
- vs. Java: 类似Java的可变参数 (varargs)
-
**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 参数顺序总结
函数定义时参数的一般顺序是:
- 标准位置参数
- 带默认值的位置参数
*args
- 仅关键字参数 (可带默认值)
**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
匿名函数:简洁的单行函数
<a name="3-lambda-匿名函数"></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规则
<a name="4-作用域-legb--"></a>
Python解释器查找变量名时遵循 LEGB 规则顺序:
- Local: 函数内部定义的变量,包括参数。
- Enclosing function locals: 嵌套函数中,对外层非全局函数的局部变量的查找。
- Global: 在模块顶层定义的变量。
- Built-in: Python 内置的名称,如
len()
,print()
。
4.1 LEGB规则详解
当代码引用变量时,Python从当前作用域 (Local) 开始查找,逐级向上。若最终找不到,抛出 NameError
。在函数内对变量赋值 (x = value
) 时,默认在当前函数的局部作用域 (Local) 创建或更新该变量。
4.2 global
与 nonlocal
关键字
global variable_name
: 在函数内部使用,声明variable_name
指的是模块顶层的全局变量。这样,函数内对该变量的赋值会修改全局变量。 Pythoncount = 0 # 全局变量 def increment_global_counter(): global count # 声明要修改的是全局的count count += 1 increment_global_counter() print(count) # 输出: 1
nonlocal variable_name
: 在嵌套函数的内部函数中使用,声明variable_name
指的是其直接外层 (非全局) 函数中定义的变量(Enclosing作用域)。 Pythondef 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中,if
、for
、while
、try
等代码块不创建新的作用域。在这些块内定义的变量(若块被执行)属于包含它们的函数作用域或模块作用域。这是与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
五、函数作为一等公民:高阶函数的基石
<a name="5-函数作为一等公民-高阶函数"></a>
在Python里,函数也是一种对象,可以被平等对待:
- 赋值给变量:
my_func = original_func
- 作为参数传递 (回调函数/高阶函数): 如
list.sort(key=my_comp_func)
。 - 作为其他函数的返回值 (工厂函数/闭包)。
- vs. Java: Java 8+ 通过函数式接口和Lambda表达式实现了类似能力。
六、闭包 (Closures):捕获自由变量的函数
<a name="6-闭包-closures"></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):优雅地增强函数功能
<a name="7-装饰器-decorators"></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!"
Comments NOTHING