guxh的python笔记三:装饰器

时间:2022-09-12 19:33:02

1,函数作用域

这种情况可以顺利执行:

total = 0
def run():
print(total)

这种情况会报错:

total = 0
def run():
print(total)
total = 1

这种情况也会报错:

total = 0
def run():
total += 1 # 等效total = total + 1

原因是函数内部对total有定义后,解释器会认为total是局部变量,但是内部执行时,却发现total还没定义。

解决办法是将total声明为全局变量:

total = 0
def run():
global total
......

  

2,*变量和闭包

*变量可以用来保持额外的状态。

什么时候需要保存额外的状态呢?

比如需要对未知输入做不断累加,需要有地方专门存放过去的累加值,然后将新增输入不断累加进去。

类似还有移动平均数的计算,需要有专门的地方存放累加值和累加的次数。

由于普通函数内部的变量在运行一次后,就会消失,无法保存额外状态,因此就需要借助其他手段。

2.1,当保存的额外状态是不可变对象时(数字,字符,元组)

方法一,全局变量

total = 0   # 额外状态
def run(val):
global total
total += val
print(total) run(1) # 1
run(2) # 3
run(3) # 6

使用全局变量不具备可扩展性:

1)如果想更改初始total值得找到全局变量total再修改,无法通过传参形式设定total

2)代码没法重用,不能给别人调用。

方法二,闭包

用高阶函数,把total封装在里面(先别管为什么这叫闭包,后面会做定义和总结)

def cal_total():
total = 0 # 额外状态
def run(val):
nonlocal total
total += val
print(total)
return run run = cal_total()
run(1) # 1
run(2) # 3
run(3) # 6

稍作改变,还可以允许用户传参设定total的初始值(默认为0):

def cal_total(total=0):
def run(val):
nonlocal total
total += val
print(total)
return run run = cal_total(10)
run(1) # 11
run(2) # 13
run(3) # 16

  

方法三,类

单个方法的类,用类的属性来保存额外状态:

class Total:
def __init__(self, total=0):
self.total = total # 额外状态
def run(self, val):
self.total += val
print(self.total) t = Total(10)
t.run(1) # 11
t.run(2) # 13
t.run(3) # 16

为什么会有单个方法的类?因为要保留额外的状态给该方法,比如本例中的total,需要保留下来。

单个方法的类可以用闭包改写。

除了通过对象的方法去调用对象保存的额外状态,还可以通过协程,和functools.partial / lambda去调用,详见函数-高阶函数笔记。

2.2,保存额外状态是可变对象时(字典,列表)

方法一:全局变量

total = []
def run(val):
total.append(val)
print(sum(total)) run(1) # 1
run(2) # 3
run(3) # 6

  

方法二,闭包

def cal_total(total=None):
total = [] if total is None else total
def run(val):
total.append(val)
print(sum(total))
return run run = cal_total([10])
run(1) # 11
run(2) # 13
run(3) # 16

  

方法三,类

class Total:
def __init__(self, total=None):
self.total = [] if total is None else total
def run(self, val):
self.total.append(val)
print(sum(self.total)) t = Total([10])
t.run(1) # 11
t.run(2) # 13
t.run(3) # 16

方法一和方法二中,并没有对total声明global / nonlocal,因为total是容器类型,对其修改时并不会创建副本,而是会就地修改,但如果在函数内部对total有赋值时,就会变成函数内部变量:

total = []
def run():
total = [1, 2, 3]
  
run()
print(total) # [], 此时全局total和局部total没关系

甚至可能会报错:

total = []
def run(val):
total.append(val) # UnboundLocalError: local variable 'total' referenced before assignment
total = [1, 2, 3]

如果想在内部修改外部定义的total,同样需要声明global(全局) / nonlocal(闭包):

total = []
def run():
global total
total = [1, 2, 3] run()
print(total) # [1, 2, 3]

2.3,不可变对象和可变对象的保留额外状态的方法总结

状态是不可变对象时(数字,字符,元组),保留额外状态的方法:

全局变量:需声明global

闭包:需声明nonlocal

类:无

状态是可变对象时(字典,列表),保留额外状态的方法:

全局变量:无需声明global

闭包:无需声明nonlocal,需要注意防御可变参数

类:需要注意防御可变参数

2.4,什么是*变量和闭包

方法二闭包中的额外状态,即*变量!*变量 + run函数即闭包!

可以对*变量和闭包做个简单总结:

*变量定义:1)在函数中引用,但不在函数中定义。2)非全局变量。

闭包定义:使用了*变量的函数 + *变量

如果把闭包所在的函数看做类的话,那么*变量就相当于类变量,闭包就相当于类变量 + 使用了类变量的类方法

回顾2.2中的方法一和方法二:

方法一的total不是*变量,因为total虽然满足了“在run函数中引用,但不在run函数中定义”,但它是全局变量。

方法二的total即*变量。因为total满足:1)run函数中引用,但不在run函数中定义。2)total不是全局变量。

方法二的total + run函数组成了闭包。

2.5,闭包的*变量到底存在哪?

函数也类,因此闭包本质上也可以看做是建了个类,然后把额外状态(*变量)当作类的实例属性来存放的,那么它到底存在哪呢?

还是这个例子:

def cal_total(total=None):
total = [] if total is None else total
def run(val):
total.append(val)
print(sum(total))
return run run = cal_total([10])
run(1)
run(2)

可以把run看成是cal_total类的实例,试试能不能访问total:

print(run.total)  # AttributeError: 'function' object has no attribute 'total'  

只能继续查查看其他属性,发现有一个叫‘__closure__’的属性:

print(type(run))   # <class 'function'>
print(dir(run)) # [..., '__class__', '__closure__', '__code__', ...]

进一步打印,发现__closure__是个长度为1的元组,说明它可以存放多个闭包的*变量:

print(run.__closure__)        #(<cell at 0x00000148E02794F8: list object at 0x00000148E03D65C8>,)
print(type(run.__closure__)) # <class 'tuple'>
print(len(run.__closure__)) # 1

这个唯一的元素是个cell类,并且有个cell_contents属性:

print(type(run.__closure__[0]))  # <class 'cell'>
print(dir(run.__closure__[0])) # [..., 'cell_contents']

尝试打印该属性,正是辛苦找寻的*变量:

print(run.__closure__[0].cell_contents)  # [10, 1, 2]
run.__closure__[0].cell_contents = [1, 2, 3] # AttributeError: attribute 'cell_contents' of 'cell' objects is not writable

访问起来比较麻烦!并且无法进行赋值。如果想访问闭包*变量,可以编写存取函数:

def cal_total(total=None):
total = [] if total is None else total def run(val):
total.append(val)
print(sum(total)) def get_total():
return total def set_total(components):
nonlocal total
total = components run.get_total = get_total # get_total是cal_total下的函数,需要把它挂到run下面,一切皆对象,给run动态赋上方法,类似猴子补丁
run.set_total = set_total
return run run = cal_total([10])
run(1) # 11
run(2) # 13
print(run.get_total()) # [10, 1, 2]
run.set_total([1, 1, 1]) # [1, 1, 1]
print(run.get_total())
run(1) # 4

3,基本装饰器

3.1,单层装饰器

单层装饰器:

import time
def timethis(func):
st = time.time()
func()
print(time.time() - st)
return func @timethis # 等效于run = timethis(run)
def run():
time.sleep(2)
print('hello world') # 执行了两遍
return 1 # 返回值无法被调用方获取 ret = run()
print(ret) # None

存在问题:

被装饰函数中的功能会被执行两遍(func执行一遍后再返回func地址,调用方获取地址后再次执行)

无法返回(并且被装饰函数有返回值时无法获取到)

无法传参(被装饰函数有参数时无法传参)

3.2,双层装饰器 — 标准装饰器

2次传参:外层传函数,内层传参数。

2次返回:第一次返回被装饰函数运行后的结果,第二次返回内层装饰器地址

def cal_time(func):
@wraps(func)
def wrapper(*args, **kwargs):
st = time.time()
result = func(*args, **kwargs)
print(time.time() - st)
return result
return wrapper @cal_time # 等效于run = cal_time(run)
def run(s):
time.sleep(2)
return '{:-^21}'.format(s) # 执行居中打印import time >>>run('hello world')
2.0003201961517334
-----hello world-----

第二次return wrapper,相当于@cal_time装饰run函数时,run = cal_time(run),让run拿到内层wrapper函数地址,运行run('hello world')时,实际运行的是wrapper('hello world')。

第一次return result,相当于让result拿到run函数运行后的结果。

如果想访问原始函数,可以用__wrapped__:

>>>run.__wrapped__('hello world')
-----hello world-----

3.3,三层装饰器 — 可接受参数的装饰器

