Python 函数详解:从语法到参数传递的思考
函数是 Python 中最核心、最常用的特性之一。掌握函数,不仅能写出可复用的代码,更能深刻理解 Python 的“一切皆对象”和参数传递机制。本文从基础语法开始,逐步深入到参数传递的本质,最后给出实际思考和最佳实践。
1. 函数的基本语法
定义函数
def 函数名(参数列表):
"""文档字符串(可选,但强烈推荐)"""
# 函数体
语句...
return 返回值 # 可选,不写则返回 None
示例:
def greet(name: str) -> str:
"""问候函数"""
return f"你好,{name}!"
print(greet("小明")) # 你好,小明!
- 类型提示(
name: str,-> str):Python 3.5+ 引入,属于注解,不强制执行,但 IDE 和类型检查工具(如 mypy)会使用。 - 文档字符串:用三引号,
help(greet)或 IDE 会显示。
调用函数
result = greet("小明")
2. 参数的种类(从简单到高级)
Python 函数参数非常灵活,支持多种传递方式。参数顺序规则(Python 3.8+):
**位置参数(Positional-only) / 普通参数 / * 可变位置参数 / 仅关键字参数 / ** 可变关键字参数**
(1) 位置参数与关键字参数
def add(a, b):
return a + b
add(3, 5) # 位置参数
add(a=3, b=5) # 关键字参数
add(b=5, a=3) # 关键字参数顺序可变
(2) 默认参数
默认参数必须放在非默认参数之后。
def power(base, exp=2):
return base ** exp
power(3) # 9
power(3, 4) # 81
(3) 可变参数:*args 和 **kwargs
def print_info(title, *args, **kwargs):
print("标题:", title)
print("位置参数:", args) # tuple
print("关键字参数:", kwargs) # dict
print_info("测试", 1, 2, 3, name="张三", age=18)
输出:
标题: 测试
位置参数: (1, 2, 3)
关键字参数: {'name': '张三', 'age': 18}
(4) Python 3.8+ 新特性:位置仅参数(/)和仅关键字参数(*)
def modern_func(a, b, /, c, *, d=10):
# a, b 必须用位置方式传入
# c 可以位置或关键字
# d 必须用关键字
pass
modern_func(1, 2, 3, d=20) # 正确
# modern_func(1, 2, c=3) # 正确
# modern_func(a=1, b=2, c=3) # 错误!a,b 不能用关键字
这个特性在库设计中非常有用(如 divmod()、range()),能强制调用者使用更清晰的方式。
3. 参数传递机制:核心思考点
Python 的参数传递本质是“按对象引用传递”(Pass by Object Reference),或者更准确地说是 “按赋值传递”(Pass by Assignment)。
Python 中一切都是对象。函数调用时,参数名只是对传入对象的引用(指针),类似赋值操作 param = argument。
关键区别:可变对象 vs 不可变对象
| 类型 | 示例 | 修改行为 | 对外部影响 |
|---|---|---|---|
| 不可变 | int, float, str, tuple, frozenset | 创建新对象 | 无影响 |
| 可变 | list, dict, set, 自定义类实例 | 原地修改(in-place) | 有影响 |
实验验证:
def modify_list(lst):
lst.append(4) # 原地修改可变对象
l = [1, 2, 3]
modify_list(l)
print(l) # [1, 2, 3, 4] ← 外部被修改!
def modify_int(x):
x = 10 # 重新绑定局部变量
n = 5
modify_int(n)
print(n) # 5 ← 外部不变!
为什么会这样?
lst.append(4):操作的是列表对象本身,lst和外部l指向同一个对象。x = 10:重新让局部变量x指向一个新的整数对象 10,外部n仍然指向 5。
这不是传统的“传值”也不是“传引用”,而是 传引用但赋值时重新绑定。
思考:
- 优点:高效!大对象(几 GB 的 DataFrame)不需要拷贝,直接传递引用。
- 缺点:容易产生副作用(side effects),尤其在多线程或大型项目中。
- 设计哲学:Python 相信程序员的责任感(“We are all consenting adults here”),不强制拷贝,但要求你了解对象可变性。
4. 常见陷阱与最佳实践
陷阱1:可变默认参数(最经典的坑!)
def bad_func(a=[]):
a.append(1)
return a
print(bad_func()) # [1]
print(bad_func()) # [1, 1]
print(bad_func()) # [1, 1, 1]
原因:默认参数在函数定义时只计算一次,可变对象会被所有调用共享。
正确写法:
def good_func(a=None):
if a is None:
a = [] # 每次调用新建列表
a.append(1)
return a
陷阱2:不必要的拷贝
# 坏:大列表每次都拷贝
def process(data):
data = data[:] # 浅拷贝
# 好:明确告诉调用者会修改
def process_inplace(data):
data.append("new")
# 或返回新对象
return [x*2 for x in data]
最佳实践建议:
- 修改可变对象时:要么原地修改并返回
None,要么返回新对象(函数式风格)。 - 团队协作:函数名或文档明确说明是否会修改参数(
inplace=True风格)。 - 大型项目:优先使用不可变数据结构(
tuple、frozendict、pydantic等)或返回新对象。
5. 进阶思考:函数设计的哲学
- Python 函数是一等公民:可以赋值、作为参数、作为返回值、放在容器里。
- 闭包与作用域(LEGB 规则):函数可以捕获外部变量,形成闭包。
- 装饰器:本质是函数包裹函数。
def timer(func):
def wrapper(*args, **kwargs):
import time
start = time.time()
result = func(*args, **kwargs)
print(f"耗时: {time.time()-start:.4f}s")
return result
return wrapper
@timer
def slow_func():
time.sleep(1)
- 函数式编程倾向:鼓励
map、filter、lambda、functools等,减少副作用。
总结
- 语法:灵活 + 类型提示 + 文档字符串 = 现代 Python 函数。
- 参数传递:按对象引用传递 → 可变对象有副作用,不可变对象安全。
- 核心思考:Python 把控制权交给程序员,同时要求程序员理解对象模型。
掌握了参数传递机制,你就真正理解了“Pythonic”的精髓:简洁、高效、但需要对对象有敬畏之心。
想深入哪个部分(装饰器、生成器、异步函数、类型提示进阶等)?随时告诉我,我可以继续展开!