函数对象与装饰器

【一】名称空间与闭包

【1】名称空间

  • 名称空间即存放名字与对象映射/绑定关系的地方。
    • 对于x=3,Python会申请内存空间存放对象3,然后将名字x与3的绑定关系存放于名称空间中,del x表示清除该绑定关系。
  • 在程序执行期间最多会存在三种名称空间

(1)内建名称空间

  • 伴随python解释器的启动/关闭而产生/回收
    • 因而是第一个被加载的名称空间,用来存放一些内置的名字,比如内建函数名
>>> max
<built-in function max> #built-in内建

(2)全局名称空间

  • 伴随python文件的开始执行/执行完毕而产生/回收,是第二个被加载的名称空间,文件执行过程中产生的名字都会存放于该名称空间中,如下名字
import sys #模块名sys

x=1 #变量名x

if x == 1:
    y=2 #变量名y

def foo(x): #函数名foo
    y=1
    def bar():
        pass

Class Bar: #类名Bar
	pass

(3)局部名称空间

  • 伴随函数的调用/结束而临时产生/回收,函数的形参、函数内定义的名字都会被存放于该名称空间中
def foo(x):
    y=3 #调用函数时,才会执行函数代码,名字x和y都存放于该函数的局部名称空间中

名称空间的加载顺序是:

​ 内置名称空间->全局名称空间->局部名称空间,

而查找一个名字,必须从三个名称空间之一找到,查找顺序为:

​ 局部名称空间->全局名称空间->内置名称空间。

【2】作用域

(1)全局作用域与局部作用域

按照名字作用范围的不同可以将三个名称空间划分为两个区域:

  1. 全局作用域:位于全局名称空间、内建名称空间中的名字属于全局范围,该范围内的名字全局存活(除非被删除,否则在整个文件执行过程中存活)、全局有效(在任意位置都可以使用);
  2. 局部作用域:位于局部名称空间中的名字属于局部范围。该范围内的名字临时存活(即在函数调用时临时生成,函数调用结束后就释放)、局部有效(只能在函数内使用)。

(2)作用域与名字查找的优先级

  • 在局部作用域查找名字时,起始位置是局部作用域,所以先查找局部名称空间,没有找到
    • 再去全局作用域查找:先查找全局名称空间,没有找到
      • 再查找内置名称空间,最后都没有找到就会抛出异常
x=100 #全局作用域的名字x
def foo():
    x=300 #局部作用域的名字x
    print(x) #在局部找x
foo()#结果为300
  • 在全局作用域查找名字时,起始位置便是全局作用域
    • 所以先查找全局名称空间,没有找到
      • 再查找内置名称空间,最后都没有找到就会抛出异常
x=100
def foo():
    x=300 #在函数调用时产生局部作用域的名字x
foo()
print(x) #在全局找x,结果为100

提示:

​ 可以调用内建函数locals()和globals()来分别查看局部作用域和全局作用域的名字,查看的结果都是字典格式。

​ 在全局作用域查看到的locals()的结果等于globals()

  • Python支持函数的嵌套定义,在内嵌的函数内查找名字时,会优先查找自己局部作用域的名字
    • 然后由内而外一层层查找外部嵌套函数定义的作用域
      • 没有找到,则查找全局作用域
x=1
def outer():
    x=2
    def inner(): # 函数名inner属于outer这一层作用域的名字
        x=3
        print('inner x:%s' %x)

    inner()
    print('outer x:%s' %x)

outer() 
#结果为
inner x:3
outer x:2
  • 在函数内,无论嵌套多少层,都可以查看到全局作用域的名字
    • 若要在函数内修改全局名称空间中名字的值
      • 当值为不可变类型时,则需要用到global关键字
x=1
def foo():
    global x #声明x为全局名称空间的名字
    x=2
foo()
print(x) #结果为2
  • 当实参的值为可变类型时
    • 函数体内对该值的修改将直接反应到原值,
