Python3 基础教程

Python3 高级函数特性:*args、闭包、装饰器、生成器与 Lambda

🎉摘要:本文深入讲解Python函数的高级特性,包括*args与**kwargs可变参数、闭包捕获词法环境、生成器惰性计算节省内存,以及lambda、map、filter、sorted、reduce等高阶函数用法,帮助开发者掌握何时使用这些特性,写出更优雅高效的代码。

Python 函数的设计远比表面看起来灵活。从可变参数的解包,到闭包对词法环境的捕获,再到装饰器对函数的无侵入扩展(前面章节已经详细介绍),以及生成器对内存的极致节省 —— 这些特性构成了 Python 函数式编程的基石。

本章将逐一讲透这些概念,读完之后,你不仅知道语法怎么写,更知道什么时候该用、什么时候不该用。

可变参数:*args 与 **kwargs

Python 函数支持位置参数和关键字参数。但有时候,你事先不知道调用者会传多少个参数。*args 和 **kwargs 就是解决这个问题的万能工具。

*args:接收多余的位置参数

调用函数时,依靠传入顺序匹配形参的实参,就是位置参数。

def log_args(first, *args):
    print(f"first={first}, type={type(first)}")
    print(f"args={args}, type={type(args)}")

log_args(1, 2, 3, 4)
# first=1, type=<class 'int'>
# args=(2, 3, 4), type=<class 'tuple'>

*args 在函数定义中是一个元组,接收所有多余的位置参数。命名习惯叫 args,但你可以改。first 前面的参数按正常规则匹配,剩下的全部进 args。

**kwargs:接收多余的关键字参数

调用函数时,以“参数名=值”形式传入,就是关键字参数。关键字参数不受顺序约束,但必须放在位置参数之后。例如:

def log_kwargs(first, **kwargs):
    print(f"first={first}")
    print(f"kwargs={kwargs}, type={type(kwargs)}")

log_kwargs(1, x=10, y=20, name="test")
# first=1
# kwargs={'x': 10, 'y': 20, 'name': 'test'}, type=<class 'dict'>

**kwargs 是一个字典,接收所有未在形参列表中显式声明的关键字参数。命名习惯叫 kwargs。

组合使用:通用接口函数

def universal(*args, **kwargs):
    """接收任意参数,打印并原样转发"""
    print(f"Positionals: {args}")
    print(f"Keywords: {kwargs}")

universal(1, 2, 3, a=10, b=20)
# Positionals: (1, 2, 3)
# Keywords: {'a': 10, 'b': 20}

universal(*args, **kwargs) 是包装器和代理函数的基础模式。它自己不处理参数,只是透传。

解包:调用时展开

* 和 ** 在函数定义中是"打包",在调用时是"解包"(unpacking)。这是两个方向的操作,不要混淆。例如:

def calc(a, b, c, d=0):
    return a + b + c + d

# 用列表/元组解包
nums = [1, 2, 3]
print(calc(*nums))  # 6  (等价于 calc(1, 2, 3))

# 用字典解包
params = {"a": 1, "b": 2, "c": 3, "d": 10}
print(calc(**params))  # 16  (等价于 calc(a=1, b=2, c=3, d=10))

# 混合解包
print(calc(*[1, 2], **{"c": 3, "d": 5}))  # 11

解包在代理函数和装饰器里极其常用:

def proxy(*args, **kwargs):
    # 把接收到的参数原样转发给底层函数
    return real_func(*args, **kwargs)

仅限关键字参数(Keyword-Only)

Python 3 允许在参数列表中用 * 作为分隔符,其后的参数必须传关键字:

def create_user(name, *, age=0, active=True):
    """age 和 active 必须关键字传参"""
    return {"name": name, "age": age, "active": active}

print(create_user("Alice", age=30))       # 正确
print(create_user("Bob"))                 # 正确,用默认值
create_user("Charlie", 30)                # TypeError: 30 不能作为位置参数给 age

这个特性在 API 设计中有奇效:强制调用者显式写出参数名,代码可读性大幅提升。比如 sorted(data, key=..., reverse=...) 里的 key 和 reverse 就是关键字-only 的惯例。

仅限位置参数(Python 3.8+)

def greet(name, /, greeting="Hello"):
    """name 必须位置传参,greeting 可位置可关键字"""
    return f"{greeting}, {name}!"

greet("Alice")           # 正确
greet("Alice", "Hi")     # 正确
greet(name="Alice")      # TypeError: name 不能关键字传参

/ 分隔符前的参数必须位置传参。这个特性在底层库和 C 扩展的 Python 绑定中常见,日常业务代码用得不多,但看到 def func(a, /, b, *, c) 这种签名时要知道它的含义。

