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进阶(三)~~~装饰器和闭包

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

  3. 20.python笔记之装饰器

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

  4. Python笔记:装饰器

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

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

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

  6. python设计模式之装饰器详解(三)

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

  7. Python学习笔记:装饰器

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

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

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

  9. Python函数06/装饰器

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

随机推荐

  1. 推举算法 AdaBoost 哥德尔奖 Godel Prize

    推举算法 AdaBoost  2003年理论计算机科学界最高奖 哥德尔奖 Godel Prize

  2. VMware Workstation下ubuntu虚拟机无法上网连不上网络解决

    写在前面:本博客为本人原创,严禁任何形式的转载!本博客只允许放在博客园(.cnblogs.com),如果您在其他网站看到这篇博文,请通过下面这个唯一的合法链接转到原文! 本博客全网唯一合法URL:ht ...

  3. 分库分表、读写分离——用Sql和ORM(EF)来实现

    分库:将海量数据分成多个库保存,比如:2017年的订单库——Order2017,2018年的订单库——Order2018... 分表:水平分表(Order拆成Order1.....12).垂直分表(O ...

  4. POJ 3264 线段树入门解题报告

    题意:给n个值, Q次询问, 每次询问给定一个区间, 要求输出该区间最大最小值之差 思路:暴力的话每次询问都要遍历多次for循环一定会超时, 用线段树记录区间的信息(左边界右边界, 该区间最大值最小值 ...

  5. Redis入门到高可用(十七)—— 持久化开发运维常见问题

    1.fork操作 2.子进程开销和优化 3.AOF阻塞

  6. 使用jfreechart生成柱状图、折线图、和饼状图

    JFreeChart是JAVA平台上的一个开放的图表绘制类库.它完全使用JAVA语言编写,是为applications, applets, servlets 以及JSP等使用所设计.下面我就详细介绍如 ...

  7. Python 进程之间共享数据

    最近遇到多进程共享数据的问题,到网上查了有几篇博客写的蛮好的,记录下来方便以后查看. 一.Python multiprocessing 跨进程对象共享  在mp库当中,跨进程对象共享有三种方式,第一种 ...

  8. 艾妮记账本微信小程序开发(失败版)

    这是一个寒假假期作业,要求是用web开发或者微信小程序或者手机app开发的,我本来是打算用微信小程序开发的,但由于这个后台数据库连接需要通过https认证后的浏览器再访问MySQL.所以做到后台数据库 ...

  9. 修复svn hook导致的字符集错误

    修改pre-commit钩子,如果返回中文信息,可能会报如下错误: Error output could not be translated from the native locale to UTF ...

  10. DelayQueue源码解析

    DelayQueue是一个支持延时获取元素的无界阻塞队列.里面的元素全部都是“可延期”的元素,列头的元素是最先“到期”的元素,如果队列里面没有元素到期,是不能从列头获取元素的,哪怕有元素也不行.也就是 ...