Python 中文教程

函数

默认参数和关键字参数让 Python 函数比 Java 方法灵活得多,也带来一个必须知道的陷阱。

  • 第 4 章 · 函数与数据
  • 24 分钟
  • 10 组对比

定义函数

def 定义函数。参数和返回值都不写类型,没有 return 时返回 None。

函数定义

Python
def add(a, b):
    """返回两数之和。"""
    return a + b

print(add(2, 3))      # => 5
print(add("a", "b"))  # => ab
static int add(int a, int b) {
    return a + b;
}

System.out.println(add(2, 3));  // => 5
对比项PythonJava
定义关键字def没有,直接写返回类型
参数类型可选必须写
返回类型可选必须写
没有返回值返回 Nonevoid
定义位置文件顶层类里

只要 a + b 这个操作成立,函数就能运行,这叫鸭子类型。函数体第一行的字符串是文档字符串,help(add) 能看到它。

函数是普通对象,可以赋值、传参和返回。写 f = add 之后,f(2, 3) 和 add(2, 3) 完全一样。

默认参数和关键字参数

调用时可以写参数名,这样可以跳过中间的默认参数,也不用记顺序。

默认值和命名调用

Python
def connect(host, port=80, timeout=30):
    return f"{host}:{port} {timeout}s"

print(connect("a.com"))
print(connect("a.com", timeout=5))
print(connect(port=443, host="b.com"))
static String connect(String host) {
    return connect(host, 80, 30);
}
static String connect(
        String host, int port, int timeout) {
    return host + ":" + port + " " + timeout + "s";
}

System.out.println(connect("a.com"));
对比项PythonJava
默认参数port=80不支持,靠方法重载
命名调用timeout=5不支持
跳过中间参数支持不支持
调用顺序位置参数在前,命名参数在后只能按位置传

这是 Python 和 Kotlin 最接近的地方之一。没有默认参数的语言通常用重载或 Builder 模式。

默认值只计算一次

Python
def bad(x, items=[]):
    items.append(x)
    return items

print(bad(1))  # => [1]
print(bad(2))  # => [1, 2]

def good(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

print(good(2))  # => [2]
static List<Integer> add(int x) {
    return add(x, new ArrayList<>());
}
static List<Integer> add(
        int x, List<Integer> items) {
    items.add(x);
    return items;
}

System.out.println(add(1));  // => [1]
System.out.println(add(2));  // => [2]
对比项PythonJava
默认值何时计算def 执行时计算一次每次调用重载方法时
多次调用共用同一个对象每次新建
可变默认值写 None,在函数体里创建没有这个问题

列表、字典这类可变对象不能当默认值,这是 Python 最经典的陷阱。

限定参数的传递方式选读

参数列表里的 / 和 * 可以限定传参方式。/ 之前只能按位置传,* 之后只能按名字传。

Python
def send(to, /, body, *, urgent=False):
    return (to, body, urgent)

print(send("ann", "hi", urgent=True))  # => ('ann', 'hi', True)
print(send("ann", body="hi"))          # => ('ann', 'hi', False)

写库的时候用得上,日常业务代码很少需要。

可变参数和解包

*args 收集多余的位置参数,得到元组。**kwargs 收集多余的关键字参数,得到字典。

可变参数

Python
def total(*nums, **opts):
    return sum(nums) * opts.get("scale", 1)

print(total(1, 2))           # => 3
print(total(1, 2, scale=10)) # => 30

args = [1, 2, 3]
opts = {"scale": 2}
print(total(*args, **opts))  # => 12
static int total(int... nums) {
    return Arrays.stream(nums).sum();
}

System.out.println(total(1, 2));  // => 3
int[] args = {1, 2, 3};
System.out.println(total(args));  // => 6
对比项PythonJava
收集位置参数*nums,得到元组int... nums,得到数组
收集命名参数**opts,得到字典不支持,改传 Map
调用时展开*args、**opts直接传数组

* 把序列展开成位置参数,** 把字典展开成关键字参数,定义和调用用的是同一对符号。

Lambda 和闭包

lambda 是单表达式的匿名函数。逻辑超过一个表达式时,直接在函数里定义一个内部函数。

传递函数

Python
words = ["pear", "fig", "apple"]

words.sort(key=lambda w: len(w))
print(words)  # => ['fig', 'pear', 'apple']

words.sort(key=len)   # 函数本身也能传
square = lambda x: x * x
print(square(4))      # => 16
var words = new ArrayList<>(
    List.of("pear", "fig", "apple"));

words.sort(Comparator.comparing(String::length));
System.out.println(words);  // => [fig, pear, apple]

Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(4));  // => 16
对比项PythonJava
语法lambda x: x * xx -> x * x
函数体只能一个表达式表达式或代码块
单参数简写没有没有
传已有函数key=lenString::length
调用方式square(4)square.apply(4)

逻辑超过一个表达式时,直接在函数内部 def 一个函数再传出去。

作用域

名字按 LEGB 顺序查找:局部、外层函数、全局、内置。函数、类、模块和推导式会创建作用域,if、for、while 不会。

if 和 for 不创建作用域

Python
for i in range(3):
    last = i * 2

print(i, last)  # => 2 4

if True:
    msg = "可见"
print(msg)      # => 可见
int last = 0;
for (int i = 0; i < 3; i++) {
    last = i * 2;
}
// System.out.println(i);  编译错误
System.out.println(last);  // => 4
对比项PythonJava
块级作用域没有有
循环变量循环后仍然存在循环后失效
创建作用域的结构函数、类、模块、推导式任何花括号
声明变量首次赋值时创建必须先声明

这一点最容易被忽略。注意循环变量可能遮盖外面的同名变量。