闭包:函数记住它的词法环境

闭包(Closure)是指一个函数记住了它被定义时的环境,即使那个环境里的变量已经不在当前作用域中。Python 的嵌套函数天然支持闭包。

最基本的闭包

def make_multiplier(n: float):
    """返回一个乘以 n 的函数"""
    def multiplier(x: float) -> float:
        return x * n
    return multiplier

times3 = make_multiplier(3)
times5 = make_multiplier(5)

print(times3(10))  # 30
print(times5(10))  # 50

# 查看闭包保存的变量
print(times3.__closure__[0].cell_contents)  # 3
print(times5.__closure__[0].cell_contents)  # 5

make_multiplier 返回的 multiplier 函数 "记住" 了 n 的值。即使 make_multiplier 已经执行完毕,n 仍然被闭包持有,不会随着栈帧销毁而消失。__closure__ 是一个元组,每个元素是 cell 对象,通过 cell_contents 可以读取捕获的值。

闭包做延迟计算

def make_power(base: int):
    """延迟计算幂次"""
    def power(exp: int) -> int:
        return base ** exp
    return power

square = make_power(2)
cube = make_power(10)

print(square(10))  # 1024  (2^10)
print(cube(2))     # 100   (10^2)

闭包的延迟绑定陷阱

这是初学者最容易踩的坑:

def make_functions():
    """错误:所有函数都返回 9"""
    funcs = []
    for i in range(3):
        def inner():
            return i  # i 是延迟绑定,不是值捕获!
        funcs.append(inner)
    return funcs

f0, f1, f2 = make_functions()
print(f0(), f1(), f2())  # 都输出 2 2 2

原因:i 在闭包里是引用绑定,不是值拷贝。循环结束后 i 的值是 2,三个函数都指向同一个 i。修复方法有两种:

  • 方法一:默认参数绑定值

def make_functions():
    funcs = []
    for i in range(3):
        def inner(x=i):  # x 在定义时绑定当前 i 的值
            return x
        funcs.append(inner)
    return funcs

f0, f1, f2 = make_functions()
print(f0(), f1(), f2())  # 都输出 0 1 2
  • 方法二:工厂函数显式捕获

def make_functions():
    funcs = []
    for i in range(3):
        funcs.append((lambda x: lambda: x)(i))
    return funcs

f0, f1, f2 = make_functions()
print(f0(), f1(), f2())  # 都输出 0 1 2

注意,更推荐方法一,它利用默认参数在函数定义时求值的特性,把 i 的值冻结在 x 里。

nonlocal:修改外层变量

默认情况下,闭包内层函数只能读取外层变量,不能修改。如果需要修改,用 nonlocal 声明:

def make_counter(start: int = 0):
    count = start
    def increment(step: int = 1):
        nonlocal count  # 声明修改外层变量,不是创建局部变量
        count += step
        return count
    def reset():
        nonlocal count
        count = start
        return count
    return increment, reset

inc, reset = make_counter(10)
print(inc())    # 11
print(inc(5))   # 16
print(reset())  # 10
print(inc())    # 11

nonlocal 是 Python3 新增的关键字。在 Python 2 中,只能用可变对象(如列表或字典)来间接修改外层状态。nonlocal 让闭包的状态管理更加直观。

生成器:惰性计算与内存节省

生成器(Generator)是 Python 处理大数据的利器。它不像列表那样一次性把所有数据加载到内存,而是逐个产出(yield)值,用的时候才生成。

Python 中,关键字 yield 用来创建生成器(generator),暂停函数、返回值,下次调用从暂停处继续执行,不用一次性把所有数据放进内存。例如:

def gen():
    print("A")
    yield 1
    print("B")
    yield 2
    print("C")

g = gen()   # 仅仅创建生成器,函数内部代码一行都不执行!

val = next(g)
# 执行:打印A → 碰到 yield 1 → 暂停,返回 1
print(val) # 1

val = next(g)
# 从暂停位置继续:打印B → 碰到 yield 2 → 暂停,返回2
print(val) # 2

val = next(g)
# 继续执行:打印C → 函数跑完,没有yield
# 抛出 StopIteration

注意,next(生成器对象) 驱动生成器执行代码,直到遇到下一个 yield,取出产出的值;没有更多 yield 就抛 StopIteration。

生成器函数 vs 列表

import sys

# 列表:一次性创建 100 万个整数
nums_list = [i for i in range(1000000)]
print(f"List size: {sys.getsizeof(nums_list)} bytes") # 8448728 bytes
# 通常约 800 万字节

