Python 语法篇 · 第7章

函数:组织代码的基本单位

把重复逻辑封装成函数,是写好程序的第一步。本章彻底讲清 Python 灵活的参数体系、作用域、lambda 与递归。

🎯 学习目标

  • 理解函数的作用,会用 def 定义与调用函数
  • 彻底掌握五类参数:位置、关键字、默认、*args、**kwargs
  • 了解仅限位置参数 / 与仅限关键字参数 *
  • 理解 LEGB 作用域规则与 global/nonlocal
  • 掌握 lambda 及 map/filter/sorted 高阶函数
  • 会写递归、文档字符串与类型提示,避开可变默认参数陷阱

1定义与调用

函数是一段可重复使用、完成特定功能的代码块。使用函数的好处:复用代码、命名语义、隔离复杂度、便于测试。

def greet(name):                 # def 关键字 + 函数名 + (参数):
    """向某人打招呼。"""          # 文档字符串(函数说明书)
    return "你好," + name        # return 返回结果

msg = greet("小明")              # 调用:实参 "小明" 传给形参 name
print(msg)                       # 你好,小明
print(greet)                     # <function greet at ...> 函数也是对象
  • 形参:定义时括号里的变量(如 name);实参:调用时传入的具体值。
  • 没有 return 的函数默认返回 None。
  • 函数必须先定义后调用;函数体在调用时才执行。
# 先定义
def f():
    print("定义时不执行,调用时才执行")

f()       # 这里才真正执行函数体

2参数体系(本章重点)

2.1 位置参数与关键字参数

def introduce(name, age, city):
    return f"{name},{age}岁,来自{city}"

# 位置传参:按顺序对应
introduce("小明", 20, "北京")

# 关键字传参:按名字对应,顺序可随意,可读性好
introduce(age=20, city="北京", name="小明")

# 混用:位置参数必须在关键字参数前面
introduce("小明", city="北京", age=20)

2.2 默认参数

def power(x, n=2):        # n 有默认值,调用时可省略
    return x ** n

power(3)        # 9   n 取默认值 2
power(3, 3)     # 27  显式覆盖
power(2, n=4)   # 16

# 有默认值的参数必须放在无默认值参数之后
# def f(a=1, b): ...    # 语法错误!

2.3 可变参数 *args:接收任意多个位置参数

def my_sum(*nums):         # * 把多余的位置参数打包成【元组】
    print(nums)             # (1, 2, 3, 4)
    return sum(nums)

my_sum(1, 2, 3, 4)          # 10
my_sum()                    # ()  空元组,也合法

# 反过来:解包调用——把列表/元组拆成多个参数
def add(a, b, c):
    return a + b + c
args = [1, 2, 3]
add(*args)                  # 6

2.4 关键字可变参数 **kwargs

def show_info(name, **extra):    # ** 把多余关键字参数打包成【字典】
    print(name, extra)

show_info("小明", age=20, city="北京", hobby="读书")
# 小明 {'age': 20, 'city': '北京', 'hobby': '读书'}

# 解包字典调用
opts = {"b": 2, "c": 3}
add(1, **opts)            # 6(键名必须与形参对应)

典型的"万能函数签名":

def func(*args, **kwargs):
    print("任意位置参数:", args)
    print("任意关键字参数:", kwargs)

func(1, 2, x=3, y=4)

2.5 参数顺序与特殊分隔符

# 完整顺序:位置参数 → / → *args → 仅限关键字参数 → **kwargs
def f(a, b, /, c, d, *, e, **kwargs):
    # a,b 只能按位置传(/ 之前)
    # e 只能按关键字传(* 之后)
    return (a, b, c, d, e)

f(1, 2, 3, d=4, e=5)       # 合法
# f(a=1, b=2, ...)        # 报错:a、b 不允许关键字传
# f(1,2,3,4,5)            # 报错:e 必须写 e=5

# 仅关键字参数非常适合布尔开关,强制调用者写清楚含义
def split_text(text, *, strip=True, lower=False):
    ...