num_list=[1,2,3]
def foo(nums):
    nums.append(5)

foo(num_list)
print(num_list)
#结果为
[1, 2, 3, 5]
  • 对于嵌套多层的函数
    • 使用nonlocal关键字可以将名字声明为来自外部嵌套函数定义的作用域(非全局)
def  f1():
    x=2
    def f2():
        nonlocal x
        x=3
    f2() #调用f2(),修改f1作用域中名字x的值
    print(x) #在f1作用域查看x

f1()

#结果
3
  • nonlocal x 会从当前函数的外层函数开始一层层去查找名字x
    • 若是一直到最外层函数都找不到,则会抛出异常。

【二】函数对象与闭包

【1】函数对象

  • 函数对象指的是函数可以被当做 数据 来处理,具体可以分为四个方面的使用

(1)函数可以被引用

>>> def add(x,y):
...     return x+y
... 
>>> func=add
>>> func(1,2)
3

(2)函数可以作为容器类型的元素

>>> dic={'add':add,'max':max}
>>> dic
{'add': <function add at 0x100661e18>, 'max': <built-in function max>}
>>> dic['add'](1,2)
3

(3)函数可以作为参数传入另外一个参数

>>> def foo(x,y,func):
...     return func(x,y)
...
>>> foo(1,2,add)
3

(4)函数的返回值可以是一个函数

>>> def bar():
...     return add
...
>>> func=bar()
>>> func(1,2)
3

【2】闭包函数

(1)闭与包

  • 基于函数对象的概念,可以将函数返回到任意位置去调用
    • 但作用域的关系是在定义完函数时就已经被确定了的,与函数的调用位置无关。
x=1

def f1():
    def f2():
        print(x)

    return f2

def f3():
    x=3
    f2=f1() #调用f1()返回函数f2
    f2() #需要按照函数定义时的作用关系去执行,与调用位置无关

f3() #结果为1
  • 也就是说函数被当做数据处理时,始终以自带的作用域为准。
    • 若内嵌函数包含对外部函数作用域(而非全局作用域)中变量的引用
      • 那么该’内嵌函数’就是闭包函数,简称闭包(Closures)
x=1
def outer():
    x=2
    def inner():
        print(x)
    return inner

func=outer()
func() # 结果为2
  • 也就是说函数被当做数据处理时,始终以自带的作用域为准。
    • 若内嵌函数包含对外部函数作用域(而非全局作用域)中变量的引用,那么该’内嵌函数’就是闭包函数,简称闭包(Closures)
x=1
def outer():
    x=2
    def inner():
        print(x)
    return inner

func=outer()
func() # 结果为2
  • 可以通过函数的closure属性,查看到闭包函数所包裹的外部变量
>>> func.__closure__
(<cell at 0x10212af78: int object at 0x10028cca0>,)
>>> func.__closure__[0].cell_contents
2
  • “闭”代表函数是内部的,“包”代表函数外’包裹’着对外层作用域的引用。
    • 因而无论在何处调用闭包函数,使用的仍然是包裹在其外层的变量。

(2)闭包函数的用途

  • 目前为止,我们得到了两种为函数体传值的方式
    • 一种是直接将值以参数的形式传入
    • 另外一种就是将值包给函数
import requests

# 方式一:
def get(url):
    return requests.get(url).text

# 方式二:
def page(url):
    def get():
        return requests.get(url).text
    return get

提示:requests模块是用来模拟浏览器向网站发送请求并将页面内容下载到本地,需要事先安装:pip3 install requests

  • 对比两种方式
    • 方式一在下载同一页面时需要重复传入url
    • 方式二只需要传一次值,就会得到一个包含指定url的闭包函数,以后调用该闭包函数无需再传url
# 方式一下载同一页面
get('https://www.python.org')
get('https://www.python.org')
get('https://www.python.org')
……

