跳转至

第 2 章 · 函数

函数是 Python 的头等公民(first-class citizen)。在 Java 里,方法必须挂在类上、想当"值"传递得靠函数式接口/方法引用绕一圈;在 Python 里,函数就是普通对象——能赋值、能塞进容器、能当参数传、能当返回值。这是你要适应的第一个大差异。


2.1 定义函数:def

  • Java

    public static int add(int a, int b) {
        return a + b;
    }
    
  • Python

    def add(a: int, b: int) -> int:
        return a + b
    

要点:

  • def 定义,参数和返回值的类型注解是可选的(运行时不强制,给工具看)。
  • 函数没有显式 return 时,自动返回 None(Java 是编译错误或 void)。
  • 文档字符串(docstring):函数体第一行的字符串,是 Python 版的 Javadoc:
def add(a: int, b: int) -> int:
    """Return the sum of a and b."""
    return a + b

2.2 函数是一等公民

这是和 Java 最大的心智差异之一:

  • Java:函数不是"值"

    // 要先定义函数式接口
    interface Func { int apply(int x); }
    Func square = (x) -> x * x;
    List<Integer> r = list.stream()
        .map(square::apply)
        .collect(Collectors.toList());
    
  • Python:函数就是对象

    def square(x): return x * x
    funcs = [square, abs, len]   # 函数塞进列表
    result = [square(n) for n in nums]
    

函数可以被赋值、传递、返回:

def apply(func, value):      # 把函数当参数传
    return func(value)

apply(square, 5)             # 25

Java 程序员的视角

Python 不需要 Function<T,R>Consumer<T>Supplier<T> 这一整套函数式接口——任何函数都能直接传递,签名靠类型注解表达(如 Callable[[int], int],第 8 章详讲)。


2.3 参数的四种姿势

Python 的参数远比 Java 灵活。掌握这四种,你就能读懂大部分 Python 代码。

① 默认参数

  • Java

    void greet(String name, String greeting) { ... }
    // Java 没有默认参数,得重载
    void greet(String name) { greet(name, "Hello"); }
    
  • Python

    def greet(name, greeting="Hello"):
        return f"{greeting}, {name}!"
    
    greet("Alice")              # "Hello, Alice!"
    greet("Bob", greeting="Hi") # 关键字传参
    

② 关键字参数(keyword arguments)

调用时可按 名字=值 传参,顺序无关——这在参数多时非常清晰:

create_user(name="Alice", age=30, active=True, role="admin")

Java 没有这个特性(要么靠 builder 模式模拟),这是 Python 的显著便利。

*args:可变位置参数(Java 的 int...

  • Java

    int sum(int... nums) {
        int total = 0;
        for (int n : nums) total += n;
        return total;
    }
    
  • Python

    def sum_all(*nums):
        return sum(nums)      # nums 是一个 tuple
    
    sum_all(1, 2, 3)          # 6
    

*nums 把所有多余的位置参数收集成 tuple

**kwargs:可变关键字参数

Java 完全没有对应物。它收集所有"名字=值"参数成一个 dict

def create_user(name, **fields):
    print(name, fields)

create_user("Alice", age=30, role="admin")
# Alice {'age': 30, 'role': 'admin'}

组合使用

def f(a, b, *args, **kwargs): ...
这是 Python 函数签名的"终极形态",很多框架(FastAPI、Django)靠它转发参数。


2.4 ⚠️ 可变默认参数陷阱(经典必踩)

这是 Python 最臭名昭著的坑,每个 Java 程序员都会踩一次:

致命陷阱

def add_item(item, basket=[]):   # 🚫 默认值是可变对象!
    basket.append(item)
    return basket

add_item("apple")   # ['apple']
add_item("banana")  # ['apple', 'banana']  ← 不是 ['banana']!

原因:默认参数在函数定义时只求值一次,所有调用共享同一个 list 对象。

为什么 Java 没这个问题:Java 没有默认参数,每次调用都新建容器。

正解:用 None 作哨兵,函数体里再创建:

def add_item(item, basket=None):
    if basket is None:
        basket = []        # 每次调用新建
    basket.append(item)
    return basket

Pythonic 写法

记住口诀:可变默认值(list/dict/set)一律用 None 哨兵替代


2.5 多返回值 = tuple 解包

Java 想返回多个值要造一个类、用 Pair/Record 或传输出参数。Python 直接"返回多个"(本质是返回 tuple):

  • Java

    class Result { int q; int r; ... }
    Result divmod(int a, int b) { ... }
    Result r = divmod(17, 5);
    
  • Python

    def divmod_(a, b):
        return a // b, a % b   # 返回 tuple
    
    q, r = divmod_(17, 5)      # 解包
    

解包还能交换变量(不需要临时变量):

a, b = b, a        # 一行交换

2.6 lambda:受限的匿名函数

  • Java lambda(可多语句)

    Function<Integer, Integer> f = x -> {
        int y = x + 1;
        return y * 2;
    };
    
  • Python lambda(只能单表达式)

    f = lambda x: (x + 1) * 2
    # 没有 {}、没有 return、只能一个表达式
    

⚠️ Java 程序员的陷阱

别把 Java 习惯带过来,到处用 lambda 写复杂逻辑。Python 的 lambda 只能是一个表达式,不能写语句块。需要复杂逻辑就老老实实 def 一个命名函数——Python 社区推崇"显式胜于隐式",命名函数远比一长串 lambda 清晰。

lambda 真正的舞台是作为简短的回调(sortedmapfilter 的 key):

sorted(users, key=lambda u: u["age"])

2.7 闭包与变量捕获

Python 闭包捕获的是变量本身(可观察到后续变化),而 Java lambda 要求捕获变量是 effectively final

def make_counters():
    count = 0
    def inc():
        nonlocal count      # 要修改外层变量,需声明 nonlocal
        count += 1
        return count
    return inc

c = make_counters()
c()   # 1
c()   # 2

与 Java 的区别

  • Java lambda 只能外层变量,且变量必须 effectively final。
  • Python 闭包能读写外层变量,写需要 nonlocal 声明。

还有个"晚绑定"陷阱值得知道:

funcs = [lambda: i for i in range(3)]
[f() for f in funcs]   # [2, 2, 2],不是 [0, 1, 2]!

因为 lambda 捕获的是变量 i 本身,循环结束时 i == 2。修正:lambda i=i: i(用默认参数固化当时的值)。


本章练习

练习 2.1

下面函数有什么 bug?修复它。

def register(name, tags=[]):
    tags.append("default")
    return {"name": name, "tags": tags}

参考答案

默认参数 tags=[] 是可变对象,会在多次调用间共享。改为:

def register(name, tags=None):
    if tags is None:
        tags = []
    tags.append("default")
    return {"name": name, "tags": tags}

练习 2.2

写一个函数 compose(f, g),返回一个新函数 h(x) = f(g(x))。体会"函数是一等公民"。

参考答案
def compose(f, g):
    def h(x):
        return f(g(x))
    return h
# 测试
add_one = lambda x: x + 1
double = lambda x: x * 2
f = compose(double, add_one)   # double(add_one(x))
assert f(3) == 8
练习 2.3

解释 funcs = [lambda: i for i in range(3)] 为何 [f() for f in funcs] 得到 [2, 2, 2],并给出两种修复方法。

参考答案

lambda 捕获变量 i 本身(引用),循环结束后 i 值为 2,故三个函数都返回 2。 修复一:默认参数固化 lambda i=i: i。修复二:用普通函数 + 闭包工厂,或用生成器/functools.partial


下一章:第 3 章 · 数据模型与"一切皆对象"