1.2. 函数进阶

Python 函数是一等公民,理解其高级特性对于写出优雅代码至关重要。

1.2.1. 参数传递机制

1.2.1.1. 易错点:传值 vs 传引用

Python 使用"传对象引用"(pass by object reference),理解这一点很重要:

def modify_list(lst):
    lst.append(4)       # 修改原对象
    lst = [1, 2, 3, 4, 5]  # 重新绑定局部变量,不影响原对象

original = [1, 2, 3]
modify_list(original)
print(original)  # [1, 2, 3, 4] - append 生效,重新赋值无效

def modify_number(n):
    n = n + 1  # 创建新对象,不影响原变量

x = 10
modify_number(x)
print(x)  # 10 - 不变

警告

可变默认参数陷阱是 Python 最著名的陷阱之一:

# ❌ 错误示例
def append_to(element, to=[]):
    to.append(element)
    return to

print(append_to(1))  # [1]
print(append_to(2))  # [1, 2] - 不是 [2]!
print(append_to(3))  # [1, 2, 3]

# ✅ 正确做法
def append_to_fixed(element, to=None):
    if to is None:
        to = []
    to.append(element)
    return to

1.2.2. *args**kwargs

1.2.2.1. 位置参数与关键字参数

def flexible_func(required, *args, keyword_only, **kwargs):
    """
    参数顺序:
    1. 普通参数
    2. *args(收集额外位置参数)
    3. keyword-only 参数(*args 之后的参数)
    4. **kwargs(收集额外关键字参数)
    """
    print(f"required: {required}")
    print(f"args: {args}")
    print(f"keyword_only: {keyword_only}")
    print(f"kwargs: {kwargs}")

flexible_func(1, 2, 3, 4, keyword_only="must specify", extra="value")
# required: 1
# args: (2, 3, 4)
# keyword_only: must specify
# kwargs: {'extra': 'value'}

1.2.2.2. 仅位置参数(Python 3.8+)

def pos_only(x, y, /, z):
    """x 和 y 只能通过位置传递"""
    return x + y + z

pos_only(1, 2, 3)      # ✅
pos_only(1, 2, z=3)    # ✅
# pos_only(x=1, y=2, z=3)  # ❌ TypeError

# 实际应用:避免参数名变更破坏 API
def compute(x, y, /, *, precision=2):
    """x, y 位置参数;precision 关键字参数"""
    return round(x / y, precision)

1.2.2.3. 参数解包

def point(x, y, z):
    return f"({x}, {y}, {z})"

# 列表/元组解包
coords = [1, 2, 3]
print(point(*coords))  # (1, 2, 3)

# 字典解包
params = {'x': 1, 'y': 2, 'z': 3}
print(point(**params))  # (1, 2, 3)

# 合并字典(Python 3.9+)
defaults = {'a': 1, 'b': 2}
overrides = {'b': 3, 'c': 4}
combined = {**defaults, **overrides}  # {'a': 1, 'b': 3, 'c': 4}
# 或使用 | 运算符
combined = defaults | overrides  # Python 3.9+

1.2.3. 闭包

闭包是函数式编程的基础,理解它对掌握装饰器至关重要。

1.2.3.1. 闭包原理

def make_counter():
    count = 0  # 自由变量
    
    def counter():
        nonlocal count  # 声明使用外层变量
        count += 1
        return count
    
    return counter

c1 = make_counter()
c2 = make_counter()

print(c1())  # 1
print(c1())  # 2
print(c2())  # 1 - 独立的计数器

# 查看闭包变量
print(c1.__closure__[0].cell_contents)  # 2

1.2.3.2. 易错点:循环中的闭包

# ❌ 常见错误
funcs = []
for i in range(3):
    funcs.append(lambda: i)

print([f() for f in funcs])  # [2, 2, 2] - 都是 2!

# ✅ 解决方案1:默认参数捕获
funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)  # i=i 捕获当前值

print([f() for f in funcs])  # [0, 1, 2]

# ✅ 解决方案2:使用 functools.partial
from functools import partial

funcs = [partial(lambda x: x, i) for i in range(3)]
print([f() for f in funcs])  # [0, 1, 2]

1.2.4. 装饰器

1.2.4.1. 基本装饰器

import functools
import time

def timer(func):
    """测量函数执行时间的装饰器"""
    @functools.wraps(func)  # 保留原函数的元信息
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} took {end - start:.4f}s")
        return result
    return wrapper

@timer
def slow_function(n):
    """一个慢函数"""
    time.sleep(n)
    return n

slow_function(1)  # slow_function took 1.0012s

