python装饰器
函数基础
函数定义
在 Python 中,函数是一等对象,编程语言理论家把“一等对象”定义为满足下述条件的程序实体:
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
有了一等函数,就可以使用函数式风格编程。
函数式编程
的特点之一是使用高阶函数——接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher-order function)。在函数式编程范式中,最为人熟知的高阶函数有
map
、filter
、reduce
和apply
。在 Python 3 中,
map
和filter
还是内置函数,但是由于引入了列表推导和生成器表达式,它们变得没那么重要了。
sum
和reduce
的通用思想是把某个操作连续应用到序列的元素上,累计之前的结果,把一系列值归约成一个值。
all
和any
也是内置的归约函数。all(iterable): 如果
iterable
的每个元素都是真值,返回True
;all([])
返回True
。any(iterable): 只要
iterable
中有元素是真值,就返回True
;any([])
返回False
。
lambda
关键字在 Python 表达式内创建匿名函数。
lambda
函数的定义体中不能赋值,也不能使用while
和try
等 Python 语句。
lambda
句法只是语法糖:与def
语句一样,lambda
表达式会创建函数对象。
函数和方法
分类角度分析
(1) 函数的分类:
- 内置函数:python内嵌的一些函数。
- 匿名函数:一行代码实现一个函数功能。
- 递归函数
- 自定义函数:根据自己的需求,来进行定义函数。
(2) 方法的分类:
- 普通方法:直接用self调用的方法。
- 私有方法:__函数名,只能在类中被调用的方法。
- 属性方法:@property,将方法伪装成为属性,让代码看起来更合理。
- 特殊方法(双下划线方法):以__init__为例,是用来封装实例化对象的属性,只要是实例化对象就一定会执行__init__方法,如果对象子类中没有则会寻找父类(超类),如果父类(超类)也没有,则直接继承object(python 3.x)类,执行类中的__init__方法。
- 类方法:通过类名的调用去操作公共模板中的属性和方法。
- 静态方法:不用传入类空间、对象的方法, 作用是保证代码的一致性,规范性,可以完全独立类外的一个方法,但是为了代码的一致性统一的放到某个模块(py文件)中。
作用域角度分析
(1) 函数作用域:从函数调用开始至函数执行完成,返回给调用者后,在执行过程中开辟的空间会自动释放,也就是说函数执行完成后,函数体内部通过赋值等方式修改变量的值不会保留,会随着返回给调用者后,开辟的空间会自动释放。
(2) 方法作用域:通过实例化的对象进行方法的调用,调用后开辟的空间不会释放,也就是说调用方法中对变量的修改值会一直保留。
调用方式分析
(1) 函数:通过
函数名()
的方式进行调用。(2) 方法:通过
对象.方法名()
的方式进行调用。
1
2
3
4
5
6
7
8
9
10
11
12 class Foo(object):
def func(self):
pass
#实例化
obj = Foo()
# 执行方式一:调用的func是方法
obj.func() #func 方法
# 执行方式二:调用的func是函数
Foo.func(123) # 函数
可调用对象
除了用户定义的函数,调用运算符(即
()
)还可以应用到其他对象上。如果想判断对象能否调用,可以使用内置的callable()
函数。如果类定义了
__call__
方法,那么它的实例可以作为函数调用。判断对象能否调用,最安全的方法是使用内置的
callable()
函数任何 Python 对象都可以表现得像函数。为此,只需实现实例方法
__call__
。
函数内省
除了
__doc__
,函数对象还有很多属性。使用dir
函数可以探知factorial
具有下述属性:
1
2
3
4
5
6
7
8
9 dir(factorial)
['__annotations__', '__call__', '__class__', '__closure__', '__code__',
'__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__','__format__', '__ge__', '__get__', '__getattribute__', '__globals__',
'__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__',
'__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__']
>>>用户定义的函数的属性
名称 类型 说明 __annotations__
dict
参数和返回值的注解 __call__
method-wrapper
实现 ()
运算符;即可调用对象协议__closure__
tuple
函数闭包,即自由变量的绑定(通常是 None
)__code__
code
编译成字节码的函数元数据和函数定义体 __defaults__
tuple
形式参数的默认值 __get__
method-wrapper
实现只读描述符协议(参见第 20 章) __globals__
dict
函数所在模块中的全局变量 __kwdefaults__
dict
仅限关键字形式参数的默认值 __name__
str
函数名称 __qualname__
str
函数的限定名称,如 Random.choice
( 参阅PEP 3155)
__defaults__
、__code__
和__annotations__
属性,IDE 和框架使用它们提取关于函数签名的信息。
参数传递
args、*kwargs用法
1 | # *args是用来发送一个非键值对的可变数量的参数列表给一个函数 |
1 | # ** kwargs允许您将keyworded可变长度的参数传递给函数。如果要在函数中处理命名参数,则应使用** kwargs |
args、*kwargs用法
1 | def foo(*args, **kwargs): |
- *args`表示任何多个无名参数,它是一个tuple
**kwargs
表示关键字参数,它是一个dict- 同时使用
*args
和**kwargs
时,必须*args
参数列要在**kwargs
前
有限个参数
1 | # -*- coding: UTF-8 -*- |
可变参数
1 | # 当装饰器不知道业务函数到底有多少个参数时,用*args 来代替 |
函数注解
函数声明中的各个参数可以在
:
之后增加注解表达式。如果参数有默认值,注解放在参数名和=
号之间。如果想注解返回值,在)
和函数声明末尾的:
之间添加->
和一个表达式。
1
2
3
4
5
6
7 >from typing import List
>def func(num: int = 0, type: str = 'default') -> List[int]:
return [num, num]
>func(num=2)
>Out[3]: [2, 2]注解不会做任何处理,只是存储在函数的
__annotations__
属性(一个字典)Python 对注解所做的唯一的事情是,把它们存储在函数的
__annotations__
属性里。仅此而已,Python 不做检查、不做强制、不做验证,什么操作都不做。换句话说,注解对 Python 解释器没有任何意义。注解只是元数据,可以供 IDE、框架和装饰器等工具使用。标准库中还没有什么会用到这些元数据,唯有
inspect.signature()
函数知道怎么提取注解
signature
函数返回一个Signature
对象,它有一个return_annotation
属性和一个parameters
属性,后者是一个字典,把参数名映射到Parameter
对象上。每个Parameter
对象自己也有annotation
属性
inspect.signature
函数返回一个 inspect.Signature 对象,它有一个 parameters 属性,这是一个有序映射,把参数名和 inspect.Parameter 对象对应起来。各个 Parameter 属性也有自己的属性,例如 name 、 default 和 kind 。特殊的 inspect._empty 值表示没有默认值,考虑到 None 是有效的默认值(也经常这么做),而且这么做是合理的。
kind 属性的值是 _ParameterKind 类中的 5 个值之一,列举如下。
1
2
3
4
5
6
7
8
9
10 POSITIONAL_OR_KEYWORD
可以通过定位参数和关键字参数传入的形参(多数 Python 函数的参数属于此类)。
VAR_POSITIONAL
定位参数元组。
VAR_KEYWORD
关键字参数字典。
KEYWORD_ONLY
仅限关键字参数(Python 3 新增)。
POSITIONAL_ONLY
仅限定位参数;目前,Python 声明函数的句法不支持,但是有些使用 C 语言实现且不接受关键字参数的函数(如 divmod )支持。除了 name 、 default 和 kind , inspect.Parameter 对象还有一个 annotation (注解)属性,它的值通常是 inspect._empty
inspect.Signature 对象有个 bind 方法,它可以把任意个参数绑定到签名中的形参上,所用的规则与实参到形参的匹配方式一样。
函数式编程包
operator模块
Python 的目标不是变成函数式编程语言,但是得益于
operator
和functools
等包的支持,函数式编程风格也可以信手拈来。
reduce
在函数式编程中,经常需要把算术运算符当作函数使用。例如,不使用递归计算阶乘。求和可以使用
sum
函数,但是求积则没有这样的函数。我们可以使用reduce
operator
模块为多个算术运算符提供了对应的函数,从而避免编写lambda a, b: a*b
这种平凡的匿名函数
1
2
3
4
5 >from functools import reduce
>from operator import mul
>def fact(n):
return reduce(mul, range(1, n+1))
operator
模块中还有一类函数,能替代从序列中取出元素或读取对象属性的lambda
表达式:因此,itemgetter
和attrgetter
其实会自行构建函数。
itemgetter
itemgetter
的常见用途:根据元组的某个字段给元组列表排序。
itemgetter(1)
的作用与lambda fields: fields[1]
一样:创建一个接受集合的函数,返回索引位 1 上的元素。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 >>>> metro_data = [
>... ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
>... ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
>... ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
>... ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
>... ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
>... ]
>>>>
>>>> from operator import itemgetter
>>>> for city in sorted(metro_data, key=itemgetter(1)):
>... print(city)
>...
>('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833))
>('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889))
>('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
>('Mexico City', 'MX', 20.142, (19.433333, -99.133333))
>('New York-Newark', 'US', 20.104, (40.808611, -74.020386))
>d = {"a":1,"b":2,"c":3}
>itemgetter("a",'b')(d)
>Out[67]: (1, 2)
attrgetter
attrgetter
与itemgetter
作用类似,它创建的函数根据名称提取对象的属性。如果把多个属性名传给attrgetter
,它也会返回提取的值构成的元组。
1
2 >>>> for city in sorted(metro_areas, key=attrgetter('coord.lat')):
>... print(name_lat(city))
methodcaller
methodcaller
。它的作用与attrgetter
和itemgetter
类似,它会自行创建函数。methodcaller
创建的函数会在对象上调用参数指定的方法
1
2
3
4
5
6
7
8 >>>> from operator import methodcaller
>>>> s = 'The time has come'
>>>> upcase = methodcaller('upper')
>>>> upcase(s)
>'THE TIME HAS COME'
>>>> hiphenate = methodcaller('replace', ' ', '-')
>>>> hiphenate(s)
>'The-time-has-come'
其他模块
下面是
operator
模块中定义的部分函数(省略了以_
开头的名称,因为它们基本上是实现细节):Python 3.5 中增加了imatmul
和matmul
。
1
2
3
4
5
6
7
8
9 >>>> [name for name in dir(operator) if not name.startswith('_')]
>['abs', 'add', 'and_', 'attrgetter', 'concat', 'contains',
>'countOf', 'delitem', 'eq', 'floordiv', 'ge', 'getitem', 'gt',
>'iadd', 'iand', 'iconcat', 'ifloordiv', 'ilshift', 'imod', 'imul',
>'index', 'indexOf', 'inv', 'invert', 'ior', 'ipow', 'irshift',
>'is_', 'is_not', 'isub', 'itemgetter', 'itruediv', 'ixor', 'le',
>'length_hint', 'lshift', 'lt', 'methodcaller', 'mod', 'mul', 'ne',
>'neg', 'not_', 'or_', 'pos', 'pow', 'rshift', 'setitem', 'sub',
>'truediv', 'truth', 'xor']这 52 个名称中大部分的作用不言而喻。以
i
开头、后面是另一个运算符的那些名称(如iadd
、iand
等),对应的是增量赋值运算符(如+=
、&=
等)。如果第一个参数是可变的,那么这些运算符函数会就地修改它;否则,作用与不带i
的函数一样,直接返回运算结果以下表格显示了抽象运算是如何对应于 Python 语法中的运算符和
operator
模块中的函数的。
运算 语法 函数 加法 a + b
add(a, b)
字符串拼接 seq1 + seq2
concat(seq1, seq2)
包含测试 obj in seq
contains(seq, obj)
除法 a / b
truediv(a, b)
整除法 a // b
floordiv(a, b)
按位与 a & b
and_(a, b)
按位异或 a ^ b
xor(a, b)
按位取反 ~ a
invert(a)
按位或 a
|b
or_(a, b)
取幂 a ** b
pow(a, b)
一致标识 a is b
is_(a, b)
一致标识 a is not b
is_not(a, b)
索引赋值 obj[k] = v
setitem(obj, k, v)
索引删除 del obj[k]
delitem(obj, k)
索引取值 obj[k]
getitem(obj, k)
左移 a << b
lshift(a, b)
取模 a % b
mod(a, b)
乘法 a * b
mul(a, b)
矩阵乘法 a @ b
matmul(a, b)
取反(算术) - a
neg(a)
取反(逻辑) not a
not_(a)
正数 + a
pos(a)
右移 a >> b
rshift(a, b)
切片赋值 seq[i:j] = values
setitem(seq, slice(i, j), values)
切片删除 del seq[i:j]
delitem(seq, slice(i, j))
切片取值 seq[i:j]
getitem(seq, slice(i, j))
字符串格式化 s % obj
mod(s, obj)
减法 a - b
sub(a, b)
真值测试 obj
truth(obj)
比较 a < b
lt(a, b)
比较 a <= b
le(a, b)
相等 a == b
eq(a, b)
不等 a != b
ne(a, b)
比较 a >= b
ge(a, b)
比较 a > b
gt(a, b)
functools模块
说到高阶函数,这是函数式编程范式中很重要的一个概念,简单地说, 就是一个可以接受函数作为参数或者以函数作为返回值的函数,因为 Python 中函数是一类对象, 因此很容易支持这样的函数式特性。
functools 模块中函数只有
cmp_to_key
、partial
、reduce
、total_ordering
、update_wrapper
、wraps
、lru_cache
这几个:
partial
用于创建一个偏函数,它用一些默认参数包装一个可调用对象,返回结果是可调用对象,并且可以像原始对象一样对待,这样可以简化函数调用。
1 | from functools import partial |
partialmethod
partialmethod
是 Python 3.4 中新引入的装饰器,作用基本类似于partial
, 不过仅作用于方法(函数和方法的异同见上方)。
1 | class Cell(object): |
total_ordering
total_ordering
同样是 Python 2.7 中新增函数,用于简化比较函数的写法。如果你已经定义了__eq__
方法,以及__lt__
、__le__
、__gt__
或者__ge__()
其中之一, 即可自动生成其它比较方法。官方示例:
1 |
|
cmp_to_key
cmp_to_key
是 Python 2.7 中新增的函数,用于将比较函数转换为 key 函数, 这样就可以应用在接受 key 函数为参数的函数中。比如sorted()
、min()
、max()
、heapq.nlargest()
、itertools.groupby()
等。
1 | sorted(range(5), key=cmp_to_key(lambda x, y: y-x)) # [4, 3, 2, 1, 0] |
lru_cache
一个装饰器是在 Python3 中新加的,在 Python2 中如果想要使用可以安装第三方库
functools32
。该装饰器用于缓存函数的调用结果,对于需要多次调用的函数,而且每次调用参数都相同,则可以用该装饰器缓存调用结果,从而加快程序运行。
1 | from functools import lru_cache |
由于该装饰器会将不同的调用结果缓存在内存中,因此需要注意内存占用问题,避免占用过多内存,从而影响系统性能。
lru_cache
可以使用两个可选的参数来配置
maxsize
参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize
应该设为 2 的幂。typed
参数如果设为True
,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如1
和1.0
)区分开。顺便说一下,因为lru_cache
使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被lru_cache
装饰的函数,它的所有参数都必须是可散列的。
singledispatch
见本页-单分派泛函数singledispatch
wraps
使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring、name、参数列表
functools.wraps,wraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的 func 函数中,这使得装饰器里面的log函数也有和原函数test一样的元信息了说到“接受函数为参数,以函数为返回值”,在 Python 中最常用的当属装饰器了。 functools 库中装饰器相关的函数是
update_wrapper
、wraps
,还搭配WRAPPER_ASSIGNMENTS
和WRAPPER_UPDATES
两个常量使用,作用就是消除 Python 装饰器的一些负面作用。
1 | def log(func): |
1 | import functools |
update_wrapper
update_wrapper
的作用与wraps
类似,不过功能更加强大,换句话说,wraps
其实是update_wrapper
的特殊化,实际上wraps(wrapped)
相当于partial(update_wrapper, wrapped=wrapped, **kwargs)
。
1
2
3
4 def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return update_wrapper(wrapper, func)
装饰器(Dercrator)
用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景
用装饰器抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用严格来说,装饰器只是语法糖,装饰器的两大特性是:
- 能把被装饰的函数替换成其他函数
- 函数装饰器在加载模块时立即执行(Python 加载模块时),被装饰的函数只有在明确调用时运行,这突出了导入时和运行时的区别。
装饰器的4种类型:函数装饰函数、函数装饰类、类装饰函数、类装饰类
globals关键字
返回一个字典,表示当前的全局符号表。这个符号表始终针对当前模块(对函数或方法来说,是指定义它们的模块,而不是调用它们的模块)。
方式一
globals
函数帮助best_promo
自动找到其他可用的*_promo
函数方式二
1
2
3 promos = [globals()[name] for name in globals() ➊
if name.endswith('_promo') ➋
and name != 'best_promo'] ➌收集所有可用促销的另一种方法是,在一个单独的模块中保存所有策略函数,把
best_promo
排除在外。最大的变化是内省名为
promotions
的独立模块,构建策略函数列表。
1
2 promos = [func for name, func in
inspect.getmembers(promotions, inspect.isfunction)]
闭包
创建保有内部状态的函数,还有一种截然不同的方式——使用闭包。
在表达式中引用变量时,Python解释器将按如下顺序遍历各作用域,以解析该引用:
- 当前函数的作用域
- 任何外围作用域(例如,包含当前函数的其他函数)
- 包含当前代码的那个模块的作用域(也叫全局作用域,global scope)
- 内置作用域(也就是包含len及str等函数的那个作用域)
如果上面这些地方都没有定义过名称相符的变量,那就抛出NameError异常。
nonlocal语句清楚地表明: 如果在闭包内给该变量赋值,那么修改的其实是闭包外那个作用域中的变量。
这与global语句互为补充,global 用来表示对该变量的赋值操作,将会直接修改模块作用域里的那个变量。
ps:按照我们正常的认知,一个函数结束的时候,会把自己的临时变量都释放还给内存,之后变量都不存在了。一般情况下,确实是这样的。
但是闭包是一个特别的情况,外部函数发现,自己的临时变量会在将来的内部函数中用到,自己在结束的时候,返回内函数的同时,会把外函数的临时变量送给内函数绑定在一起。
所以外函数已经结束了,调用内函数的时候仍然能够使用外函数的临时变量。
global关键字
如果在函数中赋值时想让解释器把 b 当成全局变量,要使用 global 声明:
1
2
3
4
5
6 b = 6
def func(a):
global b
print(a)
print(b)
b = 9
自由变量
闭包
:只有涉及嵌套函数时才有闭包问题闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。
函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。
在
averager
函数中,series
是自由变量(free variable),指未在本地作用域中绑定的变量
averager
的闭包延伸到那个函数的作用域之外,包含自由变量series
的绑定只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量
nonlocal声明
但是对数字、字符串、元组等
不可变类型
来说,只能读取,不能更新。如果尝试重新绑定,例如
count = count + 1
,其实会隐式创建局部变量count
。这样,
count
就不是自由变量了,因此不会保存在闭包中,为了解决这个问题,Python 3 引入了nonlocal
声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。
如果为
nonlocal
声明的变量赋予新值,闭包中保存的绑定会更新。
1
2
3
4
5
6
7
8
9
10 def make_averager():
count, total = 0, 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager小结:外函数内部定义
可变类型
的变量可以在内函数使用,对数字、字符串、元组等不可变类型
来说,需要使用nonlocal
声明变量为自由变量,内函数才可以访问到。
函数装饰函数
装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装饰的函数本该返回的值,同时还会做些额外操作。
1 | def decorator(func): |
functools.wrap装饰器
inner的返回值要与func的一致,并且inner与func参数相同
为了不改变被装饰函数或类的性质,添加functools.wrap装饰器
1 | from functools import wraps |
带参数的装饰器(3层)
1 | from functools import wraps |
带有不定参数的装饰器
1 | # 带有不定参数的装饰器 |
多个装饰器
1 | # 一个函数需要加入很多功能,一个装饰器怕是搞不定,装饰器能支持多个嘛 |
函数装饰类
1 | def wrapClass(cls): |
- 定义
- 装饰器不仅可以是函数,还可以是类
- 相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点
- 像
__call__
这样前后都带下划线的方法在Python中被称为内置(魔法)方法。重载这些魔法方法一般会改变对象的内部行为
- 用法
- 让类的构造函数
__init__()
接受一个函数 - 重载call()并 返回一个函数
- 使用
@类
形式将装饰器附加到业务函数上
类装饰函数
1 | class ShowFunName(): |
类装饰类
1 | class ShowClassName(object): |
内置装饰器
命令行神器Click
1 | # -*- coding: utf-8 -* |
@property和@classmethod
1 | # @property |
functools模块的内置装饰器
更详细的见上方的functools模块
lru_cache做备忘
functools.lru_cache
是非常实用的装饰器,它实现了备忘(memoization)功能。这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。
LRU 三个字母是“Least Recently Used”的缩写,表明缓存不会无限制增长,一段时间不用的缓存会被扔掉。
生成第 n 个斐波纳契数这种慢速递归函数适合使用
lru_cache
1 | import functools |
除了优化递归算法之外,
lru_cache
在从 Web 中获取信息的应用中也能发挥巨大作用。特别要注意,
lru_cache
可以使用两个可选的参数来配置。它的签名是:
1 | functools.lru_cache(maxsize=128, typed=False) |
maxsize
参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize
应该设为 2 的幂。typed
参数如果设为True
,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如1
和1.0
)区分开。顺便说一下,因为
lru_cache
使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被lru_cache
装饰的函数,它的所有参数都必须是可散列的。
单分派泛函数singledispatch
PEP 443 — Single-dispatch generic functions
functools.singledispatch
是 Python 3.4 增加的,PyPI 中的singledispatch
包可以向后兼容 Python 2.6 到 Python 3.3。假设我们在开发一个调试 Web 应用的工具,我们想生成 HTML,显示不同类型的 Python 对象。
我们可能会编写这样的函数:
1
2
3
4
5 import html
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)这个函数适用于任何 Python 类型,但是现在我们想做个扩展,让它使用特别的方式显示某些类型。
str
:把内部的换行符替换为' \n'
;不使用,而是使用
int
:以十进制和十六进制显示数字list
:输出一个 HTML 列表,根据各个元素的类型进行格式化因为 Python 不支持重载方法或函数,所以我们不能使用不同的签名定义
htmlize
的变体,也无法使用不同的方式处理不同的数据类型。在 Python 中,一种常见的做法是把htmlize
变成一个分派函数,使用一串if/elif/elif
,调用专门的函数,如htmlize_str
、htmlize_int
,等等。这样不便于模块的用户扩展,还显得笨拙:时间一长,分派函数htmlize
会变得很大,而且它与各个专门函数之间的耦合也很紧密。Python 3.4 新增的
functools.singledispatch
装饰器可以把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。使用@singledispatch
装饰的普通函数会变成泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的一组函数如果根据多个参数选择专门的函数,那就是
多分派
了。
singledispatch
创建一个自定义的htmlize.register
装饰器,把多个函数绑在一起组成一个泛函数
1 | from functools import singledispatch |
❶
@singledispatch
标记处理object
类型的基函数。❷ 各个专门函数使用
@«base_function».register(«type»)
装饰。❸ 专门函数的名称无关紧要;
_
是个不错的选择,简单明了。❹ 为每个需要特殊处理的类型注册一个函数。
numbers.Integral
是int
的虚拟超类。❺ 可以叠放多个
register
装饰器,让同一个函数支持不同类型。只要可能,注册的专门函数应该处理抽象基类(如
numbers.Integral
和abc.MutableSequence
),不要处理具体实现(如int
和list
)。这样,代码支持的兼容类型更广泛。例如,Python 扩展可以子类化numbers.Integral
,使用固定的位数实现int
类型。
singledispatch
机制的一个显著特征是,你可以在系统的任何地方和任何模块中注册专门函数。如果后来在新的模块中定义了新的类型,可以轻松地添加一个新的专门函数来处理那个类型。
此外,你还可以为不是自己编写的或者不能修改的类添加自定义函数。
@singledispatch
不是为了把 Java 的那种方法重载带入 Python。在一个类中为同一个方法定义多个重载变体,比在一个函数中使用一长串if/elif/elif/elif
块要更好。但是这两种方案都有缺陷,因为它们让代码单元(类或函数)承担的职责太多。@singledispath
的优点是支持模块化扩展:各个模块可以为它支持的各个类型注册一个专门函数。
柯里化
柯里化(Currying)
将原来接受两个参数的函数变成新的接受一个参数的函数的过程。
新的函数返回一个以原有第二个参数为参数的函数。
1
2
3
4
5
6
7
8
9 ># pip install simplecurry -i https://pypi.tuna.tsinghua.edu.cn/simple
>from simplecurry import curried
>@curried
>def add2(a,b,c):
return c * a + b
>add2(2)(5)(8)
>>>> 21curry化最大的意义在于把多个参数的function等价转化成多个单参数function的级联,这样所有的函数就都统一了,方便做lambda演算。 在scala里,curry化对类型推演也有帮助,scala的类型推演是局部的,在同一个参数列表中后面的参数不能借助前面的参数类型进行推演,curry化以后,放在两个参数列表里,后面一个参数列表里的参数可以借助前面一个参数列表里的参数类型进行推演。这就是为什么 foldLeft这种函数的定义都是curry的形式。
案例
时间装饰器
计算函数运行时间
1 | from functools import wraps |