07. Python函数进阶:从作用域到闭包与装饰器

最近在Python的学习道路上遇到了几个“拦路虎”:作用域(Scope)闭包(Closure)装饰器(Decorator)

刚开始看的时候,什么L、E、G、B作用域链,还有内外函数嵌套,简直让人头大。但经过一番死磕和调试代码后,终于搞懂了它们的运行机制。

为了防止自己以后忘记,也为了帮同样在挣扎的小伙伴理清思路,我决定把这部分知识总结下来。老规矩,文章开头先带大家回顾一下上一节学的函数基础,然后正式进入今天的进阶内容。


📚 第一部分:温故知新(上节函数基础回顾)

在上一阶段的学习中,我们掌握了函数的基本语法:

  • 定义与调用:使用 def 关键字定义,函数名是变量,指向函数对象。
  • 参数传递:位置参数、默认参数、以及 *args**kwargs 处理不定长参数。
  • 返回值:使用 return 返回结果,不写默认返回 None

掌握了这些,我们就能写出结构清晰的代码了。但如果我们想在函数内部修改全局变量,或者想给函数“动态”增加功能而不改源码,就需要今天学的进阶知识了。


🚀 第二部分:函数进阶核心(本节重点)

今天的内容主要包含三个核心部分:变量作用域闭包装饰器。这三者是层层递进的关系。

1. 变量的作用域 (LEGB规则)

在Python中,变量不是在任何地方都能被访问的。Python遵循 LEGB 规则来查找变量:

  • L (Local):局部作用域,函数内部定义的变量。
  • E (Enclosing):闭包函数外的函数中(即外函数的局部作用域)。
  • G (Global):全局作用域,模块级别定义的变量。
  • B (Built-in):内建作用域,Python自带的内置函数(如 print, len)。

重点笔记:

  • 分支和循环没有作用域:在 iffor 里定义的变量,在外面是可以访问的。
  • 函数才有作用域:函数内部定义的变量(局部变量),函数执行完就会被释放,外部无法访问。

如何在函数内修改全局变量?
这时候需要两个“神器”关键字:

  • global:在函数内部声明变量为全局变量。
  • nonlocal:在嵌套函数中,修改外层函数(非全局)的变量。
# global 示例
num = 10  # 全局变量
def test():
    global num  # 声明我要修改全局的num
    num = 20
test()
print(num)  # 输出 20

# nonlocal 示例 (用于闭包)
def outer():
    x = 10
    def inner():
        nonlocal x  # 声明修改的是外层函数的x
        x += 1
        print(x)
    return inner

fn = outer()
fn()  # 输出 11

2. 闭包 (Closure)

闭包听起来很高级,其实很简单。如果在一个函数内部定义了另一个函数,并且外部函数返回了内部函数,这就构成了闭包。

构成闭包的3个条件:

  1. 函数嵌套函数。
  2. 内部函数引用了外部函数的变量。
  3. 外部函数返回内部函数。

闭包的作用: 闭包可以保存当前的运行环境(状态)。即使外部函数执行完了,内部函数依然可以使用外部函数的变量。

def outer(b):
    a = 10
    def inner():  # 内函数
        print(a + b)  # 引用了外部变量
    return inner  # 返回内函数

# 这就是一个闭包
closure_func = outer(5)
closure_func()  # 输出 15

3. 装饰器 (Decorator)(了解)

装饰器是Python中最经典的应用,它基于闭包实现。核心目的:在不修改原函数代码的前提下,给原函数增加新功能。

应用场景: 比如你想统计每个函数的运行时间,或者检查用户登录权限,就可以用装饰器。

原理: 把原函数作为参数传入装饰器,然后在装饰器里包装一下(先干点别的,再执行原函数,最后再干点别的),最后返回这个包装后的函数。

代码演示(标准写法):

# 定义一个装饰器
def my_decorator(fn):
    def eat(*args, **kwargs):  # 通用写法,适配任何参数
        print("先吃个饭")
        result = fn(*args, **kwargs)  # 执行原函数
        print("再睡个觉")
        return result
    return eat

# 使用装饰器 (语法糖)
@my_decorator
def hit():
    print("打个豆豆")

# 调用
hit()

代码运行结果如图:

注意: @my_decorator 等价于 hit = my_decorator(hit)


💻 第三部分:课后作业与实战

我整理了几个典型的练习题,用来巩固上面的知识:

1. 实现一个支持两种调用方式的求和函数
要求:my_sum(2, 3)my_sum(2)(3) 都能输出 5。
这道题考察了对参数数量的判断和闭包的使用。