假如想让装饰器能够接收参数,当传入'center'时,输出的时间能够精确到小数点后一位并且居中打印,可以使用三层装饰器:

def cal_time(ptype=None):
def decorate(func):
fmt = '{:-^21.1f}' if ptype == 'center' else '{}'
@wraps(func)
def wrapper(*args, **kwargs):
st = time.time()
result = func(*args, **kwargs)
print(fmt.format(time.time() - st))
return result
return wrapper
return decorate @cal_time('center')
def run(s):
time.sleep(2)
return '{:-^21}'.format(s) >>>run('hello world')
---------2.0---------
-----hello world-----

不传入参数时:

@cal_time()
def run(s):
time.sleep(2)
return '{:-^21}'.format(s) >>>run('hello world')
2.0021121501922607
-----hello world-----

备注:

如果想实现不传入参数时用法与双层装饰器保持一致(@cal_time),同时又可以接受参数,即可选参数的装饰器,详见4.1

如果想实现可接受参数,并且可以更改属性的装饰器,详见4.2

4,高级装饰器 

4.1,双层装饰器 - 可选参数的装饰器

标准的可选参数装饰器,通过3层装饰器实现,依次传入:参数/被装饰函数地址/被装饰函数参数

本例用双层就能搞定可选参数,是因为直接在外层传入参数和被装饰函数地址,然后通过partial绑定了参数

def cal_time(func=None, *, ptype=None):
if func is None:
return partial(cal_time, ptype=ptype)
fmt = '{:-^21.1f}' if ptype == 'center' else '{}'
@wraps(func)
def wrapper(*args, **kwargs):
st = time.time()
result = func(*args, **kwargs)
print(fmt.format(time.time() - st))
return result
return wrapper @cal_time(ptype='center') # 装饰时,必须用关键字参数,否则传入字符会被装饰器当作func
def run(s):
time.sleep(2)
return '{:-^21}'.format(s) >>>run('hello world')
---------2.0---------
-----hello world-----

不传入参数时,与标准的双层装饰器一致:

@cal_time
def run(s):
time.sleep(2)
return '{:-^21}'.format(s) >>>run('hello world')
2.001026153564453
-----hello world-----

4.2,三层装饰器 — 可接受参数,并且可以更改属性的装饰器

装饰时可以添加参数,装饰完以后,可以更改属性的装饰器

def attach_wrapper(obj, func=None):
if func is None:
return partial(attach_wrapper, obj)
setattr(obj, func.__name__, func)
return func def cal_time(ptype=None):
def decorate(func):
fmt = '{:-^21.1f}' if ptype == 'center' else '{}'
@wraps(func)
def wrapper(*args, **kwargs):
st = time.time()
result = func(*args, **kwargs)
print(fmt.format(time.time() - st))
return result @attach_wrapper(wrapper)
def set_fmt(new_fmt):
nonlocal fmt
fmt = new_fmt return wrapper
return decorate @cal_time('center')
def run(s):
time.sleep(2)
return '{:-^21}'.format(s) >>>run('hello world')
---------2.0---------
-----hello world-----
>>>run.set_fmt('{:->21.1f}') # 直接更改装饰器的fmt属性
>>>run('hello world')
------------------2.0
-----hello world-----

4.3,能够实现函数参数检查的装饰器

对函数的参数进行检查可以通过:property,工厂函数,描述符

本例演示了通过装饰器对函数参数的检查:

def typeassert(*ty_args, **ty_kwargs):
def decorate(func):
if not __debug__: # -O或-OO的优化模式执行时直接返回func
return func sig = signature(func)
bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments @wraps(func)
def wrapper(*args, **kwargs):
bound_values = sig.bind(*args, **kwargs)
for name, value in bound_values.arguments.items():
if name in bound_types:
if not isinstance(value, bound_types[name]):
raise TypeError('Argument {} must be {}'.format(name, bound_types[name]))
return func(*args, **kwargs)
return wrapper
return decorate @typeassert(int, int)
def add(x, y):
return x + y >>>add(1, '3')
TypeError: Argument y must be <class 'int'>

4.4,在类中定义装饰器

property就是一个拥有getter(), setter(), deleter()方法的类,这些方法都是装饰器

为什么要这样定义?因为多个装饰器方法都在操纵property实例的状态

