基础

Python 函数:def、return、参数、*args 与 **kwargs、lambda 和类型注解

更新于: · Skillaria.Top

函数是 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。

deflambda
名字有没有(显示为 <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 中不允许使用语句、循环和赋值。

通过视频学习本主题

课程“Python 3.15: от новичка до профи”结合实践详细讲解了这一主题。

前往课程