# 方式二下载同一页面
python=page('https://www.python.org')
python()
python()
python()
……
  • 闭包函数的这种特性有时又称为惰性计算。
    • 使用将值包给函数的方式,在接下来的装饰器中也将大有用处

【三】装饰器

【1】装饰器介绍

(1)装饰器的用途

  • 软件的设计应该遵循开放封闭原则,即对扩展是开放的,而对修改是封闭的。

    • 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
    • 对修改封闭,意味着对象一旦设计完成,就可以独立完成其工作,而不要对其进行修改。
  • 软件包含的所有功能的源代码以及调用方式,都应该避免修改,否则一旦改错,则极有可能产生连锁反应,最终导致程序崩溃

    • 而对于上线后的软件,新需求或者变化又层出不穷,我们必须为程序提供扩展的可能性,这就用到了装饰器。

(2)装饰器的介绍

  • 装饰 代指为被装饰对象添加新的功能, 代指器具/工具,装饰器与被装饰的对象均可以是任意可调用对象。
  • 概括地讲,装饰器的作用就是在不修改被装饰对象源代码和调用方式的前提下为被装饰对象添加额外的功能。
  • 装饰器经常用于有切面需求的场景
    • 比如:
      • 插入日志、性能测试、事务处理、缓存、权限校验等应用场景
      • 装饰器是解决这类问题的绝佳设计
      • 有了装饰器,就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。

提示:可调用对象有函数,方法或者类,此处我们单以本章主题函数为例,来介绍函数装饰器,并且被装饰的对象也是函数。

【2】装饰器的实现

函数装饰器分为:无参装饰器和有参装饰两种,二者的实现原理一样,都是’函数嵌套+闭包+函数对象’的组合使用的产物。

(1)无参装饰器的实现

  • 如果想为下述函数添加统计其执行时间的功能
import time