在函数里给变量赋值,默认创建局部变量。要修改外层的变量,必须先声明。

修改外层变量

Python
def make_counter():
    n = 0
    def step():
        nonlocal n
        n += 1
        return n
    return step

c = make_counter()
c()
print(c())  # => 2
static Supplier<Integer> makeCounter() {
    var n = new AtomicInteger();
    return () -> n.incrementAndGet();
}

var c = makeCounter();
c.get();
System.out.println(c.get());  // => 2
对比项PythonJava
修改外层局部变量声明 nonlocal不允许,要借助对象
修改全局变量声明 global直接修改静态字段
闭包捕获捕获变量本身捕获值,变量必须实际不可变
读取外层变量相同直接读取

不声明 nonlocal 时,n += 1 会抛 UnboundLocalError,因为赋值让 n 变成了局部变量。

装饰器

前面已经见过 @property、@dataclass 这些写法,它们都是装饰器。装饰器是一个接收函数、返回新函数的函数,@logged 写在 def 上方,等价于定义后执行 add = logged(add)。

给函数加上日志

Python
import functools

def logged(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("调用", func.__name__)
        return func(*args, **kwargs)
    return wrapper

@logged
def add(a, b):
    return a + b

print(add(1, 2))  # => 3
static <A, B> Function<A, B> logged(
        String name, Function<A, B> func) {
    return arg -> {
        System.out.println("调用 " + name);
        return func.apply(arg);
    };
}

Function<Integer, Integer> twice =
    logged("twice", x -> x * 2);
System.out.println(twice.apply(2));  // => 4
对比项PythonJava
@ 的含义调用函数,替换被装饰的函数注解,只是元数据
谁来生效定义函数时立即执行编译器或框架读取注解
实现横切逻辑写一个装饰器动态代理或 AOP
底层思路相同函数包裹函数

@ 在 Python 里会真正替换函数,这和 Java、Kotlin 的注解完全不同。@functools.wraps 保留原函数的名字和文档,写装饰器时都要加上。

functools 里有几个现成的工具:@lru_cache 给函数加缓存,partial 固定函数的部分参数,reduce 做累积计算。

文档字符串和类型注解

类型注解是可选的,解释器运行时完全不检查。它的价值在于 IDE 补全和 mypy、pyright 这样的静态检查工具。

带类型的函数

Python
def area(w: float, h: float = 1.0) -> float:
    """计算矩形面积。

    首行写摘要,空一行后再写细节。
    """
    return w * h

print(area(2, 3))      # => 6
print(area("ab", 2))   # => abab
/** 计算矩形面积。 */
static double area(double w, double h) {
    return w * h;
}

System.out.println(area(2, 3));  // => 6.0
对比项PythonJava
类型标注可选必须写
何时检查运行时不检查编译时
检查工具mypy、pyrightjavac
文档写法函数体内首行字符串函数前的 /** */

area("ab", 2) 能运行,说明注解不影响运行结果。新项目建议从一开始就写注解。

注解保存在函数的 __annotations__ 属性里,框架可以在运行时读取,FastAPI 和 pydantic 就靠它工作。

常用的类型写法

泛型、可空和函数类型

Python
from collections.abc import Callable

def first[T](xs: list[T]) -> T | None:
    return xs[0] if xs else None

def call(f: Callable[[int], int], x: int) -> int:
    return f(x)

ages: dict[str, int] = {"Ann": 30}

print(first([1, 2]))             # => 1
print(first([]))                 # => None
print(call(lambda n: n + 1, 1))  # => 2
static <T> Optional<T> first(List<T> xs) {
    return xs.stream().findFirst();
}

static int call(IntUnaryOperator f, int x) {
    return f.applyAsInt(x);
}

Map<String, Integer> ages = Map.of("Ann", 30);

System.out.println(first(List.of(1, 2)));  // => Optional[1]
System.out.println(call(n -> n + 1, 1));   // => 2
对比项PythonJava
容器类型list[int]、dict[str, int]List<Integer>、Map<String, Integer>
可能为空T | NoneOptional<T> 或 @Nullable
泛型函数def f[T](x: T)<T> void f(T x)
函数类型Callable[[int], int]Function、IntUnaryOperator 等接口
类型别名type Ids = list[int]不支持
空值保护只有类型检查工具提示只有工具提示

def f[T] 和 type 语句是 3.12 的新语法。老代码里常见 Optional[str]、List[int] 和 TypeVar,含义相同。

编码风格

Python 社区统一遵循 PEP 8 规范。写之前先记住下面的命名习惯,它们和 Java、Kotlin 有明显区别。格式化工具用 ruff format 或 black。

命名与风格
PythonJava说明
snake_casecamelCase函数和变量
PascalCasePascalCase类名
UPPER_CASEUPPER_CASE常量
snake_case.pyPascalCase.java文件名
selfthisPython 要显式写在参数里
_internalprivatePython 只是约定
每行不超过 79 字符100 到 120 字符很多团队会放宽到 88 或 120

本章要点

  • 默认参数和关键字参数的用法和 Kotlin 几乎相同
  • 默认值只计算一次,可变对象一律用 None 代替
  • *args 收集成元组,**kwargs 收集成字典,调用时可以反向展开
  • 只有函数、类、模块创建作用域,for 和 if 不创建
  • 修改外层变量要声明 nonlocal 或 global
  • 装饰器是包裹函数的函数,@ 会真正替换函数,和 Java 注解不同
  • 类型注解运行时不检查,要配合 mypy 或 pyright
  • 可空类型写 T | None,3.12 起泛型函数写 def f[T](x: T)

官方原文:定义函数作用域和命名空间