函数基础

函数定义

在 Python 中,函数是一等对象,编程语言理论家把“一等对象”定义为满足下述条件的程序实体:

  • 在运行时创建
  • 能赋值给变量或数据结构中的元素
  • 能作为参数传给函数
  • 能作为函数的返回结果

有了一等函数,就可以使用函数式风格编程。

函数式编程的特点之一是使用高阶函数——接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher-order function)。

在函数式编程范式中,最为人熟知的高阶函数有 mapfilterreduceapply

在 Python 3 中,mapfilter 还是内置函数,但是由于引入了列表推导和生成器表达式,它们变得没那么重要了。

sumreduce 的通用思想是把某个操作连续应用到序列的元素上,累计之前的结果,把一系列值归约成一个值。

allany 也是内置的归约函数。

all(iterable): 如果 iterable 的每个元素都是真值,返回 Trueall([]) 返回 True

any(iterable): 只要 iterable 中有元素是真值,就返回 Trueany([]) 返回 False

lambda 关键字在 Python 表达式内创建匿名函数。

lambda 函数的定义体中不能赋值,也不能使用 whiletry 等 Python 语句。

lambda 句法只是语法糖:与 def 语句一样,lambda 表达式会创建函数对象。

函数和方法

从几个维度来介绍下python中函数和方法的区别

分类角度分析