def index():
    time.sleep(3)
    print('Welcome to the index page’)
    return 200

index() #函数执行
  • 遵循不修改被装饰对象源代码的原则,我们想到的解决方法可能是这样
start_time=time.time()
index() #函数执行
stop_time=time.time()
print('run time is %s' %(stop_time-start_time))
  • 考虑到还有可能要统计其他函数的执行时间
    • 于是我们将其做成一个单独的工具,函数体需要外部传入被装饰的函数从而进行调用
    • 我们可以使用参数的形式传入
def wrapper(func): # 通过参数接收外部的值
    start_time=time.time()
    res=func()
    stop_time=time.time()
    print('run time is %s' %(stop_time-start_time))
    return res
  • 但之后函数的调用方式都需要统一改成
wrapper(index)
wrapper(其他函数)
  • 这便违反了不能修改被装饰对象调用方式的原则
    • 于是我们换一种为函数体传值的方式,即将值包给函数
def timer(func):
    def wrapper(): # 引用外部作用域的变量func
        start_time=time.time()
        res=func()
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
  
return res
    return wrapper
  • 这样我们便可以在不修改被装饰函数源代码和调用方式的前提下为其加上统计时间的功能
    • 只不过需要事先执行一次timer将被装饰的函数传入,返回一个闭包函数wrapper重新赋值给变量名 /函数名index
index=timer(index)  #得到index=wrapper,wrapper携带对外作用域的引用:func=原始的index
index() # 执行的是wrapper(),在wrapper的函数体内再执行最原始的index
  • 至此我们便实现了一个无参装饰器timer,可以在不修改被装饰对象index源代码和调用方式的前提下为其加上新功能。
    • 但我们忽略了若被装饰的函数是一个有参函数,便会抛出异常
def home(name):
    time.sleep(5)
    print('Welcome to the home page',name)

home=timer(home)
home('egon')

#抛出异常
TypeError: wrapper() takes 0 positional arguments but 1 was given
  • 之所以会抛出异常,是因为home(egon)调用的其实是wrapper(egon),而函数wrapper没有参数。
    • wrapper函数接收的参数其实是给最原始的func用的
    • 为了能满足被装饰函数参数的所有情况,便用上*args+**kwargs组合
    • 于是修正装饰器timer如下
def timer(func):
    def wrapper(*args,**kwargs):
        start_time=time.time()
        res=func(*args,**kwargs)
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    return wrapper
  • 此时我们就可以用timer来装饰带参数或不带参数的函数了
    • 但是为了简洁而优雅地使用装饰器,Python提供了专门的装饰器语法来取代index=timer(index)的形式
    • 需要在被装饰对象的正上方单独一行添加 @timer
    • 当解释器解释到 @timer 时就会调用timer函数
    • 且把它正下方的函数名当做实参传入
    • 然后将返回的结果重新赋值给原函数名
@timer # index=timer(index)
def index():
    time.sleep(3)
    print('Welcome to the index page')
    return 200
@timer # index=timer(home)
          def home(name):
    time.sleep(5)
    print('Welcome to the home page’,name)
  • 如果我们有多个装饰器,可以叠加多个
@deco3
@deco2
@deco1
def index():
    pass
  • 叠加多个装饰器也无特殊之处,上述代码语义如下:
index=deco3(deco2(deco1(index)))

(2)有参装饰器的实现

  • 了解无参装饰器的实现原理后
    • 我们可以再实现一个用来为被装饰对象添加认证功能的装饰器
    • 实现的基本形式如下
def deco(func):
    def wrapper(*args,**kwargs):
        编写基于文件的认证,认证通过则执行res=func(*args,**kwargs),并返回res
    return wrapper
  • 如果我们想提供多种不同的认证方式以供选择,单从wrapper函数的实现角度改写如下
  def deco(func):
        def wrapper(*args,**kwargs):
            if driver == 'file':
                编写基于文件的认证,认证通过则执行res=func(*args,**kwargs),并返回res
            elif driver == 'mysql':
                编写基于mysql认证,认证通过则执行res=func(*args,**kwargs),并返回res
        return wrapper
  • 函数wrapper需要一个driver参数,而函数deco与wrapper的参数都有其特定的功能
    • 不能用来接受其他类别的参数,可以在deco的外部再包一层函数auth,用来专门接受额外的参数
    • 这样便保证了在auth函数内无论多少层都可以引用到
def auth(driver):
    def deco(func):
        ……
    return deco
  • 此时我们就实现了一个有参装饰器,使用方式如下
# 先调用auth_type(driver='file'),得到@deco,deco是一个闭包函数,包含了对外部作用域名字driver的引用,@deco的语法意义与无参装饰器一样
@auth(driver='file')
def index():
   pass

@auth(driver='mysql')
def home():
   pass
  • 可以使用help(函数名)来查看函数的文档注释,本质就是查看函数的doc属性
    • 但对于被装饰之后的函数,查看文档注释
@timer
def home(name):
    '''
    home page function
    :param name: str
    :return: None
    '''
    time.sleep(5)
    print('Welcome to the home page',name)

print(help(home))
'''
打印结果:

Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

None
'''
  • 在被装饰之后home=wrapper
    • 查看home.name也可以发现home的函数名确实是wrapper
    • 想要保留原函数的文档和函数名属性,需要修正装饰器
def timer(func):
    def wrapper(*args,**kwargs):
        start_time=time.time()
        res=func(*args,**kwargs)
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    wrapper.__doc__=func.__doc__
    wrapper.__name__=func.__name__
    return wrapper
  • 按照上述方式来实现保留原函数属性过于麻烦
    • functools模块下提供一个装饰器wraps专门用来帮我们实现这件事
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args,**kwargs):
        start_time=time.time()
        res=func(*args,**kwargs)
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    return wrapper
posted @ 2023-05-20 14:41  Chimengmeng  阅读(68)  评论(0编辑  收藏  举报
/* */