# 生成器:几乎不占用内存
def gen_nums():
    for i in range(1000000):
        yield i

nums_gen = gen_nums()
print(f"Generator size: {sys.getsizeof(nums_gen)} bytes") # 192 bytes
# 通常约 190 字节

# 取值时才计算
print(next(nums_gen))  # 0
print(next(nums_gen))  # 1

生成器对象只保存当前状态(执行到哪个 yield、局部变量是什么),而不是全部数据。处理百万级数据时,生成器能避免内存溢出。

生成器表达式

列表推导 [x for x in range(10)] 的生成器版本是 (x for x in range(10)),用圆括号而不是方括号:

# 生成器表达式
squares = (x * x for x in range(1000000))
print(sum(squares))  # 333332833333500000

# 对比列表推导,内存占用差异巨大
squares_list = [x * x for x in range(1000000)]
print(sum(squares_list))  # 结果一样,但内存用了列表的全部空间

生成器表达式只能遍历一次,遍历完后自动耗尽。如果需要多次使用,要么转成列表,要么重新创建生成器。

yield from:委托子生成器

yield from 可以把一个生成器的产出委托给另一个生成器,简化嵌套:

def sub_generator(start, end):
    for i in range(start, end):
        yield i

def main_generator():
    yield "A"
    yield from sub_generator(1, 4)  # 等价于 yield 1; yield 2; yield 3
    yield "B"
    yield from sub_generator(10, 13)
    yield "C"

for item in main_generator():
    print(item, end=" ")
# 输出: A 1 2 3 B 10 11 12 C

yield from 还能让子生成器把 return 的值传回父生成器:

def counter():
    total = 0
    for i in range(1, 4):
        total += i
        yield i
    return total  # 最终返回值

def main():
    result = yield from counter()
    print(f"Total: {result}")
    yield "done"

for item in main():
    print(item)
# 1
# 2
# 3
# Total: 6
# done

生成器的高级接口:send、throw、close

生成器不是单向的产出通道,调用者可以通过 .send() 向生成器发送数据:

def running_average():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average  # 产出当前平均值,等待接收新值
        total += term
        count += 1
        average = total / count

avg = running_average()
next(avg)  # 或 avg.send(None),启动生成器
print(avg.send(10))   # 10.0
print(avg.send(20))   # 15.0
print(avg.send(30))   # 20.0
avg.close()            # 终止生成器

说明:

  • .send(value) 把值传给 yield 表达式左侧的变量,同时推进到下一个 yield;

  • 第一次启动必须用 next() 或 send(None),因为此时生成器还没执行到 yield;

  • .close() 发送 GeneratorExit 异常,让生成器执行清理逻辑;

  • .throw(exc) 向生成器抛出异常,用于错误注入和测试。

生成器处理大文件

这是生成器最常见的实战场景:

def read_large_file(filepath: str, chunk_size: int = 8192):
    """逐块读取大文件,避免一次性载入内存"""
    with open(filepath, 'rb') as f:
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            yield chunk

# 逐行处理(更常用)
def read_lines(filepath: str):
    """逐行读取,支持文件对象被垃圾回收前的清理"""
    with open(filepath, 'r', encoding='utf-8') as f:
        for line in f:
            yield line.strip()

# 使用:每次读取64字节
for chunk in read_large_file("huge.log", chunk_size=64):
    print(chunk)

# 使用:处理一个 10GB 的日志文件,内存只占一行
for line in read_lines("huge.log"):
    if "POST" in line:
        print(line)

with open(...) 放在生成器函数里有一个精妙之处:文件对象的生命周期与生成器绑定。当生成器被垃圾回收或 close() 时,with 语句块确保文件被关闭。这比返回列表的函数更优雅——后者必须在所有数据处理完毕后才关闭文件。

Lambda 与高阶函数

lambda 用来创建匿名函数(没有名字的临时小函数)。语法如下:

lambda 参数列表: 表达式

注意:

  • lambda 没有 return,表达式的结果自动作为返回值。

  • 只能写一条表达式,不能写 if 块、for循环、while、try 等多行代码。

  • 适合简单逻辑、临时回调、配合 sorted/map/filter。

Lambda:匿名函数

下面是 Lambda 简单示例:

# 一个参数,实现平方
square = lambda x: x * x
print(square(5))  # 25

# 多参数,实现加法
add = lambda x, y: x + y
print(add(2, 3))  # 5

# lambda 不能写语句,只能写表达式
# lambda x: print(x)  # 语法上可行,但不太推荐,可读性差

注意,lambda 适合写简单的、用完即丢的函数。如果逻辑超过一行,应该写成普通函数。PEP 8 建议:"如果 lambda 体超过一行,用 def 定义。"