class A:
def decorate1(self, func): # 通过实例调用的装饰器
@wraps(func)
def wrapper(*args, **kwargs):
print('Decorate 1')
return func(*args, **kwargs)
return wrapper @classmethod
def decorate2(cls, func): # 通过类调用的装饰器
@wraps(func)
def wrapper(*args, **kwargs):
print('Decorate 2')
return func(*args, **kwargs)
return wrapper a = A() @a.decorate1 # 通过实例调用装饰器
def spam():
pass @A.decorate2 # 通过类调用装饰器
def grok():
pass >>>spam()
Decorate 1
>>>grok()
Decorate 2

4.5,把装饰器定义成类 

python一切皆对象,函数也是对象,可以从类的角度看待装饰器。

装饰函数的2步操作,第一步可以看作是cal_time类的实例化,第二步可以看做是cal_time类实例的__call__调用:

run = cal_time(run)   # @cal_time
run('hello world') # 调用cal_time类的实例run

通过类改写二层装饰器:

class cal_time:
def __init__(self, fun):
self.fun = fun def cal_timed(self, *args, **kwargs):
st = time.time()
ret = self.fun(*args, **kwargs)
print(time.time() - st)
return ret def __call__(self, *args, **kwargs):
return self.cal_timed(*args, **kwargs) @cal_time
def run(s):
time.sleep(2)
return '{:-^21}'.format(s) # 执行居中打印 result = run('hello world')
print(result) output:
2.0132076740264893
-----hello world-----  

从上可以看到,装饰器可以通过函数实现,也可以通过类实现。

这与闭包中保存额外状态的实现方法类似,保存额外状态可以通过类实例的属性(方法三),也可以通过闭包的*变量(方法二)。

当然闭包中“方法三”没有实现__call__,因此需要通过“实例.方法”的方式去调用,也可以参照装饰器类的实现做如下改造:

class cal_total:

    def __init__(self, total=None):
self.total = [] if total is None else total def run(self, val):
self.total.append(val)
print(sum(self.total)) def __call__(self, val):
return self.run(val) run = cal_total([10])
run(1) # 11
run(2) # 13
run(3) # 16

同样,本例中的装饰器实现也可以实现闭包“方法三”的效果,即通过“实例.方法”的方式去调用。

因此,本质上完全可以把嵌套函数(闭包,装饰器),看做是类的特例,即实现了可调用特殊方法__call__的类。

再回头看看,闭包在执行run = cal_total(10)时,装饰器在执行@cal_time,即run = cal_time(run)时,都相当于在实例化,前者在实例化时输入的是属性,后者实例化时输入的是方法。

然后,闭包执行run(1),装饰器执行run('hello world')时,都相当于调用实例__call__方法。

python cookbook要求把装饰器定义成类必须实现__call__和__get__:

class Profiled:
def __init__(self, func):
wraps(func)(self)
self.ncalls = 0 def __call__(self, *args, **kwargs):
self.ncalls += 1
return self.__wrapped__(*args, **kwargs) def __get__(self, instance, cls):
if instance is None:
return self
else:
return types.MethodType(self, instance) @Profiled
def add(x, y):
return x + y >>>add(2, 3)
5
>>>add(4, 5)
9
>>>add.ncalls
2

4.6,实现可以添加参数的装饰器

def optional_debug(func):
@wraps(func)
def wrapper(*args, debug=False, **kwargs):
if debug:
print('Calling', func.__name__)
return func(*args, **kwargs)
return wrapper @optional_debug
def spam(a, b, c):
print(a, b, c) >>>spam(1, 2, 3)
1 2 3
>>>spam(1, 2, 3, debug=True)
Calling spam
1 2 3

4.7,通过装饰器为类的方法打补丁

常用打补丁方法有:mixin技术,元类(复杂),继承(需要理清继承关系),装饰器(速度快,简单)

def log_getattribute(cls):
orig_getattribute = cls.__getattribute__
def new_getattribute(self, name):
print('Getting x:', name)
return orig_getattribute(self, name)
cls.__getattribute__ = new_getattribute
return cls @log_getattribute
class Foo:
def __init__(self, x):
self.x = x >>>f = Foo(5)
>>>f.x
Getting x: x
5

5,其他

装饰器作用到类和静态方法上:需要放在@classmethod和@staticmethod下面

所有代码中涉及到到库主要包括:

from inspect import signature
from functools import wraps, partial
import logging
import time