split_text(" Hi ", strip=True)   # 一眼看出开关意图

2.6 可变默认参数陷阱(面试必考)

默认值在函数定义时只创建一次!若默认值是可变对象(列表/字典),多次调用会共享同一个对象,造成诡异的状态累积:

# 错误写法
def add_item(item, bucket=[]):
    bucket.append(item)
    return bucket

print(add_item("a"))    # ['a']
print(add_item("b"))    # ['a', 'b']  ← 不是预期的 ['b']!

# 正确写法:用 None 作哨兵
def add_item(item, bucket=None):
    if bucket is None:
        bucket = []     # 每次调用都新建
    bucket.append(item)
    return bucket

3返回值

def stats(data):
    return min(data), max(data), sum(data) / len(data)
    # 多个值用逗号分隔 → 自动打包成元组

lo, hi, avg = stats([80, 90, 100])   # 解包接收
print(lo, hi, avg)                   # 80 100 90.0

def early_return(x):
    if x < 0:
        return None          # 卫语句提前返回,减少嵌套
    return x * 2

def no_return():
    print("只是做点事")
print(no_return())           # None(默认返回值)

4作用域:变量在哪里可见

Python 按 LEGB 顺序查找变量:Local(函数内)→ Enclosing(外层嵌套函数)→ Global(模块级)→ Built-in(内置,如 print/len)。

x = "global"                  # 全局变量

def outer():
    x = "enclosing"           # 外层函数局部变量
    def inner():
        x = "local"           # 内层局部变量
        print(x)              # local:最近优先
    inner()
    print(x)                  # enclosing

outer()
print(x)                      # global

global 与 nonlocal

count = 0

def visit():
    global count          # 声明:我要修改的是【全局】的 count
    count += 1

visit(); visit()
print(count)             # 2

def counter_maker():
    n = 0
    def add():
        nonlocal n       # 声明:修改【外层函数】的 n(不是新建局部变量)
        n += 1
        return n
    return add

c = counter_maker()
print(c(), c(), c())     # 1 2 3 —— 闭包计数器(第12章详讲)

函数内读取全局变量不需要声明;但只要函数内出现赋值,该变量就被视为局部变量。函数内修改全局/外层变量需 global/nonlocal。工程上应尽量少用 global,优先通过参数传入、return 返回。

5lambda 匿名函数

lambda 用来创建一句话的小型匿名函数,语法:lambda 参数: 表达式,表达式的值就是返回值(不能写语句)。

# 普通函数 vs lambda
def add(a, b):
    return a + b
add2 = lambda a, b: a + b
print(add2(2, 3))         # 5

# 最常见用途:作为 sorted/max/min 的 key
students = [("张三", 88), ("李四", 76), ("王五", 95)]
sorted(students, key=lambda s: s[1])           # 按分数排
max(students, key=lambda s: s[1])              # ('王五',95)

words = ["banana", "fig", "apple"]
sorted(words, key=lambda w: (len(w), w))       # 先按长度再按字母

# 字典按值排序
freq = {"a": 5, "b": 2, "c": 8}
sorted(freq.items(), key=lambda kv: kv[1], reverse=True)

lambda 只适合短小、即用即弃的逻辑。超过一个表达式或需要复用时,老老实实写 def,可读性更重要。

6高阶函数:函数作为参数

函数在 Python 中是"一等公民",可以当作参数传递、作为返回值。

nums = [1, 2, 3, 4]

# map:对每个元素做变换(现代 Python 更推荐列表推导式)
list(map(lambda x: x ** 2, nums))          # [1,4,9,16]
[x ** 2 for x in nums]                     # 等价推导式,更易读

# filter:保留满足条件的元素
list(filter(lambda x: x % 2 == 0, nums))   # [2,4]
[x for x in nums if x % 2 == 0]            # 等价推导式

# reduce:把序列"累积"成一个值(需导入)
from functools import reduce
reduce(lambda acc, x: acc + x, nums, 0)    # 10 累加
reduce(lambda acc, x: acc * x, nums, 1)    # 24 累乘