# 保留了原函数的信息
print(slow_function.__name__)  # slow_function
print(slow_function.__doc__)   # 一个慢函数

重要

始终使用 @functools.wraps 装饰 wrapper 函数,否则原函数的 __name____doc__ 等属性会丢失。

1.2.4.2. 带参数的装饰器

def retry(max_attempts=3, delay=1):
    """带重试功能的装饰器"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    print(f"Attempt {attempt + 1} failed: {e}")
                    if attempt < max_attempts - 1:
                        time.sleep(delay)
            raise last_exception
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5)
def unstable_api_call():
    import random
    if random.random() < 0.7:
        raise ConnectionError("Network error")
    return "Success"

1.2.4.3. 类装饰器

class CountCalls:
    """统计函数调用次数的类装饰器"""
    
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.count = 0
    
    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.func(*args, **kwargs)

@CountCalls
def greet(name):
    return f"Hello, {name}!"

print(greet("Alice"))  # Hello, Alice!
print(greet("Bob"))    # Hello, Bob!
print(greet.count)     # 2

1.2.4.4. 装饰器执行顺序

def decorator_a(func):
    print("Applying decorator A")
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("Before A")
        result = func(*args, **kwargs)
        print("After A")
        return result
    return wrapper

def decorator_b(func):
    print("Applying decorator B")
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("Before B")
        result = func(*args, **kwargs)
        print("After B")
        return result
    return wrapper

@decorator_a
@decorator_b
def hello():
    print("Hello!")

# 应用顺序:从下到上
# Applying decorator B
# Applying decorator A

hello()
# 调用顺序:从外到内
# Before A
# Before B
# Hello!
# After B
# After A

1.2.5. 函数式编程

1.2.5.1. map, filter, reduce

from functools import reduce

numbers = [1, 2, 3, 4, 5]

# map: 对每个元素应用函数
squared = list(map(lambda x: x**2, numbers))
# 更 Pythonic: [x**2 for x in numbers]

# filter: 过滤元素
evens = list(filter(lambda x: x % 2 == 0, numbers))
# 更 Pythonic: [x for x in numbers if x % 2 == 0]

# reduce: 累积计算
total = reduce(lambda acc, x: acc + x, numbers, 0)
# 更 Pythonic: sum(numbers)

# reduce 的实际应用场景
data = [{'name': 'Alice', 'score': 85},
        {'name': 'Bob', 'score': 92},
        {'name': 'Charlie', 'score': 78}]

# 找出最高分
highest = reduce(
    lambda best, x: x if x['score'] > best['score'] else best, 
    data
)

1.2.5.2. 偏函数

from functools import partial

def power(base, exponent):
    return base ** exponent

# 创建平方函数
square = partial(power, exponent=2)
# 创建立方函数
cube = partial(power, exponent=3)

print(square(5))  # 25
print(cube(3))    # 27

# 实际应用:配置 API 调用
import urllib.request

def fetch_url(url, timeout=10, headers=None):
    req = urllib.request.Request(url, headers=headers or {})
    return urllib.request.urlopen(req, timeout=timeout)

# 创建预配置的函数
api_fetch = partial(
    fetch_url, 
    timeout=30, 
    headers={'Authorization': 'Bearer token'}
)

1.2.5.3. operator 模块

from operator import attrgetter, itemgetter, methodcaller

# itemgetter: 获取字典/序列的项
students = [
    {'name': 'Alice', 'grade': 85, 'age': 20},
    {'name': 'Bob', 'grade': 92, 'age': 19},
    {'name': 'Charlie', 'grade': 78, 'age': 21},
]

# 按成绩排序
by_grade = sorted(students, key=itemgetter('grade'))

# 多字段排序
by_grade_age = sorted(students, key=itemgetter('grade', 'age'))

# attrgetter: 获取对象属性
class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade

students = [Student('Alice', 85), Student('Bob', 92)]
by_grade = sorted(students, key=attrgetter('grade'))

# methodcaller: 调用方法
names = ['alice', 'bob', 'charlie']
upper_names = list(map(methodcaller('upper'), names))
# ['ALICE', 'BOB', 'CHARLIE']

1.2.6. 最佳实践

函数设计原则
  1. 单一职责:一个函数只做一件事

  2. 合理的参数数量:超过 5 个考虑使用配置对象

  3. 避免可变默认参数:使用 None 作为默认值

  4. 返回一致的类型:不要返回 None 或值混用

装饰器最佳实践
  1. 始终使用 @functools.wraps

  2. 保持装饰器简单,复杂逻辑抽取到辅助函数

  3. 考虑支持同步和异步函数

  4. 提供清晰的文档和示例