Python装饰器全解析:从作用域、闭包到高级应用

落日邮差
落日邮差
发布时间:2026-10-13| 3人浏览过
原创

本文深入解析Python装饰器原理,从全局与局部作用域、高级函数特性入手,详细阐述闭包函数的定义与条件。通过无参、有参及多层装饰器的实例,展示如何在不修改源码和调用方式的前提下扩展函数功能,并解析语法糖@的使用及执行顺序。

一.作用域

在Python中,变量的作用域主要分为两种:全局作用域和局部作用域。

全局作用域指的是在文件层级定义的变量或函数名;而局部作用域则是指定义在函数内部的变量或名称。

理解作用域时,需要掌握以下两个核心要点:

a. 在全局作用域中,无法直接访问局部作用域内定义的变量。

b. 在局部作用域中,可以读取全局作用域定义的变量,但默认情况下不能直接修改全局变量(尽管存在特定方法可以修改)。

接下来通过具体实例进行说明:

x = 1
def funx():
  x = 10
  print(x) # 打印出10
funx()
print(x) # 打印出1

如果局部作用域中未定义变量x,函数内部会遵循从内到外的顺序查找x;若最终未找到,程序将抛出错误。

x = 1
def funx():
  print(x) 
funx()
print(x) # 打印出1
x = 1
def funx():
  def func1():
    print(x) 
  func1()
funx()
print(x) # 打印出1

因此,关于作用域的规则可以总结为两点:

全局变量可以在文件的任何位置被引用,但修改操作必须在全局作用域中进行;如果在局部作用域中未找到所需变量,解释器会向外层作用域查找,若仍找不到则报错。

二.高级函数

在Python中,函数名本质上是指向内存地址的引用。既然函数名是地址,我们就可以利用这一特性来实现更灵活的功能。

def delete(ps):
  import os
  filename = ps[-1]
  delelemetns = ps[1]
  with open(filename, encoding='utf-8') as f_read,\
    open('tmp.txt', 'w', encoding='utf-8') as f_write:
    for line in iter(f_read.readline, ''):
      if line != '\n': # 处理非空行
        if delelemetns in line:
          line = line.replace(delelemetns,'')
        f_write.write(line)
  os.remove(filename)
  os.rename('tmp.txt',filename)
def add(ps):
  filename = ps[-1]
  addelemetns = ps[1]
  with open(filename, 'a', encoding='utf-8') as fp:
    fp.write("\n", addelemetns)
def modify(ps):
  import os
  filename = ps[-1]
  modify_elemetns = ps[1]
  with open(filename, encoding='utf-8') as f_read, \
      open('tmp.txt', 'w', encoding='utf-8') as f_write:
    for line in iter(f_read.readline, ''):
      if line != '\n': # 处理非空行
        if modify_elemetns in line:
          line = line.replace(modify_elemetns, '')
        f_write.write(line)
  os.remove(filename)
  os.rename('tmp.txt', filename)
def search(cmd):
  filename = cmd[-1]
  pattern = cmd[1]
  with open(filename, 'r', encoding="utf-8") as f:
    for line in f:
      if pattern in line:
        print(line, end="")
    else:
      print("没有找到")
dic_func ={'delete': delete, 'add': add, 'modify': modify, 'search': search}
while True:
  inp = input("请输入您要进行的操作:").strip()
  if not inp:
    continue
  cmd_1 = inp.split()
  cmd = cmd_1[0]
  if cmd in dic_func:
    dic_func[cmd](cmd_1)
  else:
    print("Error")

例如,将函数名作为字典的值,可以实现对文本数据的增、删、查、改操作。

b. 函数名可以作为返回值返回。

def outer():
  def inner():
    pass
  return inner
s = outer()
print(s)
######输出结果为#######
<function outer.<locals>.inner at 0x000000D22D8AB8C8>

c. 函数名可以作为参数传递给其他函数。

def index():
  print("index func")
def outer(index):
  s = index
  s()
outer(index)
######输出结果#########
index func

只要满足上述任意一个条件,该函数即可被称为高级函数。

三.闭包函数

闭包函数的定义必须同时满足两个条件:1. 函数内部定义了另一个函数;2. 内部函数引用了外部作用域(非全局作用域)的变量。

下面通过几个实例来具体说明闭包函数的概念:

实例一:以下代码仅在函数内部定义了一个函数,但这并不构成闭包函数。

def outer():
  def inner():
    print("inner func excuted")
  inner() # 调用执行inner()函数
  print("outer func excuted")
outer() # 调用执行outer函数
####输出结果为##########
inner func excuted
outer func excuted

实例二:以下代码在函数内部定义了一个函数,并引用了外部变量x,这是否是闭包函数?答案是否定的。

x = 1
def outer():
  def inner():
    print("x=%s" %x) # 引用了一个非inner函数内部的变量
    print("inner func excuted")
  inner() # 执行inner函数
  print("outer func excuted")
outer()
#####输出结果########
x=1
inner func excuted
outer func excuted

回顾闭包函数的定义,检查是否满足两条标准?细心的读者会发现,这里不满足第二条。因为变量x属于全局变量,而非外部作用域的变量。请查看下面的例子:

def outer():
  x = 1
  def inner():
    print("x=%s" %x)
    print("inner func excuted")
  inner()
  print("outer func excuted")
outer()
#####输出结果#########
x=1
inner func excuted
outer func excuted

显然,上面的实例满足了闭包函数的所有条件。现在你应该清楚,一个闭包函数必须同时满足上述两个条件,缺一不可。不过,通常情况下,我们会给闭包函数返回一个值。至于为什么,将在后续内容中揭示该返回值的用途。