guxh的python笔记三:装饰器的更多相关文章

  1. python笔记 - day4-之装饰器

                 python笔记 - day4-之装饰器 需求: 给f1~f100增加个log: def outer(): #定义增加的log print("log") ...

  2. python进阶&lpar;三&rpar;~~~装饰器和闭包

    一.闭包 满足条件: 1. 函数内嵌套一个函数: 2.外层函数的返回值是内层函数的函数名: 3.内层嵌套函数对外部作用域有一个非全局变量的引用: def func(): print("=== ...

  3. 20&period;python笔记之装饰器

    装饰器 装饰器是函数,只不过该函数可以具有特殊的含义,装饰器用来装饰函数或类,使用装饰器可以在函数执行前和执行后添加相应操作. 装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插 ...

  4. Python笔记:装饰器

    装饰器        1.特点:装饰器的作用就是为已存在的对象添加额外的功能,特点在于不用改变原先的代码即可扩展功能: 2.使用:装饰器其实也是一个函数,加上@符号后放在另一个函数“头上”就实现了装饰 ...

  5. Noah的学习笔记之Python篇:装饰器

    Noah的学习笔记之Python篇: 1.装饰器 2.函数“可变长参数” 3.命令行解析 注:本文全原创,作者:Noah Zhang  (http://www.cnblogs.com/noahzn/) ...

  6. python设计模式之装饰器详解&lpar;三&rpar;

    python的装饰器使用是python语言一个非常重要的部分,装饰器是程序设计模式中装饰模式的具体化,python提供了特殊的语法糖可以非常方便的实现装饰模式. 系列文章 python设计模式之单例模 ...

  7. Python学习笔记:装饰器

    Python 装饰器的基本概念和应用 代码编写要遵循开放封闭原则,虽然在这个原则是用的面向对象开发,但是也适用于函数式编程,简单来说,它规定已经实现的功能代码不允许被修改,但可以被扩展,即: 封闭:已 ...

  8. 三分钟搞定Python中的装饰器

    python的装饰器是python的特色高级功能之一,言简意赅得说,其作用是在不改变其原有函数和类的定义的基础上,给他们增添新的功能. 装饰器存在的意义是什么呢?我们知道,在python中函数可以调用 ...

  9. Python函数06&sol;装饰器

    Python函数06/装饰器 目录 Python函数06/装饰器 内容大纲 1.装饰器 1.1 开放封闭原则 1.2 装饰器 2.今日练习 内容大纲 1.装饰器 1.装饰器 1.1 开放封闭原则 扩展 ...

随机推荐

  1. 发布在即!&period;NET Core 1&period;0 RC2已准备就绪!!

    先说点废话,从去年夏天就开始关注学习ASP.NET Core,那时候的版本还是beta5,断断续续不停踩坑.一路研究到11月份RC1发布. 在这个乐此不疲的过程里,学习了很多新的东西,对ASP.NET ...

  2. POJ 2253 Frogger(Dijkstra&rpar;

    传送门 Frogger Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 39453   Accepted: 12691 Des ...

  3. validate

    <?php $rules = [ "ip" => function ($var) {return ip2long($var);}, "email" ...

  4. jqgrid表格列动态加载的实现

    选中几个测点名,在表格中就显示几列. 具体代码如下: function reloadGrid(postData){ $('#gridTable').jqGrid('GridUnload'); var ...

  5. ngnix 配置

    #运行用户 user www-data;     #启动进程,通常设置成和cpu的数量相等 worker_processes  1; #全局错误日志及PID文件 error_log  /var/log ...

  6. GROUP BY 和 ORDER BY一起使用时的注意点

    order by的列,必须是出现在group by子句里的列ORDER BY要在GROUP BY的后面

  7. Docker 配置国内镜像加速器,加速下载速度

    文章首发自个人微信公众号:小哈学Java 个人网站地址:https://www.exception.site/docker/docker-configuration-of-mirror-acceler ...

  8. 00004-20180324-20180517-fahrenheit&lowbar;converter--华氏温度到摄氏温度转换计算器

    00004-20180324-20180517-fahrenheit_converter--华氏温度到摄氏温度转换计算器 def fahrenheit_converter(C): fahrenheit ...

  9. Docker优势

    设计,开发 ---> 测试 ----> 部署,运行 代码+运行环境 ---> 镜像 image 环境一致,资源占用少 自动化平台 Docker image的制作很重要

  10. Linux下可视化空间分析工具ncdu

    场景:磁盘空间占满后快速查找某个目录(子目录)占用空间大. ncdu /var (分析后按左右键查看即可)