# 自定义高阶函数:把操作当作参数
def apply_twice(f, x):
    return f(f(x))
print(apply_twice(lambda n: n + 1, 10))    # 12

7递归:自己调用自己

递归必须有两个要素:基线条件(何时停止)与递归关系(如何缩小问题)。

# 阶乘:n! = n × (n-1)!,0!=1
def factorial(n):
    if n <= 1:                # 基线条件
        return 1
    return n * factorial(n - 1)   # 递归调用,规模减1

print(factorial(5))          # 120

# 斐波那契数列(朴素递归有大量重复计算)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print([fib(i) for i in range(10)])   # [0,1,1,2,3,5,8,13,21,34]

# 目录遍历、JSON 解析、树结构是递归的天然场景

Python 默认递归深度限制约 1000(sys.getrecursionlimit()),过深会 RecursionError;朴素 fib 递归时间复杂度 O(2ⁿ),实际项目用循环或缓存(functools.lru_cache,第 14 章讲)。

8文档字符串与类型提示

def divide(a: float, b: float) -> float:
    """计算 a 除以 b。

    Args:
        a: 被除数
        b: 除数,不能为 0
    Returns:
        商(浮点数)
    Raises:
        ZeroDivisionError: 当 b 为 0 时
    """
    return a / b

# help(divide) 可查看文档;类型提示只是"建议",解释器不强制
divide(10, 3)

# 常用类型提示
from typing import List, Dict, Optional, Union, Callable

def process(items: List[str],
            config: Dict[str, int],
            sep: Optional[str] = None) -> str:
    return (sep or ",").join(items)

# Python 3.10+ 可用内置写法:
# list[str]、dict[str,int]、str | None、X | Y

类型提示不会被运行时强制检查,但配合编辑器(Pylance)能实现自动补全与提前查错,在中大型项目与团队协作中强烈推荐。

9综合案例:文本处理工具函数集

def clean_text(text: str) -> str:
    """去除首尾空白并转小写。"""
    return text.strip().lower()

def tokenize(text: str, sep: str = " ") -> list:
    """按分隔符切词并过滤空串。"""
    return [w for w in text.split(sep) if w]

def word_frequency(words: list) -> dict:
    """统计词频。"""
    freq = {}
    for w in words:
        freq[w] = freq.get(w, 0) + 1
    return freq

raw = "  Python is Easy  Python is Fun  "
words = tokenize(clean_text(raw))
print(words)                # ['python','is','easy','python','is','fun']
print(word_frequency(words))  # {'python':2,'is':2,'easy':1,'fun':1}

10本章小结与练习

📌 关键结论

  • *args 收元组、**kwargs 收字典;解包方向相反(*列表、**字典)。
  • 默认参数必须不可变,可变默认值用 None 哨兵解决。
  • 作用域遵循 LEGB;函数内改全局/外层变量要 global/nonlocal,但应尽量避免。
  • lambda + key 是排序/查找的黄金搭档;复杂逻辑用 def。
  • 递归要有基线条件,警惕深度与重复计算。
练习1:定义函数 calc(a, b, op="+"),支持 +、-、*、/ 四种运算并返回结果。
用默认参数 + if 分支(或字典映射 op 到 lambda):ops={'+':lambda a,b:a+b,'-':lambda a,b:a-b,'*':lambda a,b:a*b,'/':lambda a,b:a/b}; return ops[op](a,b),除法前判断 b 是否为 0。
练习2:写函数接收任意个数字,返回它们的最大值(不使用 max 函数)。
def my_max(*nums): m = nums[0] for n in nums[1:]: if n > m: m = n return m 关键是 *nums 收集成元组,再遍历比较。可加 if not nums 处理空参数。
练习3:为什么默认参数写成 def f(data=[]) 很危险?正确写法是什么?
默认值在定义时只创建一次,多次调用不传参时共享同一个列表,append 的结果会累积。正确写法 data=None,函数体内 if data is None: data = [],保证每次调用独立新建。