(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
2
3
4
5
6
7
8
9
10
11
12
13
14
# *args是用来发送一个非键值对的可变数量的参数列表给一个函数

def test_var_args(f_arg, *argv):
print("first normal arg:", f_arg)
for arg in argv:
print("another arg through *argv:", arg)

test_var_args('yasoob', 'python', 'eggs', 'test')


first normal arg: yasoob
another arg through *argv: python
another arg through *argv: eggs
another arg through *argv: test
1
2
3
4
5
6
7
8
9
10
# ** kwargs允许您将keyworded可变长度的参数传递给函数。如果要在函数中处理命名参数,则应使用** kwargs 
def greet_me(**kwargs):
for key, value in kwargs.items():
print("{0} = {1}".format(key, value))
# 必须含有key
greet_me(name="yasoob",age="19",sex="girl")

name = yasoob
age = 19
sex = girl

args、*kwargs用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def foo(*args, **kwargs):
print('args = ', args)
print('kwargs = ', kwargs)
print('---------------------------------------')

if __name__ == '__main__':
foo(1,2,3,4)
foo(a=1,b=2,c=3)
foo(1,2,3,4, a=1,b=2,c=3)
foo('a', 1, None, a=1, b='2', c=3)

args = (1, 2, 3, 4)
kwargs = {}
---------------------------------------
args = ()
kwargs = {'a': 1, 'b': 2, 'c': 3}
---------------------------------------
args = (1, 2, 3, 4)
kwargs = {'a': 1, 'b': 2, 'c': 3}
---------------------------------------
args = ('a', 1, None)
kwargs = {'a': 1, 'b': '2', 'c': 3}
---------------------------------------
  • *args`表示任何多个无名参数,它是一个tuple
  • **kwargs表示关键字参数,它是一个dict
  • 同时使用*args**kwargs时,必须*args参数列要在**kwargs

有限个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -*- coding: UTF-8 -*- 
def log(func):
def wrapper(a,b):
print("call test(%d,%d)" %(a,b))
return func(a,b)
return wrapper

@log
def test(a,b):
print("sum = %d" % (a+b))

test(2,4)


call test(24)
sum = 6

可变参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 当装饰器不知道业务函数到底有多少个参数时,用*args 来代替
def log(func):
def wrapper(*args):
print("call test()" )
return func(*args)
return wrapper

@log
def test(a,b,c):
print("sum = %d" % (a+b+c))

test(2,4,5)


call test()
sum = 11

函数注解

函数声明中的各个参数可以在 : 之后增加注解表达式。如果参数有默认值,注解放在参数名和 = 号之间。如果想注解返回值,在 ) 和函数声明末尾的 : 之间添加 -> 和一个表达式。

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 的目标不是变成函数式编程语言,但是得益于 operatorfunctools 等包的支持,函数式编程风格也可以信手拈来。

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 表达式:因此,itemgetterattrgetter 其实会自行构建函数。

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

attrgetteritemgetter 作用类似,它创建的函数根据名称提取对象的属性。如果把多个属性名传给 attrgetter,它也会返回提取的值构成的元组。

1
2
>>>> for city in sorted(metro_areas, key=attrgetter('coord.lat')):
>... print(name_lat(city))

methodcaller

methodcaller。它的作用与 attrgetteritemgetter 类似,它会自行创建函数。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 中增加了 imatmulmatmul

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 开头、后面是另一个运算符的那些名称(如 iaddiand 等),对应的是增量赋值运算符(如 +=&= 等)。如果第一个参数是可变的,那么这些运算符函数会就地修改它;否则,作用与不带 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

Python functools 模块

说到高阶函数,这是函数式编程范式中很重要的一个概念,简单地说, 就是一个可以接受函数作为参数或者以函数作为返回值的函数,因为 Python 中函数是一类对象, 因此很容易支持这样的函数式特性。

functools 模块中函数只有 cmp_to_keypartialreducetotal_orderingupdate_wrapperwrapslru_cache 这几个:

partial

用于创建一个偏函数,它用一些默认参数包装一个可调用对象,返回结果是可调用对象,并且可以像原始对象一样对待,这样可以简化函数调用。

1
2
3
4
5
from functools import partial
def add(x, y):
return x + y
add_y = partial(add, 3) # add_y 是一个函数
add_y(4) # 结果是 7

partialmethod

partialmethod是 Python 3.4 中新引入的装饰器,作用基本类似于 partial, 不过仅作用于方法(函数和方法的异同见上方)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Cell(object):
def __init__(self):
self._alive = False
@property
def alive(self):
return self._alive
def set_state(self, state):
self._alive = bool(state)
set_alive = partialmethod(set_state, True)
set_dead = partialmethod(set_state, False)

c = Cell()
c.alive # False
c.set_alive()
c.alive # True

total_ordering

total_ordering 同样是 Python 2.7 中新增函数,用于简化比较函数的写法。如果你已经定义了 __eq__ 方法,以及 __lt____le____gt__ 或者 __ge__() 其中之一, 即可自动生成其它比较方法。官方示例:

1
2
3
4
5
6
7
8
9
10
@total_ordering
class Student:
def __eq__(self, other):
return ((self.lastname.lower(), self.firstname.lower()) ==
(other.lastname.lower(), other.firstname.lower()))
def __lt__(self, other):
return ((self.lastname.lower(), self.firstname.lower()) <
(other.lastname.lower(), other.firstname.lower()))

dir(Student) # ['__doc__', '__eq__', '__ge__', '__gt__', '__le__', '__lt__', '__module__']

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from functools import lru_cache

@lru_cache(None)
def add(x, y):
print("calculating: %s + %s" % (x, y))
return x + y

print(add(1, 2))
print(add(1, 2)) # 直接返回缓存信息
print(add(2, 3))

calculating: 1 + 2
3
3
calculating: 2 + 3
5

由于该装饰器会将不同的调用结果缓存在内存中,因此需要注意内存占用问题,避免占用过多内存,从而影响系统性能。

lru_cache 可以使用两个可选的参数来配置

maxsize 参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize 应该设为 2 的幂。typed 参数如果设为 True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 11.0)区分开。顺便说一下,因为 lru_cache 使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被 lru_cache 装饰的函数,它的所有参数都必须是可散列的

singledispatch

见本页-单分派泛函数singledispatch

wraps

使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring、name、参数列表
functools.wraps,wraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的 func 函数中,这使得装饰器里面的log函数也有和原函数test一样的元信息了

说到“接受函数为参数,以函数为返回值”,在 Python 中最常用的当属装饰器了。 functools 库中装饰器相关的函数是 update_wrapperwraps,还搭配 WRAPPER_ASSIGNMENTSWRAPPER_UPDATES 两个常量使用,作用就是消除 Python 装饰器的一些负面作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def log(func):
def wrapper():
print("call test()")
return func()
return wrapper

@log
def test():
print("this is what I want")

test()
print(test.__name__)

call test()
this is what I want
wrapper
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import functools

def log(func):
@functools.wraps(func)
def wrapper():
print("call test()")
return func()
return wrapper

@log
def test():
print("this is what I want")

test()
print(test.__name__)


call test()
this is what I want
test

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 装饰器的五个理由

Python装饰器

用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景
用装饰器抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用

严格来说,装饰器只是语法糖,装饰器的两大特性是:

  1. 能把被装饰的函数替换成其他函数
  2. 函数装饰器在加载模块时立即执行(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解释器将按如下顺序遍历各作用域,以解析该引用:

  1. 当前函数的作用域
  2. 任何外围作用域(例如,包含当前函数的其他函数)
  3. 包含当前代码的那个模块的作用域(也叫全局作用域,global scope)
  4. 内置作用域(也就是包含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),指未在本地作用域中绑定的变量

img

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def decorator(func):
def inner(*args, **kwargs):
print('before...........')
res = func(*args, **kwargs)
print('after............')
return res
return inner

@decorator
def run():
print('run...............')
return 0

if __name__ == "__main__":
run()
run.__name__
# 此时decorator叫做装饰器
------------------------------------------
before...........
run...............
after............
inner
---------------------

functools.wrap装饰器

inner的返回值要与func的一致,并且inner与func参数相同

为了不改变被装饰函数或类的性质,添加functools.wrap装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from functools import wraps

def decorator(func):
@wraps(func)
def inner():
print('before...........')
res = func()
print('after............')
return res
return inner

@decorator
def run():
print('run...............')
return 0

if __name__ == "__main__":
run()
print(run.__name__)
------------------------------------------
before...........
run...............
after............
run
---------------------

带参数的装饰器(3层)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from functools import wraps
from datetime import datetime

def start():
return datetime.now()
def end():
return datetime.now()
def Filter(start_time, end_time):
def decorator(func):
@wraps(func)
def inner(*args, **kwargs):
s = start_time()
res = func(*args,**kwargs)
e = end_time()
print("耗时 {} s".format((e-s).total_seconds()))
return res
return inner
return decorator

@Filter(start, end)
def run():
for i in range(2):
for j in range(3):
print(j)
return 0
if __name__ == "__main__":
run()

0
1
2
0
1
2
耗时 0.003987 s

带有不定参数的装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 带有不定参数的装饰器
# 拓展的函数好多可是有参数,有的参数还是个数不定的那种
import time

def deco(func):
def wrapper(*args, **kwargs):
startTime = time.time()
func(*args, **kwargs)
endTime = time.time()
msecs = (endTime - startTime) * 1000
print("time is %d ms" % msecs)
return wrapper

@deco
def func(a, b):
print("hello,here is a func for add:")
time.sleep(1)
print("result is %d" % (a + b))

@deco
def func2(a, b, c):
print("hello,here is a func for add:")
time.sleep(1)
print("result is %d" % (a + b + c))

if __name__ == '__main__':
f = func
func2(3, 4, 5)
f(3, 4)

hello,here is a func for add:
result is 12
time is 1000 ms
hello,here is a func for add:
result is 7
time is 1000 ms

多个装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 一个函数需要加入很多功能,一个装饰器怕是搞不定,装饰器能支持多个嘛
# 多个装饰器执行的顺序就是从最后一个装饰器开始,执行到第一个装饰器,再执行函数本身。
# 多个装饰器
import time

def deco01(func):
def urapper(*args, **kwargs):
print("this is decoe1")
startTime = time.time()
func(*args, **kwargs)
endTime = time.time()
msecs = (endTime - startTime) * 1000
print("time is %d ms" % msecs)
print("decoe1 end here")
return urapper

def deco02(func):
def wrapper(*args, **kwargs):
print("this is decoe2")
func(*args, **kwargs)
print("decoe2 end here")
return wrapper

@deco01
@deco02
def func(a, b):
print("hello,here is a func for add:")
time.sleep(1)
print("result is %d" % (a + b))

if __name__ == '__main__':
f = func
f(3, 4)

this is decoe1
this is decoe2
hello,here is a func for add:
result is 7
decoe2 end here
time is 1003 ms
decoe1 end here

函数装饰类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def wrapClass(cls):
def inner(a):
print('class name:', cls.__name__)
return cls(a)
return inner

@wrapClass
class Foo():
def __init__(self, a):
self.a = a

def fun(self):
print('self.a =', self.a)

m = Foo('xiemanR')
m.fun()

class name: Foo
self.a = xiemanR
  1. 定义
  • 装饰器不仅可以是函数,还可以是类
  • 相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点
  • __call__这样前后都带下划线的方法在Python中被称为内置(魔法)方法。重载这些魔法方法一般会改变对象的内部行为
  1. 用法
  • 让类的构造函数__init__()接受一个函数
  • 重载call()并 返回一个函数
  • 使用@类形式将装饰器附加到业务函数上

类装饰函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ShowFunName():
def __init__(self, func):
self._func = func

def __call__(self, a):
print('function name:', self._func.__name__)
return self._func(a)
@ShowFunName
def Bar(a):
return a
print(Bar('xiemanR'))

function name: Bar
xiemanR

类装饰类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ShowClassName(object):
def __init__(self, cls):
self._cls = cls
def __call__(self, a):
print('class name:', self._cls.__name__)
return self._cls(a)
@ShowClassName
class Foobar(object):
def __init__(self, a):
self.value = a
def fun(self):
print(self.value)
a = Foobar('xiemanR')
a.fun()

class name: Foobar
xiemanR

内置装饰器

命令行神器Click

  1. 命令行神器 Click教程A篇
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -*- coding: utf-8 -*
import click
@click.command()
@click.option('--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name', help='The person to greet.')
def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times."""
for x in range(count):
click.echo('Hello %s!' % name)
if __name__ == '__main__':
hello()

Q:\pyCharmWS>python ./tempTest.py --count=3 --name=Ethan
Hello Ethan!
Hello Ethan!
Hello Ethan!

@property和@classmethod

  1. python中常用的内置装饰器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# @property
# 使调用类中的方法像引用类中的字段属性一样。被修饰的特性方法,内部可以实现处理逻辑,但对外提供统一的调用方式。遵循了统一访问的原则。
# @classmethod
# 类方法的第一个参数是类,将类本身作为操作的方法。类方法被哪个类调用,就传入哪个类作为第一个参数进行操作。
# 注意,静态方法和类方法是为类操作准备的。虽然通过实例也能调用,但是不建议

# -*- coding: utf-8 -*-
# coding: utf-8
class TestClass:
name = "test"
def __init__(self, name):
self.name = name
@property
def sayHello(self):
print("hello", self.name)
@staticmethod
def fun(self, x, y):
return x + y
cls = TestClass("felix")
print(f"通过实例引用属性: {cls.name}")
print(f"像引用属性一样调用@property修饰的方法: {cls.sayHello}")
print(f"类名直接引用静态方法: {TestClass.fun(None, 2, 3)}")

通过实例引用属性: felix
hello felix
像引用属性一样调用@property修饰的方法: None
类名直接引用静态方法: 5

functools模块的内置装饰器

更详细的见上方的functools模块

lru_cache做备忘

functools.lru_cache 是非常实用的装饰器,它实现了备忘(memoization)功能。

这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。

LRU 三个字母是“Least Recently Used”的缩写,表明缓存不会无限制增长,一段时间不用的缓存会被扔掉。

生成第 n 个斐波纳契数这种慢速递归函数适合使用 lru_cache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import functools
from clockdeco import clock

@functools.lru_cache() # ➊
@clock # ➋
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)

if __name__=='__main__':
print(fibonacci(6))

# 这样一来,执行时间减半了,而且 n 的每个值只调用一次函数
[0.00000119s] fibonacci(0) -> 0
[0.00000119s] fibonacci(1) -> 1
[0.00010800s] fibonacci(2) -> 1
[0.00000787s] fibonacci(3) -> 2
[0.00016093s] fibonacci(4) -> 3
[0.00001216s] fibonacci(5) -> 5
[0.00025296s] fibonacci(6) -> 8

除了优化递归算法之外,lru_cache 在从 Web 中获取信息的应用中也能发挥巨大作用。

特别要注意,lru_cache 可以使用两个可选的参数来配置。它的签名是:

1
functools.lru_cache(maxsize=128, typed=False)

maxsize 参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize 应该设为 2 的幂。typed 参数如果设为 True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 11.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_strhtmlize_int,等等。这样不便于模块的用户扩展,还显得笨拙:时间一长,分派函数 htmlize 会变得很大,而且它与各个专门函数之间的耦合也很紧密。

Python 3.4 新增的 functools.singledispatch 装饰器可以把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。使用 @singledispatch 装饰的普通函数会变成泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的一组函数

如果根据多个参数选择专门的函数,那就是多分派了。

singledispatch 创建一个自定义的 htmlize.register 装饰器,把多个函数绑在一起组成一个泛函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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>'

@singledispatch 标记处理 object 类型的基函数。

❷ 各个专门函数使用 @«base_function».register(«type») 装饰。

❸ 专门函数的名称无关紧要;_ 是个不错的选择,简单明了。

❹ 为每个需要特殊处理的类型注册一个函数。numbers.Integralint 的虚拟超类。

❺ 可以叠放多个 register 装饰器,让同一个函数支持不同类型。

只要可能,注册的专门函数应该处理抽象基类(如 numbers.Integralabc.MutableSequence),不要处理具体实现(如 intlist)。这样,代码支持的兼容类型更广泛。例如,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)

>>>> 21

curry化最大的意义在于把多个参数的function等价转化成多个单参数function的级联,这样所有的函数就都统一了,方便做lambda演算。 在scala里,curry化对类型推演也有帮助,scala的类型推演是局部的,在同一个参数列表中后面的参数不能借助前面的参数类型进行推演,curry化以后,放在两个参数列表里,后面一个参数列表里的参数可以借助前面一个参数列表里的参数类型进行推演。这就是为什么 foldLeft这种函数的定义都是curry的形式。

案例

时间装饰器

计算函数运行时间

1
2
3
4
5
6
7
8
9
10
11
12
from functools import wraps

# 作为装饰器使用,返回函数执行需要花费的时间
def time_this_function(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f'{func.__name__}, 耗时{round(end - start, 4)}s')
return result
return wrapper