def outer():
  x = 1
  def inner():
    print("x=%s" %x)
    print("inner func excuted")
  print("outer func excuted")
  return inner # 返回内部函数名
outer()

现在我们来抽象地定义闭包函数。它是函数与其相关的引用环境组合而成的实体。在实现深约束时,需要创建一个能显式表示引用环境的对象,并将其与相关的子程序捆绑在一起,这个捆绑体即成为闭包。从上面的实例中可以看出,闭包函数必须包含自身的函数定义以及一个外部变量,才能真正称为闭包函数。如果没有外部变量与其绑定,则该函数不能被称为闭包函数。

那么,如何判断一个闭包函数引用了多少个外部变量呢?请看下面的代码。

def outer():
  x = 1
  y = 2
  def inner():
    print("x= %s" %x)
    print("y= %s" %y)
  print(inner.__closure__)
  return inner
outer()
######输出结果#######
(<cell at 0x000000DF9EA965B8: int object at 0x000000006FC2B440>, <cell at 0x000000DF9EA965E8: int object at 0x000000006FC2B460>)

结果表明,在inner函数内部引用了两个外部局部变量。如果引用的是非局部变量(即全局变量),这里输出的结果为None。

闭包函数的特点包括:1. 自带作用域;2. 支持延迟计算。

那么闭包函数有什么实际作用呢?我们知道,闭包函数在定义时一定会绑定一个外部环境,这个整体才构成闭包函数。我们可以利用这种绑定特性来完成某些特殊功能。

实例三:根据传入的URL下载页面源码。

from urllib.request import urlopen
def index(url)
  def get()
    return urlopen(url).read()
  return get
python = index("http://www.python.org") # 返回的是get函数的地址
print(python()) # 执行get函数《并且将返回的结果打印出来
baidu = index("http://www.baidu.com")
print(baidu())

有人可能会质疑,这个例子不满足闭包函数的条件,因为没有引用非全局的外部变量。其实并非如此。之前我们提到,函数内部的变量都属于该函数。在index(url)中,url参数也属于函数内部变量,只是我们省略了显式绑定的步骤。因此,上述函数同样属于闭包函数。

四.装饰器

基于以上基础知识,理解装饰器就变得容易了。

装饰器的定义:外部函数接收被装饰函数的名作为参数,内部函数返回装饰后的函数名。

装饰器的特点:1. 不修改被装饰函数的调用方式;2. 不修改被装饰函数的源代码。

a. 无参装饰器

以下实例中,我们需要计算代码执行的时间。

import time, random
def index():
  time.sleep(random.randrange(1, 5))
  print("welcome to index page")

根据装饰器的特点,我们不能直接修改index()函数,且调用方式必须保持不变。此时,我们可以使用装饰器来实现上述功能。

import time, random
def outer(func): # 将index的地址传递给func
  def inner():
    start_time = time.time()
    func()  # fun = index 即func保存了外部index函数的地址
    end_time = time.time()
    print("运行时间为%s"%(end_time - start_time))
  return inner # 返回inner的地址
def index():
  time.sleep(random.randrange(1, 5))
  print("welcome to index page")
index = outer(index) # 这里返回的是inner的地址,并重新赋值给index
index()

然而,有些情况下被装饰的函数需要传递参数,而有些函数不需要参数。如何处理这种变参函数的情况?下面来看看有参装饰器的使用示例。

b. 有参装饰器

def outer(func): # 将index的地址传递给func
  def inner(*args, **kwargs):
    start_time = time.time()
    func(*args, **kwargs)  # fun = index 即func保存了外部index函数的地址
    end_time = time.time()
    print("运行时间为%s"%(end_time - start_time))
  return inner # 返回inner的地址

下面介绍一些其他情况的实例。

如果被装饰的函数有返回值,该如何处理?

def timmer(func):
  def wrapper(*args,**kwargs):
    start_time = time.time()
    res=func(*args,**kwargs) #res来接收home函数的返回值
    stop_time=time.time()
    print('run time is %s' %(stop_time-start_time))
    return res 
  return wrapper
def home(name):
  time.sleep(random.randrange(1,3))
  print('welecome to %s HOME page' %name)
  return 123123123123123123123123123123123123123123

这里补充一点,如果要执行被装饰后的函数,调用方式应如下所示:

home = timmer(home) # 等式右边返回的是wrapper函数的内存地址,再将其赋值给home。此时的home不再是原来的函数,而是被装饰后的新函数。

对于像home = timmer(home)这样的写法,Python提供了一个便捷的方式——语法糖@。

以后,我们只需在被装饰函数的定义前加上@timmer,其效果与home = timmer(home)完全相同。

如果一个函数被多个装饰器装饰,其执行顺序是怎样的?

import time
import random
def timmer(func):
  def wrapper():
    start_time = time.time()
    func()
    stop_time=time.time()
    print('run time is %s' %(stop_time-start_time))
  return wrapper
def auth(func):
  def deco():
    name=input('name: ')
    password=input('password: ')
    if name == 'egon' and password == '123':
      print('login successful')
      func() #wrapper()
    else:
      print('login err')
  return deco
@auth  # index = auth(timmer(index))         
@timmer # index = timmer(index)
def index():
  time.sleep(3)
  print('welecome to index page')
index()

实验结果表明,多个装饰器装饰一个函数时,执行顺序是从下往上。

总结

以上所述是Python装饰器的详细实例解析,希望对大家有所帮助。如果大家有任何疑问,请留言,小编会及时回复。在此也非常感谢大家对本站网站的支持!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。