这篇文章需要看 ipynb 文件,这个 html 没有转换全,而且还有很多格式不对
在 Python 中,函数是一等对象。编程语言理论家把 “一等对象” 定义为满足下面条件的程序实体:
- 在运行时创建
- 能赋值给变量或者数据结构中的元素
- 能作为参数传给函数
- 能作为函数返回结果
Python 中,整数、字符串和字典都是一等对象。人们经常将 ”把函数视作一等对象“ 简称为 ”一等函数“。这样说并不完美,似乎表明函数是一等对象中的特殊群体。在 Python 中,所有的函数都是一等对象
把函数视作对象¶
下面的例子表明 Python 函数是对象,创建了一个函数,然后调用它,读取它的 __doc__ 属性,并确定函数对象本身是 function 类的实例
def factorial(n):
'''return n!'''
return 1 if n < 2 else n * factorial(n - 1)
factorial(42)
factorial.__doc__
type(factorial)
下面展示了函数对象的的 ”一等“ 本性。我们可以把 factorial 函数赋值给变量 fact,然后通过变量名调用。我们还能把它当做参数传给 map 函数。
fact= factorial
fact(5)
map(factorial, range(11))
list(map(factorial, range(11)))
有了一等函数,就可以使用函数式风格编程,函数式编程的特点之一就是使用高阶函数
高阶函数¶
接收函数为参数,或者把函数作为返回结果的函数是高阶函数,map 就是一个例子。此外内置函数 sorted 也是。可选的 key 参数用于提供一个函数,它会应用到各个元素上进行排序,看下面例子
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key = len)
任何单参数函数都能作为 key 参数的值。例如为了创建押韵词典,可以把各个单词反过来拼写,然后排序
def reverse(word):
return word[::-1]
reverse('testing')
sorted(fruits, key = reverse)
在函数式编程中,最让人熟知的高阶函数有 map,filter,reduce 和 apply。apply 在 Python 2.3 中被标记过时,Python 3 中移除了,如果想用不定量的参数调用函数,可以编写 fn(*args, **keywords)
map,filter 和 reduce 的替代品
在 Python 3 中 map 和 filter 还是内置函数,但是由于引入了列表推导式和生成器表达式,它们变得没那么重要。因为完全可以替代它们。而且使用更方便:
list(map(fact, range(6)))
[fact(n) for n in range(6)]
list(map(factorial, filter(lambda n: n % 2, range(6)))) # 计算 6 以内的奇数的阶乘
[factorial(n) for n in range(6) if n % 2]
在 Python 2 中,reduce 是内置函数,Python 3 中放到了 functools 模块中,这个函数最常用于求和,自从 2003 年发布的 Python 2.3 开始,最好使用内置的 sum 函数。可读性和效率都更好
from functools import reduce
from operator import add
reduce(add, range(100))
sum(range(100))
sum 和 reduce 的通用思想是把某个操作连续应用到序列的元素上,累计之前的结果,把一系列值归约成一个。
all 和 any 也是内置归约函数
all(iterable)
- 如果 iterable 每个元素都为真,返回 True,all([]) 返回 True
any(iterable)
- 如果 iterable 有一个元素为真,返回 True any([]) 返回 False
第 10 章将会深入讲解 reduce 函数
匿名函数¶
lambda 关键字是在 Python 表达式创建匿名函数。
然而,Python 简单的语法限制了 lambda 函数的定义体只能用于纯表达式。换句话说,lambda 函数的定义体中不能赋值,也不能使用 while和 try 等 Python 语句。
下面用 lambda 重写了反向排序的例子,这样省去了 reverse 函数:
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key = lambda word: word[::-1])
除了作为参数传给高阶函数之外,Python 很少使用匿名函数,由于语法上的限制,非平凡的 lambda 要么非常难读,要么无法写出
可调用对象¶
除了用户自定义函数,调用运算符(即())还可以应用到其他对象上。如果想判对象能否被调用,可以使用内置的 callable() 函数。Python 数据模型文档列出了 7 种可调用对象:
用户定义的函数
- 使用 def 或 lambda 表达式创建
内置函数
- 使用 C 语言实现的函数,如 len 或 time.strftime
内置方法
- 使用 C 语言实现的方法,例如 dict.get
方法
- 在累的定义体中定义的函数
类
- 调用类时会运行类的
__new__方法创建一个实例,然后运行__init__方法,初始化实例,最后把实例返回调用方。因为 Python 没有 new 运算符,所以调用类相当于调用函数。(通常,调用类会创建那个类的实例,不过覆盖__new__方法的话,也可能出现其它行为。 19 章会看到一个例子
- 调用类时会运行类的
类的实例
- 如果类定义了
__call__方法,那么它的实例也可以作为函数调用
- 如果类定义了
生成器函数
- 使用 yield 关键字的函数或方法。调用生成器函数返回的是生成器对象
Python 中有多重可调用类型,最安全的判断一个对象能否被调用是使用内置的 callable() 方法
abs, str, 13
[callable(obj) for obj in (abs, str, 13)]
用户定义的可调用类型¶
不仅 Python 中的函数是对象,Python 中的对象也可以是函数,只要实现了 __call__ 方法。 下面展示了一个接收可迭代对象然后在本地创建一个副本打乱顺序的脚本
import random
class BingoCage:
def __init__(self, items):
self._items = list(items)
random.shuffle(self._items)
def pick(self):
try:
return self._items.pop()
except IndexError:
raise LookupError('pick from empty BingoCage')
def __call__(self):
return self.pick()
bingo = BingoCage(range(3))
bingo.pick()
bingo.pick()
callable(bingo)
实现 __call__ 方法的类是创建函数类对象的简便方式,此时必须在内部维护一个状态,让其被多次调用得到预期结果,例如上面的 _items,装饰器就是这样。装饰器必须是函数,而且有时要在多次调用之间 ”记住“ 某些事 【例如备忘(memoization),即缓存消耗大的计算结果,供后面使用】。
创建保有内部状态的函数,还有一种截然不同的方式 -- 使用闭包。闭包和装饰器在第 6 章讨论。
函数内省¶
除了 __doc__,函数对象还有很多属性。使用 dir 函数可以探知 factorial 具有以下属性:
dir(factorial)
其中大多数属性是 Python 共有的,这里讨论把函数视作对象相关的几个属性,先从 __dict__ 开始
与用户定义的常规类一样,函数使用 __dict__ 属性存储用户赋予它的属性。这相当于一种基本形式的注解。一般来说,为函数随意赋予属性不是很常见的做法,但是 Django 框架就这么做了。Django 文档中举了下面一个实例,把 short_description 属性赋予一个方法,Django 管理后台使用这个方法时,在记录列表中会出现指定的描述文本:
def upper_case_name(obj):
return ("%s %s" % (obj.first_name, obj.last_name)).upper()
upper_case_name.short_description = 'Customer name'
下面重点说说函数专有而用户定义一般对象没有的属性。计算两个属性集合的差集便能得到函数专有的属性列表:
class C: pass
obj = C()
def func(): pass
sorted(set(dir(func)) - set(dir(obj)))
本文后面会讨论 __defaults__、 __code__、 __annotations__ 属性,IDE 框架使用它们提取关于函数签名信息。但是为了深入了解这些属性,我们首先要讨论 Python 为声明函数形参和传入实参提供的强大语法
从定位参数到仅限关键字参数¶
Python 最好的特性之一就是提供了非常灵活的参数处理机制,而 Python 3 中进一步提供了仅限关键字的参数(keyword-only argument)。与之密切相关的是,调用函数时使用 * 和 ** ”展开” 迭代对象,映射到单个参数。下面代码展示了这些特性。
这段代码 tag 函数用来生成 HTML 标签,使用名为 cls 的关键字传入 “class” 属性,这是一种变通方法,因为 “class” 是 Python 的关键字
def tag(name, *content, cls = None, **attrs):
'''生成一个或多个 HTML 标签'''
if cls is not None:
attrs['class'] = cls
if attrs:
attr_str = ''.join(' %s="%s"' % (attr, value)
for attr, value
in sorted(attrs.items()))
else:
attr_str = ''
if content:
return '\n'.join('<%s%s>%s</%s>' %
(name, attr_str, c, name) for c in content)
else:
return '<%s%s />' % (name, attr_str)
tag('br')
#'<br />'
print(tag('p', 'hello', 'world'))
#<p>hello</p>
#<p>world</p>
tag('p', 'hello', id = 33) # tag 函数签名中没有明确指定名称的关键字会被 **attrs 捕获,存入一个字典
#'<p id="33">hello</p>'
print(tag('p', 'hello', 'world', cls='sidebar')) # cls 只能作为关键字传入
tag(content = 'testing', name = "img") # 调用 tag 时,即使第一个定位参数也能作为关键字参数传入
#'<img content="testing" />'
my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
'src': 'sunset.jpg', 'cls': 'framed'}
tag(**my_tag) #字典中所有元素作为单个参数传入,同名键会自动对应的具名参数上,剩下的被 **attrs 捕获
#'<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />'
仅限关键字参数是 Python 3 新增的特性。在上面的例子,cls 参数只能通过关键字参数指定,它一定不会捕获未命名的定位参数。定义函数时如果想指定仅限关键字参数,要把它们放到前面带有 * 的参数后面。如果不想支持数量不定的定位参数,但想支持仅限关键字参数,在签名中放一个 * ,如下所示:
def f(a, *, b):
return a, b
f(1, b=2)
获取关于参数的信息¶
HTTP 微框架 Bobo 有个使用函数内省的好例子。下面是对 Bobo 教程中的 “Hello world” 应用的改编,说明了内省如何使用:
import bobo
@bobo.query('/')
def hello(person):
return 'Hello %s!' % person
bobo.query 装饰器把一个普通的函数(hello)与框架的请求处理机制集成起来了,装饰器会在第 7 章讨论,这不是这个示例的关键。这里的关键是,Bobo 会内省 hello 函数,发现它需要一个名为 person 的参数,然后从请求中获取那个名称对应的参数,将其传给 hello 函数,因此程序员不用触碰请求对象
将上面代码存成 hello.py 然后使用 bobo -f hello.py 命令,在浏览器访问 http://localhost:8080 看到的消息是:
Missing form variable person
HTTP 状态码是 403,这是因为 Bobo 知道调用 hello 函数必须传入 person 参数,但是在请求中找不到同名参数。下面代码在 shell 会话中使用 curl 展示了这一行为
kaka@kaka-ubuntu:~/blog/content/fluent_python$ curl -i http://localhost:8080
HTTP/1.0 403 Forbidden
Date: Tue, 30 May 2017 06:26:21 GMT
Server: WSGIServer/0.2 CPython/3.5.3
Content-Type: text/html; charset=UTF-8
Content-Length: 103
<html>
<head><title>Missing parameter</title></head>
<body>Missing form variable person</body>
</html>
但是如果访问 http://localhost:8080/?person=Kaka 响应就会变成 'Hello Kaka!'
kaka@kaka-ubuntu:~/blog/content/fluent_python$ curl -i http://localhost:8080/?person=Kaka
HTTP/1.0 200 OK
Date: Tue, 30 May 2017 06:28:40 GMT
Server: WSGIServer/0.2 CPython/3.5.3
Content-Type: text/html; charset=UTF-8
Content-Length: 11
Hello Kaka!
Bobo 怎么知道函数需要哪个参数呢?又怎么知道参数有没有默认值呢?
函数对象有一个 __default__ 属性,它的值是一个元组,里面保存着定位参数和关键字参数的默认值。仅限关键字参数的默认值在 __kwdefaults__ 属性中。然而,参数的名称在 __code__ 属性中,他的值是一个 code 对象的引用,自身也有很多属性。
为了说明这些属性的用途,下面在 clip.py 模块中定义 clip 函数,如下所示,然后审查它
def clip(text, max_len = 80):
'''
在 max_len 前面或后面的第一个空格处截断文本
'''
end = None
if len(text) > max_len:
# rfind 方法返回被查找字符串最后一次出现的索引,没有出现返回 -1
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None: #没找到空格
end = len(text)
return text[:end].rstrip()
我们看看这个函数的 __defaults__、 __code__.co_varnames、 __code__.co_argcount 的值
clip.__defaults__
clip.__code__
clip.__code__.co_varnames
clip.__code__.co_argcount
可以看出,这种显示信息方式不是很方便,参数名称在 clip.__code__.co_varnames中,不过里面还有函数定义体中的局部变量,因此,函数名称是前 N 个字符串,N 的值由 clip.__code__.co_argcount 决定。顺便说一下,这里不包含前缀为 * 或 ** 的变长参数。参数的默认值只能通过它们在 __defaults__ 元组中的位置确定,因此要从后向前扫描才能把参数和默认值对应。在这里有两个参数,只有一个默认值 80,它属于最后一个参数 max_len,这有违常理
幸好,我们有更好的方式 -- 使用 inspect 模块
from inspect import signature
sig = signature(clip)
sig
str(sig)
for name, param in sig.parameters.items():
print(param.kind, ':', name, '=', param.default)
这样看起来就好多了, inspect.signature 函数返回一个 inspect.Signature 对象,它有一个 parameters 属性,这是一个有序映射,把参数名和 inspect.Parameter 对象对应起来。各个 Parameter 属性也有自己的属性,例如 name, default 和 kind。特殊的 inspect.empty 表示没有默认值,考虑到 None 是有效的默认值,这么做是合理的。
kind 属性的值是 _ParameterKind 类中的 5 个值之一,列举如下:
- POSITIONAL_OR_KEYWORD: 可以通过定位参数和关键字参数传入的形参(多数 Python 函数的参数属于此类)
- VAR_POSITIONAL: 定位参数元组
- KEYWORD_ONLY: 关键字参数字典
- KEYWORD_ONLY: 仅限关键字参数(Python 3 新增)
- POSITIONAL_ONLY: 仅限定位参数,目前 Python 声明函数的语法不支持,但是有些使用 C 语言实现且不接受关键字参数的函数(如 divmod)支持
除了 name、default 和 kind,inspect.Parameter 对象还有一个 annotation(注解)属性,它的值通常是 inspect._empty,但是可能包含 Python 3 新的注解语法提供的函数签名元数据,会在后面讨论
inspect.Signature 对象有一个 bind 方法,它可以把任意个参数绑定到签名函数形参上,所用的规则与实参到形参的匹配方式一样。框架可以使用这个方法在真正调用函数前验证参数。
下面是 把上面 tag 函数的签名绑定到一个参数字典上
import inspect
sig = inspect.signature(tag) # 获取 tag 函数签名
my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
'src': 'sunset.jpg', 'cls': 'framed'}
bound_args = sig.bind(**my_tag) # 把一个字典传给 bind 方法
bound_args # 得到一个 BoundArguments 对象
for name, value in bound_args.arguments.items(): #迭代 bound_args.arguments 中的元素,显示参数的名称和值
print(name, '=', value)
del my_tag['name']
bound_args = sig.bind(**my_tag) # 报错,缺少 name 参数
这个例子在 inspect 模块帮助下,展示了 Python 数据模型把实参绑定给函数调用中形参机制,这与解释器使用的机制相同
函数注解¶
Python 3 提供了一种语法,用于为函数声明中的参数和返回值附加元数据,下面例子是 clip 函数添加注解的版本,二者唯一区别在第一行
def clip(text:str, max_len: 'int > 0' = 80) -> str:
'''
在 max_len 前面或后面的第一个空格处截断文本
'''
end = None
if len(text) > max_len:
# rfind 方法返回被查找字符串最后一次出现的索引,没有出现返回 -1
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None: #没找到空格
end = len(text)
return text[:end].rstrip()
在函数声明中各个参数可以在 : 后面增加注解表达式。如果参数有默认值,注解放在参数名和 = 之间。如果想注解返回值,在 ) 和函数末尾添加 -> 和一个表达式。那个表达式可以是任何类型。注解中最常用的类型是类(如 str 或 int)和字符串(如 'int > 0')
注解不会做任何处理,只是存储在函数的 __annotations__ 属性(一个字典)中:
clip.__annotations__
return 键保存的是返回值注解。
Python 所做的唯一的是就是把注解存到 __annotations__ 属性中,仅此而已,不做任何检查。换句话说,注解对于 Python 解释器没有任何意义。注解只是元数据,可以供 IDE、框架和装饰器等工具使用。
下面是从 inspect.signature() 函数提取注解
from inspect import signature
sig = signature(clip)
sig.return_annotation
for param in sig.parameters.values():
note = repr(param.annotation).ljust(13) # ljust 作用是返回 13 个字符,靠左对其,不够用空格补
print(note, ':', param.name, '=', param.default)
signature 函数返回一个 Signature 对象,它有一个 return_annotation 属性和一个 parameters 属性,后者是一个字典,把参数名映射到 Parameter 对象上。每个 Parameter 对象也有自己的 annotation 属性。
在未来,Bobo 等框架可以支持注解,并进一步自动处理请求。例如,使用 price:float 注解的参数可以自动把查询字符串转成函数期待的 float 类型;quantity:'int > 0' 这样的字符串注解可以转换成对参数的验证
函数注解的最大的影响或许不是让 Bobo 等框架自动设置,而是为 IDE 和 lint 程序等工具的静态类型检查功能提供额外的信息。
支持函数式编程的包¶
虽然 Guido 明确表明,Python 的目标不是变成函数式编程语言,但是得益于 operator 和 functools 等包的支持,函数式编程风格也可以信手拈来。接下来介绍这两个包
operator 模块¶
在函数式编程中,经常需要把算术运算符当做函数使用。例如,不适用递归计算阶乘。求和可以用 sum 函数,但是求积则没有这样的函数,我们可以用 reduce 函数,但是需要一个函数计算序列中两个元素之积。下面展示如何用 lambda 来解决这个问题:
from functools import reduce
def fact(n):
return reduce(lambda a, b: a * b, range(1, n + 1))
operator 模块为算数运算符提供了对应的函数,从而避免写 lambda: a, b: a * b 这样的平凡函数:
from functools import reduce
from operator import mul
def fact(n):
return reduce(mul, range(1, n + 1))
operator 模块中还有一类函数,能替代从序列中去除元素或读取对象属性的 lambda 表达式,因此, itemgetter 和 attrgetter 其实会自动构建函数
下面展示了 itemgetter 的常见用途,根据元组的某个字段给元组列表排序,这个例子中,按照国家代码的顺序打印各个城市的信息。其实, itemgetter(1) 的作用与 lambda fields: fields[1] 一样
metro_data = [
('Tokyo', 'JP', 36.933, (35.69, 139.69)),
('Delhi NCR', 'IN', 21.935, (28.61, 77.21)),
('Mexico City', 'MX', 20.142, (19.43, -99.13)),
('New York-Newark', 'US', 20.104, (40.81, -74.02)),
('Sao Paulo', 'BR', 19.649, (-23.55, -46.64))
]
from operator import itemgetter
for city in sorted(metro_data, key = itemgetter(1)):
print(city)
如果把多个参数传给 itemgetter, 它构建的函数会返回提取的值构成的元组:
cc_name = itemgetter(1, 0)
for city in metro_data:
print(cc_name(city))
itemgetter 使用 [ ] 运算符,它不仅支持序列,还支持所有实现 __getitem__ 方法的类
attrgetter 和 itemgetter 作用类似,它创建的函数根据名称提取对象的属性。如果把多个属性名传给 attrgetter 也会返回提取的值构成的元组。此外如果参数名中包含 .(点号),attrgetter 会深入嵌套对象,获取指定的属性。这些行为如下面代码所示。
from collections import namedtuple
LatLong = namedtuple('LatLong', 'lat long')
Metropolis = namedtuple('Metropolis', 'name cc pop coord')
metro_areas = [Metropolis(name, cc, pop, LatLong(lat, long))
for name, cc, pop, (lat, long) in metro_data]
metro_areas[0]
metro_areas[0].coord.lat
from operator import attrgetter
name_lat = attrgetter('name', 'coord.lat')
for city in sorted(metro_areas, key = attrgetter('coord.lat')):
print(name_lat(city))
下面是 operator 模块定义的部分函数(省略了以 _ 开头的函数,因为它们基本上是实现细节
import operator
[name for name in dir(operator) if not name.startswith('_')]
这些函数差不多一眼就能看出来啥意思,以 i 开头对应的是增量运算符 += &= 等。如果第一个参数是可变的,那么这些运算符函数就会就地修改它,否则,作用与不带 i 的函数一样,直接返回运算结果
在 operator 模块余下的函数,我们最后介绍一下 methodcaller。它的作用与 attrgetter 和 itemgetter 类似,它会自行构建函数。methodcaller 创建的函数会在对象上调用参数指定的方法:
from operator import methodcaller
s = 'The time has come'
upcase = methodcaller('upper')
upcase(s)
hiphenate = methodcaller('replace', ' ', '-')
hiphenate(s)
上面第一个测试只是为了展示 methodcaller 的用法,如果想把 str.upper 作为函数使用,只需要在 str 类上调用:
str.upper(s)
上面的第二个测试表明,methodcaller 还可以冻结某些参数,也就是部分应用(partial application),这与 functools.partial 函数作用类似
使用 functools.partial 冻结参数¶
functools 模块提供了一系列高阶函数,其中最为人熟知的或许是 reduce,余下的函数中,最有用的是 partial 及其变体,partialmethod。
functools.partial 这个高阶函数用于部分应用一个函数,部分应用是指,基于一个函数创建一个新的可调用对象,把原函数某些参数固定。使用这个函数可以把接受一个或多个的函数改编成需要回调的 API,这样参数更少:
from operator import mul
from functools import partial
triple = partial(mul, 3) #使用 mul 创建 partial 函数,第一个参数指定为 3
triple(7)
list(map(triple, range(1, 10)))
我们用第 4 章的规范化语言编码的函数举个例子,如果处理多国语言编写的文本,在比较或排序之前可能想使用 unicode.normalize('NFC', s) 处理所有的字符串 s,如果经常这么做,可以定义一个 nfc 函数
import unicodedata, functools
nfc = functools.partial(unicodedata.normalize, 'NFC')
s1 = 'café'
s2 = 'cafe\u0301'
s1, s2
s1 == s2
nfc(s1) == nfc(s2)
下面的例子是在前面定义的 tag 函数上使用 partial,冻结一个定位参数和一个关键字参数
from functools import partial
picture = partial(tag, 'img', cls = 'pic-frame')
picture(src = 'wumpus.jpeg')
#'<img class="pic-frame" src="wumpus.jpeg" />'
picture #返回一个 functools.partial 对象
picture.func #functools.partial 对象提供了访问原函数的固定参数的属性
picture.args
picture.keywords
functools 中的 partialmethod 函数(Python 3.4 新增)的作用与 partial 一样,不过是用于处理方法的。
functools 模块中的 lru_cache 函数令人印象深刻,它会做备忘(memoization),这是一种自动优化措施,它会存储耗时的函数调用结果,避免重新计算。第七章会介绍这个函数,还会讨论装饰器。以及旨在用作装饰器的其它高阶函数:singledispatch 和 wraps