map:对每个元素做映射

map(函数, 可迭代对象) 函数用于把可迭代对象里的每一个元素,依次传入函数执行,收集返回结果,最终返回一个 map 对象(迭代器)。

例如:使用 lambda 作为函数。

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

# 用 lambda 函数实现对列表每个组求平方
squares = list(map(lambda x: x * x, nums))
print(squares)  # [1, 4, 9, 16, 25]

# 用内置函数
# str(object):把任意对象转换成字符串形式。
str_nums = list(map(str, nums))
print(str_nums)  # ['1', '2', '3', '4', '5']

# 多序列映射,列表对应位置的值相加
a = [1, 2, 3]
b = [10, 20, 30]
sums = list(map(lambda x, y: x + y, a, b))
print(sums)  # [11, 22, 33]

map 返回迭代器,惰性求值。list(map(...)) 把它转成列表。如果只需要遍历一次,不必 list() 化,直接用 for 循环更省内存。

filter:按条件筛选

filter(判断函数, 可迭代对象)  函数用于遍历序列每一个元素,交给判断函数;函数返回 True 就保留元素,False 丢弃;最终返回一个迭代器。

例如:使用 lambda 函数作为判断函数

nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# 过滤出偶数
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens)  # [2, 4, 6, 8, 10]

# 用 None 过滤 falsy 值
values = [0, 1, '', 'hello', None, [], [1, 2]]
truthy = list(filter(None, values))
print(truthy)  # [1, 'hello', [1, 2]]

sorted 与 key:自定义排序

sorted(iterable, key=None, reverse=False) 函数用于排序,使用迭代对象中每个对象中 key 指定元素坐做比较,不改变原始元素,只改变排列顺序。

参数说明:

  • iterable:列表、元组、字符串、生成器等可迭代对象

  • key:函数,用来指定用什么依据排序。对序列里每一个元素先执行 key(元素),拿到一个 “排序键”,程序只根据这个键大小排序;原始元素本身不变,只是按照键的顺序重新排列元素。

  • reverse=False:升序;reverse=True:降序

示例:使用 lambda 函数作为 key

data = [
    ("Alice", 30, "Engineer"),
    ("Bob", 25, "Designer"),
    ("Charlie", 35, "Manager"),
    ("David", 25, "Intern"),
]

# 按年龄排序
by_age = sorted(data, key=lambda x: x[1])
print(by_age)
# [('Bob', 25, 'Designer'), ('David', 25, 'Intern'), ...]

# 按年龄升序,年龄相同按姓名升序
by_age_name = sorted(data, key=lambda x: (x[1], x[0]))
# 或者:by_age_desc = sorted(data, key=lambda x: x[1], reverse=True)
print(by_age_name)

# 按字符串长度排序
words = ["apple", "pie", "banana", "kiwi"]
by_len = sorted(words, key=len)
print(by_len)  # ['pie', 'kiwi', 'apple', 'banana']

注意,sorted 返回新列表,不修改原列表。list.sort() 是原地排序,返回 None。key 函数只被调用一次 per 元素,结果缓存后用于比较,这是 sorted 高效的原因。

functools.reduce:累积计算

reduce() 用来对序列持续累积运算。不断取出序列元素,和上一轮计算结果传入函数,迭代压缩成单个值。语法如下:

reduce(function, iterable[, initializer])

参数说明:

  • function:必须接收 2 个参数,如 func(累计结果, 当前元素)

  • iterable:可迭代对象

  • initializer(可选):初始累积值

例如:使用 lambda 作为 function 参数

from functools import reduce

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

# 累乘(无初始值,第一轮取前两个元素)
# a=1 x b=2 = 2
# a=2 x b=3 = 6
# a=6 x b=4 = 24
# a=24 x b=5 = 120
product = reduce(lambda a, b: a * b, nums)
print(product)  # 120

# 累加(其实用 sum 更好,这里演示;设置初始值0)
# a=0 + b=1 = 1
# a=1 + b=2 = 3
# a=3 + b=3 = 6
# a=6 + b=4 = 10
# a=10 + b=5 = 15
total = reduce(lambda a, b: a + b, nums, 0)
print(total)  # 15

# 找最大值
# a保存当前最大值,逐个和b比较,保留较大值
maximum = reduce(lambda a, b: a if a > b else b, nums)
print(maximum)  # 5

reduce 在 Python 3 被移到了 functools。 Guido 不太喜欢它,因为大多数用例可以用更清晰的 for 循环或内置函数替代。但如果你的逻辑确实是"累积"(cumulation),reduce 比手写循环更简洁。

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号