《流畅的Python》学习笔记(7)—— 装饰器

《流畅的Python》(7)—— 装饰器

Python装饰器是笔者至少学习过三遍知识点,通过《Fluent Python》算是基本上弄清楚装饰器的基础了,写下一些心得体会与思考。

包括的内容有:

  • Python如何计算装饰器句法
  • Python如何判断变量是不是局部的
  • 闭包存在的原因和工作原理
  • nonlocal解决问题

1. 基础知识

装饰器是可调用的对象,
输入:函数
输出:函数

1.1句法

假设有一个装饰器decorator,将函数target装饰的句法如下:

@decorator
def target():
    print("running target")

上述代码等价于:

def target():
    print("running target")

target = decorator(target)

这样操作后原本target的功能没有改变,但是可以在target函数的上下文增加功能,类似叠汉堡,肉饼是点好不变的,可以加不同的蔬菜和沙司。

更多基本用法可以查看廖雪峰老师的Python教程

1.2 装饰器运行阶段:导入模块时即运行

1.3 函数的作用域

函数的作用域对任何语言都很重要,在C语言中,函数的作用域就是{}之内,如果想调用{}之外的变量,那必须是全局变量。而在Python中,情况有所不同,需要点出来:

  • 函数是由解释器成块编译的,而不是一行行的编译并运行
  • 如果函数使用的变量名在块内没有定义过,那么它会向上一个块寻找该变量名

一个问题: 请问下列代码运行结果是什么?

b = 6
def f2(a):
    print(a)
    print(b)
    b = 9
f2(3)

结果是:报错,b变量在声明前就使用,原因是:编译是按块编译,由于b=9在块中出现,使得解释器认为b是个局部变量,而不是全局遍历,而print(b)在b=9之前,所以造成错误。
以上两点结论可以通过查看函数的字节码中获得。

那么该如何解决这个问题呢?在这个问题中用global关键字解决,申明变量b为全局变量

下面我们看闭包。

2. 闭包(closure)

2.1闭包的基础

闭包:是引用了自由变量的函数。那么自由变量是什么呢?
自由变量:未在本地作用域中绑定的变量。
以一个计算滑动平均值的例子来说:

def make_average():
   series = []
   def averager(new_value):
       series.append(new_value)
       total = sum(series)
       return total / len(series)
   return averager
avg = make_average()

这里averager是函数闭包函数,因为它调用了series这个自由变量。series是自由变量的原因是:series在averager的作用域中没有声明或定义

那么series不绑定在averager的本地作用域那么它应该存在哪里呢?

首先一个函数被定义或导入时,就会在内存中存储。其次查看make_average中的局部变量

In [11]: make_average.__code__.co_varnames
Out[11]: ('averager',)

从make_average.code .co_varnames()(函数的局部变量),可以看到只有average这一个局部变量,而series不在make_average中,那series就应该在averager中,最后的答案是在avg.__closure __.cell_content中保存,说明装饰器的运行由闭包属性支撑。

2.2 再论函数作用域

在闭包中也会出现1.3的问题,比如2.1中的计算滑动平滑的方法效率很低,为了计算平均值我们并不需要记录所有的值,只需要记录数据总和与数据个数两个量,直觉上修改为。

def make_average():
   total = 0
   count = 0
   def averager(new_value):
       total += new_value
       count += 1
       return total / count
   return averager
avg = make_average()
avg(1)

出现同样的错误:

UnboundLocalError: local variable 'total' referenced before assignment

这是因为 total += new_value 等价于 total = total + new_value ,还是在同样的原因:在total没有定义时就在等式右侧使用了它。

解决方案:用nonlocal关键字声明两个变量为自由变量

def make_average():
   total = 0
   count = 0
   def averager(new_value):
       nonlocal total, count
       total += new_value
       count += 1
       return total / count
   return averager
avg = make_average()
avg(1)
avg(2)

这时程序运行正常。

3. 装饰器

第二节我们说了很多关于闭包的知识,那么这和我们的装饰器的关系是什么?

3.1 装饰器与闭包关系

从例子入手,一个可以给任何函数打印函数名的装饰器:

def print_name(func):
    
    def wrapper(*args, **kw):
        print("Now %s is running."%func.__name__)
        res = func(*args, **kw)
        return res
    return wrapper

# example
@print_name
def add(a, b):
    return a + b

add(1, 2)

在这个例子中,print_name就是一个装饰器,里面的函数为闭包,因为它调用自由变量 func,func不在wrapper中声明而是在print_name中,所以这是一个自由变量。装饰器必须要用到闭包的特性来实现。

3.2 标准库中的装饰器

Python内置的装饰器有:property、classmethod和staticmethod,这些将在面向对象环节中再阐述。此外,一个常见的装饰器为:functools.lru_cache做备忘,可以缓存函数输出的结果。

unctools.lru_cache做备忘

可能会使用在一些运算时间久,但是输入情况有限的情况,比如阶乘。

import time
import functools

def clock(func):
    def clocked(*args, **kw):
        t0 = time.perf_counter()
        result = func(*args, **kw)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ','.join(repr(arg) for arg in args) # 获取函数的参数表达
        print('[%0.8fs]%s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked
@functools.lru_cache() # 由于lru_cache是接受参数的装饰器,所以要加括号
@clock
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)

factorial(31)

单分配泛函数functools.singledispatch

用于解决Python没有方法或函数重载的问题(Python中只有重写函数和方法没有重载)。

from functools import singledispatch
from collections import abc
import numbers
import html
@singledispatch ➊
def htmlize(obj):
	content = html.escape(repr(obj))
	return '<pre>{}</pre>'.format(content)

@htmlize.register(str)def _(text): ➌
	content = html.escape(text).replace('\n', '<br>\n')
	return '<p>{0}</p>'.format(content)
	
@htmlize.register(numbers.Integral)def _(n):
	return '<pre>{0} (0x{0:x})</pre>'.format(n)
	
@htmlize.register(tuple) ➎
@htmlize.register(abc.MutableSequence)
def _(seq):
	inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
	return '<ul>\n<li>' + inner + '</li>\n</ul>'
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值