Python面试——装饰器

知识链接:
在这里插入图片描述

装饰器

装饰器可调用的对象,其参数是被装饰的函数。装饰器可能会处理被装饰的函数然后把它返回,或者将其替换成另外一个函数或者可调用对象。
装饰器有两大特性:

  • 能把被装饰的函数替换成其他函数(在元编程时,这样更方便 --> 在运行时改变程序的行为)
  • 装饰器在加载模块时立即执行

把被装饰的函数替换成其它函数

这样描述可能比较抽象,我们可以从一个例子来看下其特性:如何去计算函数执行的时间?
处理思路常规的处理思路是:在函数执行开始与结束时分别记录时间,并计算差值(执行时间)。这样处理有好处,也有不便的地方。
好处:如果是计算单个函数,修改简单易处理。
不便:会修改整个函数的执行逻辑,且在大量需要计算函数执行时间的地方不方便统一修改。
我们可以通过装饰器来进行处理:

import time

def fun1():
	time.sleep(3)  # 模拟函数执行过程

def timmer(func):
	def inner(*args, **kwargs):
  	start_time = time.time()
  	func(*args, **kwargs)
  	end_time = time.time()
    print("函数执行时间为:", end_time - start_time)

	return inner

fun1 = timmer(fun1)
fun1()  # 函数执行时间为: 3.0051169395446777

上诉代码的执行效果与下述写法一样:

import time

def timmer(func):
	def inner(*args, **kwargs):
  	start_time = time.time()
  	func(*args, **kwargs)
  	end_time = time.time()
    print("函数执行时间为:", end_time - start_time)

	return inner

@timmer
def fun1():
	time.sleep(3)  # 模拟函数执行过程

fun1()  # 函数执行时间为: 3.0051169395446777

这样做有一个小问题,就是被装饰的函数的__name__和__doc__属性被遮盖了。
可以通过使用functools.wraps 装饰器把相关属性从fun复制到timmer中:

import time
import functools

def timmer(func):
	@functools.wraps(func)
	def inner(*args, **kwargs):
  	start_time = time.time()
  	func(*args, **kwargs)
  	end_time = time.time()
    print("函数执行时间为:", end_time - start_time)

	return inner

@timmer
def fun1():
	time.sleep(3)  # 模拟函数执行过程

模块加载时立即执行

再通过一个例子来理解装饰器会在模块加载时立即执行:

def register(func):
    print(f"running decorator...register: {
      
      func}")
    return func


@register
def fun1():
    print("running func1")


@register
def fun2():
    print("running fun2")


def fun3():
    print("running fun3")


if __name__ == '__main__':
    fun1()
    fun2()
    fun3()
------------------------------
running decorator...register: <function fun1 at 0x7f7900088280>
running decorator...register: <function fun2 at 0x7f7900096550>
running func1
running fun2
running fun3


可以看出:装饰器在导入模块时立即执行,而被装饰的函数只有在明确调用时才会运行。这就突出了在Python中导入时和运行时的区别。

另外,如果存在多层装饰器时,导入时的执行顺序是由内到外,运行时的执行顺序是由外到内:

def war1(func):
    print("running war1")

    def inner1(*args, **kwargs):
        print("====inner1====")
        print(f'inner1中参数func:{
      
      func}')
        func(*args, **kwargs)

    return inner1


def war2(func):
    print("running war2")

    def inner2(*args, **kwargs):
        print("====inner2====")
        print(f'inner2中参数func:{
      
      func}')
        func(*args, **kwargs)

    return inner2


@war2
@war1
def fun1():
    print("running func1")


# --> fun1 = war2(war1(fun1))

if __name__ == '__main__':
    fun1()


------------------------------------
running war1
running war2
====inner2====
inner2中参数func:<function war1.<locals>.inner at 0x7fad681a54c0>
====inner1====
inner1中参数func:<function fun1 at 0x7fad680e6550>
running func1

变量作用域

在理解闭包之前,我们需要先了解下变量作用域。
先来看第一个例子,这段代码很简单也很容易理解:

b = 3

def fun():
    a = 1
    print(a)  # 获取局部变量并打印
    print(b)  # 获取全局变量并打印

fun()
--------------------------------
1
3

下面,再坐下简单的修改:

b = 3

def fun():
    a = 1
    print(a)
    print(b)
    b = 9

fun()
------------------
UnboundLocalError: local variable 'b' referenced before assignment
1

在示例一的基础上,增加一行为b赋值,可是在执行print(b)时却报错了,但是输出了1,这表明print(a)是正常执行的。为什么有全局变量b,且是在print(b)之后进行赋值操作的,为什么会报错没有声明就引用的错误了呢?

我们可以通过比较两个示例函数的字节码来找到原因:

import dis
print(dis.dis(fun))
-----------------------------------------------------
  7           0 LOAD_CONST               1 (1)
              2 STORE_FAST               0 (a)

  8           4 LOAD_GLOBAL              0 (print)
              6 LOAD_FAST                0 (a)
              8 CALL_FUNCTION            1
             10 POP_TOP

  9          12 LOAD_GLOBAL              0 (print)
             14 LOAD_GLOBAL              1 (b)
             16 CALL_FUNCTION            1
             18 POP_TOP
             20 LOAD_CONST               0 (None)
             22 RETURN_VALUE
None

