这篇文章需要看 ipynb 文件,这个 html 没有转换全,而且还有很多格式不对
在 Python 中,函数是一等对象。编程语言理论家把 “一等对象” 定义为满足下面条件的程序实体:
Python 中,整数、字符串和字典都是一等对象。人们经常将 ”把函数视作一等对象“ 简称为 ”一等函数“。这样说并不完美,似乎表明函数是一等对象中的特殊群体。在 Python 中,所有的函数都是一等对象
下面的例子表明 Python 函数是对象,创建了一个函数,然后调用它,读取它的 __doc__
属性,并确定函数对象本身是 function 类的实例
In [2]:
def factorial(n):
'''return n!'''
return 1 if n < 2 else n * factorial(n - 1)
factorial(42)
Out[2]:
In [3]:
factorial.__doc__
Out[3]:
In [4]:
type(factorial)
Out[4]:
下面展示了函数对象的的 ”一等“ 本性。我们可以把 factorial 函数赋值给变量 fact,然后通过变量名调用。我们还能把它当做参数传给 map 函数。
In [5]:
fact= factorial
fact(5)
Out[5]:
In [6]:
map(factorial, range(11))
Out[6]:
In [7]:
list(map(factorial, range(11)))
Out[7]:
In [8]:
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key = len)
Out[8]:
任何单参数函数都能作为 key 参数的值。例如为了创建押韵词典,可以把各个单词反过来拼写,然后排序
In [9]:
def reverse(word):
return word[::-1]
reverse('testing')
Out[9]:
In [10]:
sorted(fruits, key = reverse)
Out[10]:
在函数式编程中,最让人熟知的高阶函数有 map,filter,reduce 和 apply。apply 在 Python 2.3 中被标记过时,Python 3 中移除了,如果想用不定量的参数调用函数,可以编写 fn(*args, **keywords)
map,filter 和 reduce 的替代品
在 Python 3 中 map 和 filter 还是内置函数,但是由于引入了列表推导式和生成器表达式,它们变得没那么重要。因为完全可以替代它们。而且使用更方便:
In [11]:
list(map(fact, range(6)))
Out[11]:
In [12]:
[fact(n) for n in range(6)]
Out[12]:
In [13]:
list(map(factorial, filter(lambda n: n % 2, range(6)))) # 计算 6 以内的奇数的阶乘
Out[13]:
In [14]:
[factorial(n) for n in range(6) if n % 2]
Out[14]:
在 Python 2 中,reduce 是内置函数,Python 3 中放到了 functools 模块中,这个函数最常用于求和,自从 2003 年发布的 Python 2.3 开始,最好使用内置的 sum 函数。可读性和效率都更好
In [15]:
from functools import reduce
from operator import add
reduce(add, range(100))
Out[15]:
In [16]:
sum(range(100))
Out[16]:
sum 和 reduce 的通用思想是把某个操作连续应用到序列的元素上,累计之前的结果,把一系列值归约成一个。
all 和 any 也是内置归约函数
all(iterable)
any(iterable)
第 10 章将会深入讲解 reduce 函数
lambda 关键字是在 Python 表达式创建匿名函数。
然而,Python 简单的语法限制了 lambda 函数的定义体只能用于纯表达式。换句话说,lambda 函数的定义体中不能赋值,也不能使用 while和 try 等 Python 语句。
下面用 lambda 重写了反向排序的例子,这样省去了 reverse 函数:
In [17]:
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key = lambda word: word[::-1])
Out[17]:
除了作为参数传给高阶函数之外,Python 很少使用匿名函数,由于语法上的限制,非平凡的 lambda 要么非常难读,要么无法写出
除了用户自定义函数,调用运算符(即())还可以应用到其他对象上。如果想判对象能否被调用,可以使用内置的 callable() 函数。Python 数据模型文档列出了 7 种可调用对象:
用户定义的函数
内置函数
内置方法
方法
类
__new__
方法创建一个实例,然后运行 __init__
方法,初始化实例,最后把实例返回调用方。因为 Python 没有 new 运算符,所以调用类相当于调用函数。(通常,调用类会创建那个类的实例,不过覆盖 __new__
方法的话,也可能出现其它行为。 19 章会看到一个例子类的实例
__call__
方法,那么它的实例也可以作为函数调用生成器函数
Python 中有多重可调用类型,最安全的判断一个对象能否被调用是使用内置的 callable() 方法
In [18]:
abs, str, 13
Out[18]:
In [19]:
[callable(obj) for obj in (abs, str, 13)]
Out[19]:
In [20]:
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()
Out[20]:
In [21]:
bingo.pick()
Out[21]:
In [22]:
callable(bingo)
Out[22]:
In [23]:
dir(factorial)
Out[23]:
其中大多数属性是 Python 共有的,这里讨论把函数视作对象相关的几个属性,先从 __dict__
开始
与用户定义的常规类一样,函数使用 __dict__
属性存储用户赋予它的属性。这相当于一种基本形式的注解。一般来说,为函数随意赋予属性不是很常见的做法,但是 Django 框架就这么做了。Django 文档中举了下面一个实例,把 short_description 属性赋予一个方法,Django 管理后台使用这个方法时,在记录列表中会出现指定的描述文本:
In [24]:
def upper_case_name(obj):
return ("%s %s" % (obj.first_name, obj.last_name)).upper()
upper_case_name.short_description = 'Customer name'
下面重点说说函数专有而用户定义一般对象没有的属性。计算两个属性集合的差集便能得到函数专有的属性列表:
In [25]:
class C: pass
obj = C()
def func(): pass
sorted(set(dir(func)) - set(dir(obj)))
Out[25]:
本文后面会讨论 __defaults__
、 __code__
、 __annotations__
属性,IDE 框架使用它们提取关于函数签名信息。但是为了深入了解这些属性,我们首先要讨论 Python 为声明函数形参和传入实参提供的强大语法
Python 最好的特性之一就是提供了非常灵活的参数处理机制,而 Python 3 中进一步提供了仅限关键字的参数(keyword-only argument)。与之密切相关的是,调用函数时使用 *
和 **
”展开” 迭代对象,映射到单个参数。下面代码展示了这些特性。
这段代码 tag 函数用来生成 HTML 标签,使用名为 cls 的关键字传入 “class” 属性,这是一种变通方法,因为 “class” 是 Python 的关键字
In [26]:
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)
In [27]:
tag('br')
#'<br />'
Out[27]:
In [28]:
print(tag('p', 'hello', 'world'))
#<p>hello</p>
#<p>world</p>
In [29]:
tag('p', 'hello', id = 33) # tag 函数签名中没有明确指定名称的关键字会被 **attrs 捕获,存入一个字典
#'<p id="33">hello</p>'
Out[29]:
In [30]:
print(tag('p', 'hello', 'world', cls='sidebar')) # cls 只能作为关键字传入
In [31]:
tag(content = 'testing', name = "img") # 调用 tag 时,即使第一个定位参数也能作为关键字参数传入
#'<img content="testing" />'
Out[31]:
In [32]:
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" />'
Out[32]:
仅限关键字参数是 Python 3 新增的特性。在上面的例子,cls 参数只能通过关键字参数指定,它一定不会捕获未命名的定位参数。定义函数时如果想指定仅限关键字参数,要把它们放到前面带有 *
的参数后面。如果不想支持数量不定的定位参数,但想支持仅限关键字参数,在签名中放一个 *
,如下所示:
In [33]:
def f(a, *, b):
return a, b
f(1, b=2)
Out[33]:
In [34]:
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 函数,如下所示,然后审查它
In [35]:
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
的值
In [36]:
clip.__defaults__
Out[36]:
In [37]:
clip.__code__
Out[37]:
In [38]:
clip.__code__.co_varnames
Out[38]:
In [39]:
clip.__code__.co_argcount
Out[39]:
可以看出,这种显示信息方式不是很方便,参数名称在 clip.__code__.co_varnames
中,不过里面还有函数定义体中的局部变量,因此,函数名称是前 N 个字符串,N 的值由 clip.__code__.co_argcount
决定。顺便说一下,这里不包含前缀为 *
或 **
的变长参数。参数的默认值只能通过它们在 __defaults__
元组中的位置确定,因此要从后向前扫描才能把参数和默认值对应。在这里有两个参数,只有一个默认值 80,它属于最后一个参数 max_len,这有违常理
幸好,我们有更好的方式 -- 使用 inspect 模块
In [40]:
from inspect import signature
sig = signature(clip)
sig
Out[40]:
In [41]:
str(sig)
Out[41]:
In [42]:
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 个值之一,列举如下:
除了 name、default 和 kind,inspect.Parameter 对象还有一个 annotation(注解)属性,它的值通常是 inspect._empty
,但是可能包含 Python 3 新的注解语法提供的函数签名元数据,会在后面讨论
inspect.Signature 对象有一个 bind 方法,它可以把任意个参数绑定到签名函数形参上,所用的规则与实参到形参的匹配方式一样。框架可以使用这个方法在真正调用函数前验证参数。
下面是 把上面 tag 函数的签名绑定到一个参数字典上
In [43]:
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 对象
Out[43]:
In [44]:
for name, value in bound_args.arguments.items(): #迭代 bound_args.arguments 中的元素,显示参数的名称和值
print(name, '=', value)
In [45]:
del my_tag['name']
bound_args = sig.bind(**my_tag) # 报错,缺少 name 参数
In [46]:
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__
属性(一个字典)中:
In [47]:
clip.__annotations__
Out[47]:
return 键保存的是返回值注解。
Python 所做的唯一的是就是把注解存到 __annotations__
属性中,仅此而已,不做任何检查。换句话说,注解对于 Python 解释器没有任何意义。注解只是元数据,可以供 IDE、框架和装饰器等工具使用。
下面是从 inspect.signature() 函数提取注解
In [48]:
from inspect import signature
sig = signature(clip)
sig.return_annotation
Out[48]:
In [49]:
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 等包的支持,函数式编程风格也可以信手拈来。接下来介绍这两个包
在函数式编程中,经常需要把算术运算符当做函数使用。例如,不适用递归计算阶乘。求和可以用 sum 函数,但是求积则没有这样的函数,我们可以用 reduce 函数,但是需要一个函数计算序列中两个元素之积。下面展示如何用 lambda 来解决这个问题:
In [50]:
from functools import reduce
def fact(n):
return reduce(lambda a, b: a * b, range(1, n + 1))
operator 模块为算数运算符提供了对应的函数,从而避免写 lambda: a, b: a * b
这样的平凡函数:
In [51]:
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]
一样
In [52]:
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, 它构建的函数会返回提取的值构成的元组:
In [53]:
cc_name = itemgetter(1, 0)
for city in metro_data:
print(cc_name(city))
itemgetter 使用 [ ] 运算符,它不仅支持序列,还支持所有实现 __getitem__
方法的类
attrgetter 和 itemgetter 作用类似,它创建的函数根据名称提取对象的属性。如果把多个属性名传给 attrgetter 也会返回提取的值构成的元组。此外如果参数名中包含 .(点号),attrgetter 会深入嵌套对象,获取指定的属性。这些行为如下面代码所示。
In [54]:
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]
Out[54]:
In [55]:
metro_areas[0].coord.lat
Out[55]:
In [56]:
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 模块定义的部分函数(省略了以 _
开头的函数,因为它们基本上是实现细节
In [57]:
import operator
[name for name in dir(operator) if not name.startswith('_')]
Out[57]:
这些函数差不多一眼就能看出来啥意思,以 i 开头对应的是增量运算符 += &= 等。如果第一个参数是可变的,那么这些运算符函数就会就地修改它,否则,作用与不带 i 的函数一样,直接返回运算结果
在 operator 模块余下的函数,我们最后介绍一下 methodcaller。它的作用与 attrgetter 和 itemgetter 类似,它会自行构建函数。methodcaller 创建的函数会在对象上调用参数指定的方法:
In [58]:
from operator import methodcaller
s = 'The time has come'
upcase = methodcaller('upper')
upcase(s)
Out[58]:
In [59]:
hiphenate = methodcaller('replace', ' ', '-')
hiphenate(s)
Out[59]:
上面第一个测试只是为了展示 methodcaller 的用法,如果想把 str.upper 作为函数使用,只需要在 str 类上调用:
In [60]:
str.upper(s)
Out[60]:
上面的第二个测试表明,methodcaller 还可以冻结某些参数,也就是部分应用(partial application),这与 functools.partial 函数作用类似
functools 模块提供了一系列高阶函数,其中最为人熟知的或许是 reduce,余下的函数中,最有用的是 partial 及其变体,partialmethod。
functools.partial 这个高阶函数用于部分应用一个函数,部分应用是指,基于一个函数创建一个新的可调用对象,把原函数某些参数固定。使用这个函数可以把接受一个或多个的函数改编成需要回调的 API,这样参数更少:
In [61]:
from operator import mul
from functools import partial
triple = partial(mul, 3) #使用 mul 创建 partial 函数,第一个参数指定为 3
triple(7)
Out[61]:
In [62]:
list(map(triple, range(1, 10)))
Out[62]:
我们用第 4 章的规范化语言编码的函数举个例子,如果处理多国语言编写的文本,在比较或排序之前可能想使用 unicode.normalize('NFC', s) 处理所有的字符串 s,如果经常这么做,可以定义一个 nfc 函数
In [63]:
import unicodedata, functools
nfc = functools.partial(unicodedata.normalize, 'NFC')
s1 = 'café'
s2 = 'cafe\u0301'
s1, s2
Out[63]:
In [64]:
s1 == s2
Out[64]:
In [65]:
nfc(s1) == nfc(s2)
Out[65]:
下面的例子是在前面定义的 tag 函数上使用 partial,冻结一个定位参数和一个关键字参数
In [67]:
from functools import partial
picture = partial(tag, 'img', cls = 'pic-frame')
picture(src = 'wumpus.jpeg')
#'<img class="pic-frame" src="wumpus.jpeg" />'
Out[67]:
In [68]:
picture #返回一个 functools.partial 对象
Out[68]:
In [69]:
picture.func #functools.partial 对象提供了访问原函数的固定参数的属性
Out[69]:
In [70]:
picture.args
Out[70]:
In [71]:
picture.keywords
Out[71]:
functools 中的 partialmethod 函数(Python 3.4 新增)的作用与 partial 一样,不过是用于处理方法的。
functools 模块中的 lru_cache 函数令人印象深刻,它会做备忘(memoization),这是一种自动优化措施,它会存储耗时的函数调用结果,避免重新计算。第七章会介绍这个函数,还会讨论装饰器。以及旨在用作装饰器的其它高阶函数:singledispatch 和 wraps