Python 函数:def、return、参数、*args 与 **kwargs、lambda 和类型注解
函数是 Python 中有名字的代码块,用 def 定义,通过名字加括号调用,并用 return 返回结果;没有 return 时函数返回 None。参数可以按位置或按名字传递,*args 把多余的位置参数收集为元组,**kwargs 把多余的关键字参数收集为字典,而 lambda 用来创建只含一个表达式的简短匿名函数。
Python 中的函数是一段有名字的代码,可以用不同的输入数据反复调用。用 def 声明它,通过参数接收数据,并通过 return 返回结果。函数是把冗长的脚本拆分成清晰、可检验步骤的主要手段。
如何定义函数:def
函数声明以 def 开头,后面是名字、括号中的参数和冒号。函数体要缩进四个空格——缩进在 Python 中是语法的一部分,而不是装饰。
def greet(name):
"""返回给用户的问候语。"""
return f"你好,{name}!"
print(greet("安娜")) # 你好,安娜!
紧跟在函数头后面的三引号字符串是 docstring,即函数的文档。help(greet) 和编辑器的提示都会显示它。根据 PEP 8 的约定,函数名使用 snake_case 风格:add_expense、get_rate。
def greet(name): 这一行被称为函数签名:名字加上参数列表(如果有注解,还包括注解)。读者只看签名就能知道如何调用函数,无需阅读函数体。
return 在 Python 中的作用
return 结束函数,并把值交给调用者。执行 return 之后的代码不会再运行。如果没有 return,或者 return 后面没有值,函数返回 None。
def area(w, h):
w * h # 结果被计算出来,然后丢弃
print(area(3, 4)) # None
def area(w, h):
return w * h
print(area(3, 4)) # 12
忘记写 return 是新手最常见的错误之一:函数“能运行”,却返回 None,错误会在下一行才显现出来。函数也可以返回多个值——实际上它返回的是一个元组,可以方便地解包:
def min_max(numbers):
return min(numbers), max(numbers)
low, high = min_max([4, 1, 9])
print(low, high) # 1 9
提前使用 return 可以减少多余的嵌套:先处理特殊情况,再写主要逻辑。
def discount(price, is_member):
if price <= 0:
return 0
if not is_member:
return price
return price * 0.9
Python 函数的参数:位置参数与关键字参数
形参是函数声明中的名字,实参是调用时传入的具体值。实参可以按顺序传递(位置参数),也可以按名字传递(关键字参数)。关键字参数让调用更容易理解,并且顺序不再重要。
def add_expense(title, amount, category="其他"):
return {"title": title, "amount": amount, "category": category}
add_expense("咖啡", 250) # 按位置
add_expense(amount=250, title="咖啡") # 按名字
add_expense("打车", 600, category="交通") # 混合使用
带默认值的参数是可选的。规则是:带默认值的参数必须放在没有默认值的参数之后;调用时,位置参数必须写在关键字参数之前。否则 Python 会报 SyntaxError。
陷阱:可变的默认值
默认值只在执行 def 语句时计算一次,而不是在每次调用时计算。如果默认值是列表或字典,它会被所有调用共享。
def add_item(item, acc=[]):
acc.append(item)
return acc
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] — 列表“记住”了上一次的调用
正确的做法是默认值设为 None,并在函数内部创建新对象:
def add_item(item, acc=None):
if acc is None:
acc = []
acc.append(item)
return acc
*args 与 **kwargs:任意数量的参数
参数前面的一个星号会把所有多余的位置参数收集为元组(tuple),两个星号则把所有多余的关键字参数收集为字典(dict)。名字 args 和 kwargs 只是约定俗成,真正起作用的是星号。
def total(*args):
return sum(args)
print(total(100, 250, 40)) # 390
def make_user(name, **kwargs):
user = {"name": name}
user.update(kwargs)
return user
print(make_user("安娜", city="喀山", age=30))
# {'name': '安娜', 'city': '喀山', 'age': 30}
同样的星号也可以反过来使用——在调用时把序列或字典解包为实参:
values = [3, 4]
print(pow(*values)) # 81
opts = {"sep": " | ", "end": "\n"}
print("a", "b", **opts) # a | b
只能按位置与只能按名字的参数
参数列表中的单斜杠 / 和星号 * 起着“栅栏”的作用:/ 之前的参数只能按顺序传递(这一语法出现在 Python 3.8,PEP 570),* 之后的参数只能按名字传递。
def convert(amount, /, *, rate, currency="USD"):
return round(amount / rate, 2), currency
convert(100, rate=90.5) # (1.1, 'USD')
convert(amount=100, rate=90.5) # TypeError:amount 只能按位置传递
convert(100, 90.5) # TypeError:rate 只能按名字传递
只能按名字传递的参数可以防止像 convert(100, 90.5, "EUR") 这样的调用产生混淆,因为它不清楚每个值代表什么。
作用域:LEGB 规则
Python 查找变量名时按照以下顺序:Local(函数内部)→ Enclosing(外层函数)→ Global(模块层级)→ Built-in(内置名字,如 len)。在函数内部读取全局变量是可以的;但要给它赋新值,需要使用 global 声明。
limit = 1000
def check(amount):
return amount > limit # 读取全局变量——可以
def raise_limit():
global limit
limit = 2000 # 没有 global 的话,会创建一个局部变量
实践中最好避免使用 global:修改外部状态的函数很难测试。更可靠的方式是通过参数接收值,并返回结果。
嵌套函数与闭包
函数可以定义在另一个函数内部。内层函数即使在外层函数结束之后,仍然可以访问外层的变量——这就是闭包。要修改这样的变量,需要使用 nonlocal。
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
print(c(), c(), c()) # 1 2 3
闭包是装饰器和函数工厂的基础。
函数也是对象
在 Python 中,函数是普通的对象:可以把它赋给变量、作为参数传给另一个函数、从函数中返回,也可以存放在字典中。函数还拥有一些属性:__name__、__doc__、__defaults__、__annotations__。
def double(x):
return x * 2
f = double
print(f(5), f.__name__) # 10 double
print(list(map(double, [1, 2, 3]))) # [2, 4, 6]
Python 中的 lambda:匿名函数
lambda 函数是没有名字的简短函数,函数体只能是一个表达式。表达式的结果会被自动返回,不能写 return。
square = lambda x: x ** 2
print(square(4)) # 16
expenses = [("咖啡", 250), ("打车", 600), ("午饭", 450)]
expenses.sort(key=lambda e: e[1], reverse=True)
print(expenses) # [('打车', 600), ('午饭', 450), ('咖啡', 250)]
print(list(filter(lambda n: n % 2 == 0, range(10)))) # [0, 2, 4, 6, 8]
lambda 适合作为 sorted、max、map、filter 的简短参数。但 PEP 8 不推荐像上面的 square 那样把 lambda 赋给变量:如果函数需要名字,就用 def——这样它在 traceback 中有正常的 __name__,并且可以写 docstring。
| def | lambda | |
|---|---|---|
| 名字 | 有 | 没有(显示为 <lambda>) |
| 函数体 | 任意数量的语句 | 一个表达式 |
| return | 显式 | 隐式 |
| Docstring、注解 | 有 | 没有 |
| 适用场景 | 任何情况 | 简短的 key 或回调 |
类型注解:提示,而不是检查
注解描述参数和返回值的预期类型。Python 运行时不会检查它们:带有 x: int 的函数仍然会接受字符串。检查注解的是外部工具——mypy、pyright、ty——以及在运行之前就能标出错误的编辑器。
def add_expense(title: str, amount: float, category: str = "其他") -> dict[str, object]:
return {"title": title, "amount": amount, "category": category}
def area(w: float, h: float) -> float:
w * h # mypy: Missing return statement
不安装即可运行检查:uvx mypy .。上面的例子展示了注解的价值:在 -> float 的函数中忘记 return,分析器会立即发现。
自 Python 3.12 起(PEP 695),泛型函数可以不用 TypeVar 来编写:类型参数写在方括号中,类型别名用关键字 type 声明。
def first[T](xs: list[T]) -> T:
return xs[0]
type Money = int | float
在 Python 3.14 中(PEP 649),注解变成了惰性求值:它们在第一次访问时才会计算,因此可以不加引号引用后面才定义的类,也不再需要 from __future__ import annotations。
“没有传参”还是“传了 None”:3.15 中的 sentinel
把默认值设为 None 时,无法区分“没有传参”和“明确传入了 None”。多年来人们用自定义的占位符解决这个问题:
_MISSING = object()
def get_rate(currency, default=_MISSING):
rates = {"USD": 90.5}
if currency in rates:
return rates[currency]
if default is _MISSING:
raise KeyError(currency)
return default
Python 3.15 新增了内置函数 sentinel(PEP 661):MISSING = sentinel("MISSING") 创建一个唯一的标记,它有清晰的 repr,复制后依然是它本身,可以用 pickle 序列化,并且可以用在 float | MISSING 这样的注解中。在旧版本中,用 object() 的方法仍然有效。
函数中的常见错误
- 忘记 return — 函数返回
None。 - 忘记括号 —
print(greet)输出的是函数对象,而不是调用的结果。 - 可变的默认值 — 所有调用共享同一个列表。
- 参数数量错误 —
TypeError: greet() missing 1 required positional argument: 'name'。 - 名字与内置函数相同 — 自定义的
list或sum会遮盖内置函数。
小结
Python 函数用 def 定义,通过 return 返回结果,参数可以按顺序或按名字传递。*args 和 **kwargs 提供灵活性,/ 和 * 保证调用的严格性,lambda 适合一次性的简短函数,而类型注解配合 mypy 能在运行前发现错误。课程中这一主题同样结合实践讲解:支出记录器被拆分为带注解的函数,并用 mypy 检查。
常见问题
形参和实参有什么区别?
形参是函数声明中的名字,实参是调用时传入的具体值。在 def greet(name) 中,name 是形参;在 greet("安娜") 中,字符串 "安娜" 是实参。
能否在函数声明之前调用它?
在模块层级不行:def 语句必须先执行,否则会出现 NameError。但在另一个函数内部,只要调用发生在声明之后,就可以调用后面定义的函数。
一个函数可以返回多个值吗?
从形式上看只返回一个值,但这个值可以是元组:return a, b 返回一个含两个元素的元组,可以用 x, y = f() 解包。
必须写类型注解吗?
不必要,Python 没有它们也能运行。但在供他人阅读的代码中,注解大大方便维护,并且能让 mypy 或 pyright 在运行前发现错误。
lambda 中可以使用 if 吗?
可以,但只能是条件表达式:lambda x: "偶" if x % 2 == 0 else "奇"。lambda 中不允许使用语句、循环和赋值。