def my_sum(*args):
    """
    智能求和函数:支持两种调用方式
    方式1: my_sum(a, b) -> 直接传入两个参数
    方式2: my_sum(a)(b) -> 柯里化调用,先传入一个参数,返回一个函数,再传入第二个参数
    
    参数:
        *args: 可变参数,用于接收任意数量的参数(实际逻辑只处理1个或2个)
    
    返回:
        int/float: 两个数字的和
    
    异常:
        TypeError: 当参数类型不是数字,或者参数数量错误时抛出
    """
    
    # --- 情况一:参数数量为 1 (需要实现 my_sum(a)(b) 的逻辑) ---
    if len(args) == 1:
        first_num = args[0]
        
        # 类型校验:确保第一个参数是数字(int或float)
        if not isinstance(first_num, (int, float)):
            raise TypeError("参数必须是数字类型")
        
        # 定义内部函数 (Inner Function)
        # 这里利用了闭包的特性:inner函数可以记住外部函数的变量 first_num
        def inner(second_num):
            # 类型校验:确保传入的第二个参数也是数字
            if not isinstance(second_num, (int, float)):
                raise TypeError("参数必须是数字类型")
            # 返回两个数的和
            return first_num + second_num
        
        # 关键点:这里不执行 inner(),而是返回 inner 函数对象
        # 当用户调用 my_sum(2)(3) 时,实际上是先拿到了这个 inner 函数,然后立刻传入 3
        return inner
    
    # --- 情况二:参数数量为 2 (需要实现 my_sum(a, b) 的逻辑) ---
    elif len(args) == 2:
        # 使用 all() 函数和生成器表达式,一次性检查所有参数是否为数字
        if not all(isinstance(x, (int, float)) for x in args):
            raise TypeError("所有参数必须是数字类型")
        
        # 直接返回两个参数的和
        return args[0] + args[1]
    
    # --- 情况三:参数数量错误 ---
    else:
        # 抛出异常,提示用户正确的参数数量
        # 利用 f-string 格式化输出,告诉用户实际收到了几个参数
        raise TypeError(f"my_sum 需要 1 个或 2 个参数,但收到了 {len(args)} 个")


# --- 测试代码 ---

# 测试场景 1:标准的两个参数调用
n = my_sum(2, 3)
print(f"my_sum(2, 3) = {n}") # 输出: 5

# 测试场景 2:柯里化调用 (闭包的应用)
m = my_sum(5)(10)
print(f"my_sum(5)(10) = {m}") # 输出: 15

# 测试场景 3:错误的参数类型 (取消注释可看报错效果)
# p = my_sum("a", 3) # 输出: TypeError: 所有参数必须是数字类型

代码运行结果如图:

取消场景三的注释,即报错,显示所有参数必须是数字类型。

2. 随机生成十六进制颜色
考察 random 模块和字符串拼接。

import random

def random_color():
    """
    生成随机的十六进制颜色代码

    Returns:
        str: 以#开头的 7 位十六进制颜色字符串,例如 #FFFFFF、#0033CC
    """
    colors = '0123456789ABCDEF'
    color_code = '#' + ''.join([random.choice(colors) for _ in range(6)])
    return color_code
a =  random_color()
print(a)

代码运行结果如图:

注意:每次运行结果都不一样。

3. 给下面的set_age函数添加一个装饰器,

    要求:在传入age之前判断age不能小于0,如果小于0则传入0,并打印"提示:年龄不能小于0"

考察装饰器用法。

def check_age(func):
    def wrapper(age):
        # 核心判断:如果年龄小于0
        if age < 0:
            print("提示:年龄不能小于0")
            # 注意:这里没有调用 func(age),所以原函数的 print 不会执行
            # 函数直接在这里结束
        else:
            # 只有年龄合法时,才执行原函数
            func(age)
            
    return wrapper

@check_age
def set_age(age):
    print(f'大家好!我今年{age}岁')

# 测试
set_age(20)   # 正常情况
set_age(-5)   # 异常情况

代码运行结果如图:


🎓 总结

写完这篇博客,感觉自己对Python的理解又深了一层。

  1. 作用域 是Python查找变量的规则,记住 LEGBglobal/nonlocal 的用法,就能解决大部分变量报错问题。
  2. 闭包 是函数式编程的思想,它让函数有了“记忆”,能保存状态。
  3. 装饰器 是Python的“语法糖”,它利用了闭包的特性,让我们可以优雅地在不修改源码的情况下增强函数功能,这在以后会非常常见。

虽然这些概念刚开始有点绕,但只要多写几遍代码,调试一下,就能豁然开朗了!希望这篇笔记能帮到正在学习Python进阶的你。

如果觉得有用,别忘了点赞收藏哦!有什么问题欢迎在评论区交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值