import dis
print(dis.dis(fun))
-----------------------------------------------------
	7           0 LOAD_CONST               1 (1)
              2 STORE_FAST               0 (a)

  8           4 LOAD_GLOBAL              0 (print)
              6 LOAD_FAST                0 (a)
              8 CALL_FUNCTION            1
             10 POP_TOP

  9          12 LOAD_GLOBAL              0 (print)
             14 LOAD_FAST                1 (b)
             16 CALL_FUNCTION            1
             18 POP_TOP

 10          20 LOAD_CONST               2 (9)
             22 STORE_FAST               1 (b)
             24 LOAD_CONST               0 (None)
             26 RETURN_VALUE
None

从字节码中可以看出Cpython解释器在编译示例二中的函数时,把b视作局部变量(因为在函数中为b赋值了),即使在print(b)后面才为b赋值,因为变量的种类(是不是局部变量)不能改变函数的定义体。

global

有没有一种方法可以在示例二中为全局变量赋值,又不会导致解释器报错的呢?
Python提供了global关键字,可以在函数体中声明变量为全局变量:

import dis

b = 3


def fun():
    global b
    a = 1
    print(a)
    print(b)
    b = 9
  	print(b)

if __name__ == '__main__':
    fun()
    print(dis.dis(fun))
----------------------------
1 
3
9

打印其字节码可以看到,Cpython解释器已经把b视为全局变量了:

  8           0 LOAD_CONST               1 (1)
              2 STORE_FAST               0 (a)

  9           4 LOAD_GLOBAL              0 (print)
              6 LOAD_FAST                0 (a)
              8 CALL_FUNCTION            1
             10 POP_TOP

 10          12 LOAD_GLOBAL              0 (print)
             14 LOAD_GLOBAL              1 (b)
             16 CALL_FUNCTION            1
             18 POP_TOP

 11          20 LOAD_CONST               2 (9)
             22 STORE_GLOBAL             1 (b)
             24 LOAD_CONST               0 (None)
             26 RETURN_VALUE
None

闭包

闭包指延展了作用域的函数,其中包含函数定义体中的引用,但是不在定义体中定义的非全局变量。

在装饰器的示例中,存在一个疑问:func是timmer函数的局部变量,在timmer(func1)执行完后,其本地作用域也会清除,为什么在执行func1()时可以获取到对应的func参数呢?

import time

def fun1():
	time.sleep(3)

def timmer(func):
	def inner(*args, **kwargs):
  	start_time = time.time()
  	func(*args, **kwargs)
  	end_time = time.time()
    print("函数执行时间为:", end_time - start_time)

	return inner

fun1 = timmer(fun1)
fun1()

在inner函数中,func是自由变量(free varialbe),指未在本地作用域中绑定的变量。审查fun1的__code__属性可以看到这些值,自由变量的值可以在__closure__中查看:

>>> fun1.__code__.co_varnames
('args', 'kwargs', 'start_time', 'end_time')
>>> func1.__code__.co_freevars
('func',)
>>> fun1.__closure__[0].cell_contents
<function fun1 at 0x7fc0580e8040>

nonlocal

我们接下来再看一个例子,通过闭包来计算移动的平均值的高阶函数:

def make_average():
    total_step = 0  # 移动总数
    count = 0  # 移动次数

    def average(new_step):
        count += 1
        total_step += new_step
        return total_step / count

    return average


if __name__ == '__main__':
    avg = make_average()
    print(avg(10))  # 模拟移动
    print(avg(20))
-------------------------------------
UnboundLocalError: local variable 'count' referenced before assignment

通过对闭包的学习,这段代码应该没问题,但是执行却报错了。正常来说count和total_step都应该是自由变量,为什么会被Cpython解释器视为局部变量呢【在上一节可以找到答案】?
为了解决这个问题,Python3引入了nonlocal声明,它的作用是把变量声明为自由变量,即使如果为闭包中绑定的自由变量赋予了新值则会更新绑定:

def make_average():
    total_step = 0  # 移动总数
    count = 0  # 移动次数

    def average(new_step):
      	nonlocal count, total_step
        count += 1
        total_step += new_step
        return total_step / count

    return average


if __name__ == '__main__':
    avg = make_average()
    print(avg(10))  # 模拟移动
    print(avg(20))
---------------------------------------
10.0
15.0

查看字节码可以看到count和被声明为自由变量:

  7           0 LOAD_DEREF               0 (count)
              2 LOAD_CONST               1 (1)
              4 INPLACE_ADD
              6 STORE_DEREF              0 (count)

  8           8 LOAD_DEREF               1 (total_step)
             10 LOAD_FAST                0 (new_step)
             12 INPLACE_ADD
             14 STORE_DEREF              1 (total_step)

  9          16 LOAD_DEREF               1 (total_step)
             18 LOAD_DEREF               0 (count)
             20 BINARY_TRUE_DIVIDE
             22 RETURN_VALUE
None

函数是一等对象

在Python中,函数是一等对象:

  • 在运行时被创建
  • 能赋值给变量或数据结构中的元素
  • 能作为参数传递给函数
  • 能做为函数的返回结果
  • 后面亮点可以为我们理解闭包提供帮助

装饰器面试题

  • 装饰器基础知识考察

  • 装饰器作用

  • 装饰器的原理和实现

  • 装饰器存在的缺陷

  • 工作中是否有用到装饰器(Python内置的或第三方封装的)

  • 内置

  • property、classmethod、staticmethod

  • functools.wraps

  • functools.lru_cache

  • functools.singledispatch

  • 第三方

  • Django的csrf_exempt

  • 手写装饰器

  • 装饰器进阶

  • 导入时和运行时

  • 多层装饰器执行顺序

  • 自由变量

猜你喜欢

转载自blog.csdn.net/weixin_45841831/article/details/129011754