7. 函数是一等对象
不管别人怎么说或怎么想 , 我从未觉得Python受到函数式语言太多的影响 .
我非常熟悉像C和Algol 68 这样的命令式语言 .
虽然我把函数定为一等对象 , 但是并不把Python 当作函数式编程语言 .
---Guido van Rossum
Python仁慈的 '独裁者' ①
( 注 1 : 摘自Guido 的The History of Python 博客 , 题为 "Origins of Python's Functional’ Features" . )
在Python中 , 函数是一等对象 .
编程语言研究人员把 '一等对象' 定义为满足以下条件的程序实体:
• 在运行时创建 ;
• 能赋值给变量或数据结构中的元素 ;
• 能作为参数传给函数 ;
• 能作为函数的返回结果 .
在Python中 , 整数 , 字符串和字典都是一等对象--没什么特别的 .
像Clojure , Elixir和Haskell这样的函数式语言均把函数当作一等对象 .
不过 , 由于一等函数是个非常有用的功能 ,
因此像JavaScript , Go和Java ( 自JDK 8 起 ) 这样的流行语言也采用了这种设计 , 但这些语言都不算 '函数式语言' .
本章以及第三部分的大多数章会探讨把函数视为对象的现实意义 .
* -------------------------------------------------------------------------------------------- *
人们经常将 '把函数视为一等对象' 简称为 '一等函数' .
这样说并不完美 , 似乎表明这是函数中的特殊群体 .
在Python中 , 所有函数都是一等对象 .
* -------------------------------------------------------------------------------------------- *
7.1 本章新增内容
本书第 1 版中的 5.4 节 , 即 '7种可调用对象, 在第2版中变成了' 9 种可调用对象 ' .
新增的可调用对象是原生协程和异步生成器 , 分别由Python 3.5 和Python 3.6 引入 .
这两种可调用对象将在第 21 章探讨 , 但是为了保证信息完整 , 7.5 节会有所提及 .
新增 '仅限位置参数' 一节 , 涵盖Python 3.8 添加的一个功能 .
我把运行时访问函数注解相关的讨论移到了 15.5 节 .
写作第 1 版时 , 'PEP 484--Type Hints' 还在研究中 , 没有统一的注解方式 .
从Python 3.5 开始 , 注解应该遵守PEP484 .
因此 , 最好在讨论类型提示的章节说明 .
现在来说明为什么Python函数是完备的对象 .
7.2 把函数视为对象
示例 7 - 1 中的控制台会话表明 , Python函数就是对象 .
这里我们创建一个函数 , 然后调用它 , 读取它的__doc__属性 , 再确认函数对象本身是function类的实例 .
>> > def factorial ( n) :
. . . """返回n!"""
. . . return 1 if n < 2 else n * factorial( n - 1 )
. . .
>> > factorial( 42 )
1405006117752879898543142606244511569936384000000000
>> > factorial. _doc_
'returns n!'
>> > type ( factorial)
< class 'function' >
__doc__属性用于生成对象的帮助文本 .
在Python交互式控制台中 , help ( factorial ) 命令输出的内容如图 7 - 1 所示 .
图 7 - 1 : factorial函数的帮助界面 , 输出的文本来自函数对象的__doc__属性 .
( 如果有文档字符串会显示的 . . . )
示例 7 - 2 展示了函数对象的 '一等' 本性 .
可以把factorial函数赋值给变量fact , 然后通过变量名调用 .
还可以把factorial函数作为参数传给map函数 .
map ( function , iterable ) 调用会返回一个可迭代对象 ,
所含的项是把第一个参数 ( 一个函数)应用到第二个参数 ( 一个可迭代对象 , 这里是range ( 11 ) )
中各个元素上得到的结果 . ( 意思是 : 第一个参数上提供的函数作用到第二个参数的每一个元素 . )
>> > fact = factorial
>> > fact
< function factorial at Ox. . . >
>> > fact( 5 )
120
>> > map ( factorial, range ( 11 ) )
< map object at 0x. . . >
>> > list ( map ( factorial, range ( 11 ) ) )
[ 1 , 1 , 2 , 6 , 24 , 120 , 720 , 5040 , 40320 , 362880 , 3628800 ]
有了一等函数 , 便可以使用函数式风格编程 .
函数式编程的特色之一是高阶函数 , 详见 7.3 节 .
7.3 高阶函数
接受函数为参数或者把函数作为结果返回的函数是高阶函数 ( higher-order function ) .
示例 7 - 2 中的map函数就是一例 .
此外 , 内置函数sorted也是 : 通过可选的key参数提供一个函数 , 应用到每一项上进行排序 ( 参见 2.9 节 ) .
如果想根据单词的长度排序 , 则只需把len函数传给key参数 , 如示例 7 - 3 所示 .
>> > fruits = [ 'strawberry' , 'fig' , 'apple' , 'cherry' , 'raspberry' , 'banana' ]
>> > sorted ( fruits, key= len )
[ 'fig' , 'apple' , 'cherry' , 'banana' , 'raspberry' , 'strawberry' ]
任何单参数函数都能作为key参数的值 .
例如 , 为了创建押韵词典 , 可以把各个单词反过来拼写 , 然后排序 .
注意 , 在示例 7 - 4 中 , 列表内的单词没有变 , 只是把反向拼写当作了排序条件 , 因此各种浆果都排在一起了 .
>> > def reverse ( word) :
. . . return word[ : : - 1 ]
. . .
>> > reverse( 'testing' )
'gnitset'
>> > sorted ( fruits, key= reverse)
[ 'banana' , 'apple' , 'fig' , 'raspberry' , 'strawberry' , 'cherry' ]
在函数式编程范式中 , 最为人熟知的高阶函数有map , filter , reduce和apply .
apply函数在Python 2.3 中已弃用 , 在Python3中已正式移除 , 因为用不到了 .
如果想使用不定量的参数调用函数 , 可以编写fn ( * args , * * kwargs ) , 无须再编写apply ( fn , args , kwargs ) .
map , filter和reduce这 3 个高阶函数还能用到 , 不过在大多数使用场景中有更好的替代品 , 详见接下来的内容 .
7.3.1 map, filter和reduce的现代替代品
函数式语言通常会提供map , filter和reduce这 3 个高阶函数 ( 有时使用不同的名称 ) .
在Python3中 , map和filter还是内置函数 , 但是由于引入了列表推导式和生成器表达式 ,
因此二者就变得没那么重要了 .
列表推导式或生成器表达式兼具map和filter这两个函数的功能 , 而且代码可读性更高 , 如示例 7 - 5 所示。
>> > list ( map ( factorial, range ( 6 ) ) )
[ 1 , 1 , 2 , 6 , 24 , 120 ]
>> > [ factorial( n) for n in range ( 6 ) ]
[ 1 , 1 , 2 , 6 , 24 , 120 ]
>> > list ( map ( factorial, filter ( lambda n: n % 2 , range ( 6 ) ) ) )
[ 1 , 6 , 120 ]
>> > [ factorial( n) for n in range ( 6 ) if n % 2 ]
[ 1 , 6 , 120 ]
在Python 3 中 , map和filter返回生成器 ( 一种迭代器 ) , 因此现在它们的直接替代品是生成器表达式 .
( 在Python 2 中 , 这两个函数返回列表 , 因此最接近的替代品是列表推导式 . )
在Python 2 中 , reduce是内置函数 , 但是Python 3 把它放到functools模块里了 .
这个函数最常用于求和 , 但自 2003 年Python 2.3 发布以来 , 内置函数sum在执行这项操作时效果更好 .
在可读性和性能方面 , 这是一项重大改善 , 如示例 7 - 6 所示 .
>> > from functools import reduce
>> > from operator import add
>> > reduce ( add, range ( 100 ) )
4950
>> > sum ( range ( 100 ) )
4950
* * ------------------------------------------------------------------------------------------- * *
sum和reduce的整体运作方式是一样的 , 即把某个操作连续应用到序列中的项上 ,
累计前一个结果 , 把一系列值归约成一个值 .
* * ------------------------------------------------------------------------------------------- * *
内置的归约函数还有all和any .
all ( iterable )
iterable中没有表示假值的元素时返回True . all ( [ ] ) 返回True .
any ( iterable )
只要iterable中有元素是真值就返回True . any ( [ ] ) 返回False .
12.7 节将详细说明reduce函数 , 届时我们会不断改进一个示例 , 为讨论提供有意义的上下文 .
17.10 节将重点讨论可迭代对象 , 届时会总结各个归约函数 .
为了使用高阶函数 , 有时创建一次性的小型函数更便利 . 这便是匿名函数存在的原因 , 详见 7.4 节 .
7.4 匿名函数
lambda关键字使用Python表达式创建匿名函数 .
然而 , 受Python简单的句法限制 , lambda函数的主体只能是纯粹的表达式 .
也就是说 , lambda函数的主体中不能有while , try等Python语句 .
使用 = 赋值也是一种语句 , 不能出现在lambda函数的主体中 . 可以有新出现的 : = 赋值表达式 .
不过 , 有这种赋值表达式的lambda函数可能太过复杂 , 可读性低 , 因此建议重构 , 改成使用def定义的常规函数 .
在高阶函数的参数列表中最适合使用匿名函数 .
例如 , 示例 7 - 7 使用lambda表达式重写了示例 7 - 4 中排序押韵单词的示例 , 这样就省掉了reverse函数 .
>> > fruits = [ 'strawberry' , 'fig' , 'apple' , 'cherry' , 'raspberry' , 'banana' ]
>> > sorted ( fruits, key= lambda word: word[ : : - 1 ] )
[ 'banana' , 'apple' , 'fig' , 'raspberry' , "strawberry',' cherry']
除了作为参数传给高阶函数 , Python很少使用匿名函数 .
由于句法上的限制 , 非平凡的lambda表达式要么难以阅读 , 要么无法写出 .
如果发现lambda表达式的可读性低 , 强烈建议你遵照Fredrik Lundh的重构建议 .
* -----------------------------FredrikLundh 提出的lambda 表达式重构秘诀-------------------------- *
如果使用lambda表达式导致一段代码难以理解 , Fredrik Lundh建议像下面这样重构 .
1. 编写注释 , 说明lambda表达式的作用 .
2. 研究一会儿注释 , 找出一个名称来概括注释 .
3. 把lambda表达式转换成def语句 , 使用那个名称来定义函数 .
4. 删除注释 .
以上步骤摘自 'Functional Programming HOWTO' 一文 . 这篇文章不可错过 .
* --------------------------------------------------------------------------------------------- *
lambda句法只是语法糖 , lambda表达式会像def语句一样创建函数对象 .
lambda表达式只是Python中几种可调用对象的一种 .
7.5 节会说明所有可调用对象 .
7.5 9种可调用对象
除了函数 , 调用运算符 ( ( ) ) 还可以应用到其他对象上 .
如果想判断对象能否调用 , 可以使用内置的callable ( ) 函数 .
数据模型文档列出了自Python 3.9 起可用的 9 种可调用对象 .
* 1. 用户定义的函数
使用def语句或lambda 表达式创建的函数 .
* 2. 内置函数
使用C语言 ( CPython ) 实现的函数 , 例如len或time . strftime .
* 3. 内置方法
使用C语言实现的方法 , 例如dict . get .
* 4. 方法
在类主体中定义的函数 .
* 5. 类
调用类时运行类的__new__方法创建一个实例 ,
然后运行_init_方法 , 初始化实例 , 最后再把实例返回给调用方 .
Python中没有new运算符 , 调用类就相当于调用函数 . ②
( 注 2 : 通常 , 调用类会创建类的实例 , 不过 , 如果覆盖__new__方法 , 则也可能出现其他行为 .
22.2 .3 节展示了一个例子 . )
* 6. 类的实例
如果类定义了__call__方法 , 那么它的实例可以作为函数调用 . 详见 7.6 节 .
* 7. 生成器函数
主体中有yield关键字的函数或方法 . 调用生成器函数返回一个生成器对象 .
* 8. 原生协程函数
使用async def定义的函数或方法 . 调用原生协程函数返回一个协程对象 . Python 3.5 新增 .
* 9. 异步生成器函数
使用async def定义 , 而且主体中有yield关键字的函数或方法 .
调用异步生成器函数返回一个异步生成器 , 供async for 使用 . Python3 . 6 新增 .
与其他可调用对象不同 , 生成器 , 原生协程和异步生成器函数的返回值不是应用程序数据 ,
而是需要进一步处理的对象 , 要么产出应用程序数据 , 要么执行某种操作 .
生成器函数会返回迭代器 ( 详见第 17 章 ) .
原生协程函数和异步生成器函数返回的对象只能由异步编程框架 ( 例如 asyncio ) 处理 ( 详见第 21 章 ) .
* --------------------------------------------------------------------------------------------- *
Python中有各种各样的可调用类型 , 因此判断对象能否调用 , 最安全的方法是使用内置函数callable ( ) .
> > > abs , str , 'Ni!'
( < built-in function abs > , < class 'str' > , 'Ni!' )
> > > [ callable ( obj ) for obj in ( abs , str , 'Ni!' ) ]
[ True, True, False ]
* --------------------------------------------------------------------------------------------- *
接下来阐述如何把类的实例变成可调用对象。
7.6 用户定义的可调用类型
不仅Python函数是真正的对象 , 而且任何Python对象都可以表现得像函数 .
为此 , 只需实现实例方法__call__ .
示例 7 - 8 实现的BingoCage类的实例可使用任何可迭代对象构建 ,
内部存储一个随机排序的元素列表 , 调用实例从中取出一个元素 . ③
( 注 3 : 有现成的random . choice可用 , 为什么还要定义BingoCage类呢?
choice函数可能会多次返回同一个元素 , 因为被选中的元素不从指定的容器中删除 .
而调用BingoCage绝不会返回重复结果 , 当然前提是填充实例的值各不相同 . )
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( )
下面的例子简单演示了如何使用示例 7 - 8 定义的行为 .
注意 , bingo实例可以作为函数调用 , 而且内置函数callable ( ) 判定它是可调用对象 .
>> > bingo = BingoCage( range ( 3 ) )
>> > bingo. pick( )
1
>> > bingo( )
0
>> > callable ( bingo)
True
实现__call__方法是创建类似函数的对象的简便方式 ,
此时必须在内部维护一个状态让它在多次调用之间存续 , 例如BingoCage中的剩余元素 .
( 在内部维护一个状态 , 让它在多次调用之间存续 :
意味着在对象中创建一个变量来存储某些信息 , 并确保该信息在多次调用对象时得以保留和更新 . )
__call__的另一个用处是实现装饰器 .
装饰器必须可调用 , 而且有时要在多次调用之间 '记住' 某些事 [ 例如备忘(memoization),
即缓存消耗大的计算结果, 供后面使用 ] , 或者把复杂的操作分成几个方法实现 .
在函数式编程中 , 创建保有内部状态的函数要使用闭包 ( closure ) .
闭包和装饰器将在第 9 章讨论 .
下面探讨Python为声明函数形参和传入实参所提供的强大句法 .
7.7 从位置参数到仅限关键字参数
Python函数最好的功能之一是提供了极为灵活的参数处理机制 .
与之密切相关的是 , 调用函数时可以使用 * 和 * * 拆包可迭代对象 , 映射各个参数 .
下面让我们通过示例 7 - 9 中的代码和示例 7 - 10 中的测试来展示这些功能 .
def tag ( name, * content, class_= None , ** attrs) :
"""生成一个或多个HTML标签"""
if class_ is not None :
attrs[ 'class' ] = class_
attr_pairs = ( f' { attr} =" { value} "'
for attr, value in sorted ( attrs. items( ) ) )
attr_str = '' . join( attr_pairs)
if content:
elements = ( f'< { name} { attr_str} > { c} </ { name} >'
for c in content)
return '\n' . join( elements)
else :
return f'< { name} { attr_str} />'
tag 函数的调用方式很多 , 如示例 7 - 10 所示 .
>> > tag( 'br' )
'<br />'
>> > tag( 'p' , 'hello' )
'<p>hello</p>'
>> > print ( tag( 'p' , 'hello' , 'world' ) )
< p> hello< / p>
< p> world< / p>
>> > tag( 'p' , 'hello' , id = 33 )
'<p id="33">hello</p>'
>> > print ( tag( 'p' , 'hello' , 'world' , class_= 'sidebar' ) )
< p class = "sidebar" > hello< / p>
< p class = "sidebar" > world< / p>
>> > tag( content= 'testing' , name= "img" )
'<img content="testing" />'
>> > my_tag = { 'name' : 'img' , 'title' : 'Sunset Boulevard' ,
'src' : 'sunset.jpg' , 'class' : 'framed' }
>> > tag( ** my_tag)
'<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />'
仅限关键字参数是Python 3 新增的功能 .
在示例 7 - 9 中 , class_参数只能通过关键字参数指定 , 它一定不会捕获无名位置参数 .
定义函数时 , 如果想指定仅限关键字参数 , 就要把它们放到前面有 * 的参数后面 .
如果不想支持数量不定的位置参数 , 但是想支持仅限关键字参数 , 则可以在签名中放一个 * , 如下所示 .
>> > def f ( a, * , b) :
. . . return a, b
. . .
>> > f( 1 , b= 2 )
( 1 , 2 )
>> > f( 1 , 2 )
Traceback ( most recent call last) :
File "stdin>" , line 1 , in < module>
TypeError: f( ) takes 1 positional argument but 2 were given
注意 , 仅限关键字参数不一定要有默认值 , 可以像上例中的b那样 , 强制要求传入实参 .
7.7.1 仅限位置参数
从Python3 . 8 开始 , 用户定义的函数签名可以指定仅限位置参数 .
内置函数都是如此 , 例如divmod ( a , b ) 只能使用位置参数调用 , 不能写成divmod ( a = 10 , b = 4 ) .
( 严谨的说应该是divmod ( x , y ) , 这里只是大个比方来说 . )
如果想定义只接受位置参数的函数 , 则可以在参数列表中使用 / .
下面的例子摘自 "What's New In Python 3.8" , 展示了如何模拟内置函数divmod的参数行为 .
def divmod ( a, b, / ) :
return ( a // b, a % b)
/左边均是仅限位置参数. 在 / 后面 , 可以指定其他参数 , 处理方式一同往常
* * * ----------------------------------------------------------------------------------------- * * *
在Python3 . 7 或之前的版本中 , 参数列表中的 / 将导致句法错误 .
参数列表 : 是指函数的形参 . 是定义函数时使用的一组参数的列表 .
参数列表中包含了函数中需要使用的所有参数的名称和默认值 ( 如果有的话 ) .
* * * ----------------------------------------------------------------------------------------- * * *
以示例 7 - 9 中的tag函数为例 .
如果希望name是仅限位置参数 , 可以在函数签名中添加一个 / , 如下所示 .
def tag ( name , / , * content , class_ = None , * * attrs ) :
"What's New In Python 3.8" 和PEP570中还有一些仅限位置参数的例子 .
深入分析Python灵活的参数声明功能之后 , 本章余下的内容将介绍标准库为函数式编程提供支持的常用包 .
7.8 支持函数式编程的包
虽然Guido明确表明 , Python的目标不是变成函数式编程语言 ,
但是得益于一等函数 , 模式匹配 , 以及operator和functools等包的支持 ,
其对函数式编程风格也可以 '信手拈来' .
接下来的 7.8 .1 节和 7.8 .2 节将分别介绍这两个包 .
7.8.1 operator模块
在函数式编程中 , 经常需要把算术运算符当作函数使用 .
例如 , 不使用递归计算阶乘 . 求和可以使用sum函数 , 求积则没有这样的函数 .
可以使用reduce函数 ( 参见 'map, filter和reduce的现代替代品' 一节 ) , 但是需要一个函数来计算序列中两项之积 .
示例 7 - 11 展示了如何使用lambda表达式解决这个问题 .
from functools import reduce
def factorial ( n) :
return reduce ( lambda a, b: a * b, range ( 1 , n+ 1 ) )
operator模块为多个算术运算符提供了对应的函数 , 无须再动手编写像lambda a , b : a * b 这样的匿名函数 .
使用算术运算符函数 , 可以把示例 7 - 11 改写成示例 7 - 12 那样 .
from functools import reduce
from operator import mul
def factorial ( n) :
return reduce ( mul, range ( 1 , n+ 1 ) )
operator模块中还有一类函数 , 即工厂函数itemgetter和attrgetter ,
能替代从序列中取出项或读取对象属性的lambda表达式 .
示例 7 - 13 展示了itengetter的常见用途 : 根据元组的某个字段对元组列表进行排序 .
在这个示例中 , 我们按照国家代码 ( 第 2 个字段 ) 的顺序打印各个城市的信息 .
其实 , itemgetter ( 1 ) 会创建一个接受容器的函数 , 返回索引位 1 上的项 .
与作用相同的 lambda fields : fields [ 1 ] 相比 , 使用itemgetter更容易写出代码 , 而且可读性更高 .
>> > 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 ) ) ,
. . . ( 'São Paulo' , 'BR' , 19.649 , ( - 23.547778 , - 46.635833 ) ) ,
. . . ]
from operator import itemgetter
>> > for city in sorted ( metro_data, key= itemgetter( 1 ) ) :
. . . print ( city)
. . .
( 'São 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 ) )
如果传给itemgetter多个索引参数 ,
那么itemgetter构建的函数就会返回提取的值构成的元组 , 以方便根据多个键排序 .
>> > cc_name = itemgetter( 1 , 0 )
>> > for city in metro_data:
. . . print ( cc_name( city) )
. . .
( 'JP' , 'Tokyo' )
( 'IN' , 'Delhi NCR' )
( 'MX' , 'Mexico City' )
( 'US' , 'New York-Newark' )
( 'BR' , 'São Paulo' )
itemgetter使用 [ ] 运算符 , 因此它不仅支持序列 , 还支持映射和任何实现__getitem__方法的类 .
这句话很费解 , 意思为 :
itemgetter函数使用方括号 [ ] 运算符来访问元素 ,
因此它不仅适用于序列 ( 如列表 , 元组等 ) 还适用于映射 ( 如字典 ) 以及任何实现了__getitem__方法的类 .
from operator import itemgetter
my_list = [ 1 , 2 , 3 , 4 , 5 ]
get_second = itemgetter( 1 )
print ( get_second( my_list) )
my_dict = { 'a' : 1 , 'b' : 2 , 'c' : 3 }
get_value_c = itemgetter( 'c' )
print ( get_value_c( my_dict) )
class MyClass :
def __init__ ( self, data) :
self. data = data
def __getitem__ ( self, index) :
return self. data[ index]
my_obj = MyClass( [ 1 , 2 , 3 , 4 , 5 ] )
get_third = itemgetter( 2 )
print ( get_third( my_obj) )
与itemgetter的作用类似 , attrgetter创建的函数会根据名称来提取对象的属性 .
如果传给attrgetter多个属性名 , 那么它也会返回由提取的值构成的元组 .
此外 , 如果参数名中包含 . ( 点号 ) , 那么attrgetter就会深入嵌套对象 , 检索属性 .
这些行为如示例 7 - 14 所示 .
这个控制台会话不短 , 因为我们要构建一个嵌套结构 , 以展示attrgetter如何处理包含点号的属性名 .
>> > from collections import namedtuple
>> > LatLon = namedtuple( 'LatLon' , 'lat lon' )
>> > Metropolis = namedtuple( 'Metropolis' , 'name cc pop coord' )
>> > metro_areas = [ Metropolis( name, cc, pop, LatLon( lat, lon) )
. . . for name, cc, pop, ( lat, lon) in metro_data]
>> > metro_areas[ 0 ]
Metropolis( name= 'Tokyo' , cc= 'JP' , pop= 36.933 , coord= LatLon( lat= 35.689722 , lon= 139.691667 ) )
>> > metro_areas[ 0 ] . coord. lat
35.689722
>> > from operator import attrgetter
>> > name_lat = attrgetter( 'name' , 'coord.lat' )
>> > for city in sorted ( metro_areas, key= attrgetter( 'coord.lat' ) ) :
. . . print ( name_lat( city) )
. . .
( 'São Paulo' , - 23.547778 )
( 'Mexico City' , 19.433333 )
( 'Delhi NCR' , 28.613889 )
( 'Tokyo' , 35.689722 )
( 'New York-Newark' , 40.808611 )
下面是operator模块中定义的部分函数列表 ( 省略了以_开头的名称 , 因为它们基本上是实现细节 ) .
>> > [ 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' , 'imatmul' ,
'imod' , 'imul' , 'index' , 'indexof' , 'inv' , 'invert' , 'ior' ,
'lipow' , 'irshift' , 'is_' , 'is_not' , 'isub' , 'itemgetter' ,
'itruediv' , 'ixor' , 'le' , 'length_hint' , 'lshift' , 'lt' , 'matmul' ,
'methodcaller' , 'mod' , 'mul' , 'ne' , 'neg' , 'not_' , 'or_' , 'pos' ,
'lpow' , 'rshift' , 'setitem' , 'sub' , 'truediv' , 'truth' , 'xor' ]
这 54 个名称中大部分名称的作用不言而喻 .
以i开头 , 后面是另一个运算符的那些名称 ( 例如iadd , iand等 ) , 对应的是增量赋值运算符 ( 例如 + = , & = 等 ) .
如果第一个参数是可变的 , 那么这些函数就会就地修改第一个参数 ;
否则 , 作用与不带i的函数一样 , 直接返回运算结果 .
在operator模块余下的函数中 , 最后介绍一下methodcaller .
它的作用与attrgetter和itengetter类似 , 即创建函数 .
methodcaller创建的函数会在对象上调用参数指定的方法 , 如示例 7 - 15 所示 .
>> > from operator import methodcaller
>> > s = 'The time has come'
>> > upcase = methodcaller( 'upper' )
>> > upcase( s)
'THE TIME HAS COME'
>> > hyphenate = methodcaller( 'replace' , ' ' , '_' )
>> > hyphenate( s)
'The-time-has-come'
示例 7 - 15 中的第一个测试只是为了展示methodcaller的用法 .
如果想把str . upper作为函数使用 , 则只需在str类上调用 , 传入一个字符串参数即可 , 如下所示 .
> > > str . upper ( s )
'THE TIME HAS COME'
示例 7 - 15 中的第二个测试表明 , methodcaller还可以冻结某些参数 ,
( '冻结参数' : 意味着在创建偏函数时 , 可以将某些参数的值固定在偏函数中 , 使得在调用时它们是不变的 . )
也就是部分应用程序 ( partial application ) , 这与functools . partial函数的作用类似 , 详见 7.8 .2 节 .
7.8.2 使用functools.partial冻结参数
functools模块提供了一系列高阶函数 , 比如 7.3 节 'map, filter和reduce的现代替代品' 中用过的reduce .
另外一个值得关注的函数是partial , 它可以根据提供的可调用对象产生一个新可调用对象 ,
为原可调用对象的某些参数绑定预定的值 .
使用这个函数可以把接受一个或多个参数的函数改造成需要更少参数的回调的API .
示例 7 - 16 是一个简单演示 .
>> > from operator import mul
>> > from functools import partial
>> > triple = partial( mul, 3 )
>> > triple( 7 )
21
>> > list ( map ( triple, range ( 1 , 10 ) ) )
[ 3 , 6 , 9 , 12 , 15 , 18 , 21 , 24 , 27 ]
使用 4.7 节中讲过的unicode . normalize函数再举一个例子 , 这个示例更有实际意义 .
处理包含多国语言的文本时 , 你可能想在比较或排序之前使用unicode . normalize ( 'NFC' , s ) 规范化字符串S .
如果经常需要这么做 , 则可以定义一个nfc函数 , 如示例 7 - 17 所示 .
>> > import unicodedata, functools
>> > nfc = functools. partial( unicodedata. normalize, 'NFC' )
>> > s1 = 'café
>> > s2 = 'cafe\u0301'
>> > s1, s2
( 'café' , 'café' )
>> > s1 == s2
False
>> > nfc( s1) == nfc( s2)
True
partial的第一个参数是一个可调用对象 , 后面跟着任意个要绑定的位置参数和关键字参数 .
示例 7 - 18 在示例 7 - 9 中定义的tag函数上使用partial冻结了一个位置参数和一个关键字参数 .
>> > from tagger import tag
>> > tag
< function tag at 0x10206d1e0 >
>> > from functools import partial
>> > picture = partial( tag, 'img' , class_= 'pic-frame' )
>> > picture( src= 'wumpus.jpeg' )
'<img class="pic-frame" src="wumpus.jpeg" />'
>> > picture
functools. partial( < function tag at 0x10206d1e0 > , 'img' , class_= 'pic-frame' )
>> > picture. func
< function tag at 0x10206d1e0 >
>> > picture. args
( 'img' , )
>> > picture. keywords
{ 'class_' : 'pic-frame' }
functools . partialmethod函数的作用与partial一样 , 不过其用于处理方法 .
functools模块中还有一些高阶函数可用作函数装饰器 , 例如cache , singledispatch等 .
第 9 章将介绍这些函数 , 同时还将探讨如何自定义装饰器 .
7.9 本章小结
本章的目标是探讨Python函数的一等本性 .
这意味着 , 可以把函数赋值给变量 , 传给其他函数 , 存储在数据结构中 , 以及访问函数的属性 ,
供框架和一些工具使用 .
高阶函数是函数式编程的重要组成部分 , Python中也经常用到 , 比如内置函数sorted , min 和max ,
以及标准库中的functools . partial .
即使现在不像以前那样经常使用map , filter和reduce等函数了 ,
但是还有列表推导式 ( 以及类似的结构 , 例如生成器表达式 ) 以及sum , all和any等内置的归约函数 .
自Python 3.6 起 , Python有 9 种可调用对象 , 从lambda表达式创建的简单函数 , 到实现__call__方法的类实例 .
生成器和协程也是可调用对象 , 不过行为与其他可调用对象差异较大 .
可调用对象都能通过内置函数callable ( ) 检测 .
可调用对象支持丰富的形参声明句法 , 包括仅限关键字参数 , 仅限位置参数和注解 ( 注解还没讲 , 在后面 . . . ) .
最后 , 本章介绍了operator模块中的一些函数以及functools . partial函数 .
有了这些函数 , 就不太需要使用功能有限的lambda表达式实现函数式编程了 .
7.10 延伸阅读
接下来的 3 章将继续探讨编程中对函数对象的使用 :
第 8 章专门说明函数参数和返回值的类型注解 ;
第 9 章深入讲解函数装饰器 ( 一种特殊的高阶函数 ) , 以及背后用到的闭包机制 ;
第 10 章介绍一等函数如何简化某些经典的面向对象设计模式 .
< < Python 语言参考手册 > > 中的 3.2 节 '标准类型层次结构' 介绍了 9 种可调用类型和其他所有内置类型。
< < Python Cookbook ( 第 3 版 ) 中文版 > > 的第 7 章采用不同的方式探讨了相关概念 ,
对本章和第 9 章是不错的补充 .
如果对仅限关键字参数的基本原理和使用场景感兴趣 , 请阅读 'PEP 3102—Keyword-OnlyArguments' .
A . M . Kuchling 写的文章 'Python Functional Programming HOWTO' 对Python函数式编程做了很好的介绍 .
不过 , 该文章的重点是使用迭代器和生成器 ( 参见第 17 章 ) .
Stack Overflow网站中的问题 'Python: Why is functools.partial necessary?'
有个翔实而有趣的回答 , 答主是Alex Martelli , 他是经典的Python in a Nutshell一书的作者 .
'Python 是一门函数式语言吗?' 我把对这个问题的思考汇集到一个演讲中-- 'Beyond Paradigms' .
这个演讲在PyCaribbean , PyBay和PyConDE上都讲过 , 是我最得意的成就之一 .
在这次大会上我遇到了后来担任本书技术审校的Miroslav Sedivy和Jürgen Gmach .
* -----------------------------------------------杂谈------------------------------------------ *
'Python 是一门函数式语言吗?' :
2000 年的某一天 , 我参加了Zope公司在美国组织的一场讲习班 , 期间Guido van Rossum到访了教室 ( 他不是讲师 ) .
在课后的问答环节 , 有人问他Python的哪些功能是从其他语言借鉴而来的 .
他答道 : 'Python中一切好的功能都是从其他语言中借鉴来的.'
布朗大学计算机科学教授 Shriram Krishnamurthi 在其论文
' Teaching Programming Languages in a Post-Linnaean Age的开头这样写道 :
编程语言 '范式' 已近末日 , 是旧时代的遗留物 , 今人厌烦 .
既然现代语言的设计者对范式不屑一顾 , 那么我们的课程为什么要像奴隶一样对其言听计从呢?
在该论文中 , 下面这一段点名提到了Python :
对Python , Ruby或Perl这些语言还要了解什么呢?
它们的设计者没有耐心去精确实现林奈层次结构 , 设计者按照自己的意愿从别处借鉴功能 ,
创建出完全无视过往概念的大杂烩 .
Krishnamurthi指出 , 不要试图把语言归为某一类 ; 相反 , 应把语言视作功能的聚合 .
7.10 节提到的 'Beyond Paradigms' 演讲就受到了他的影响。
为Python提供一等函数打开了函数式编程的大门 , 不过这并不是Guido的本意 .
他在 "Origins of Python's Functional Features" 一文中说 ,
map , filter和reduce的最初目的是为Python增加lambda表达式 .
这些功能都由Amrit Prem贡献 , 添加在 1994 年发布的Python1 . 0 中 ( 参见CPython源码中的Misc / HISTORY文件 ) .
map , filter和reduce等函数首次出现在Lisp中 , 这是最早的一门函数式语言 .
然而 , Lisp不限制在lambda表达式中能做什么? 因为Lisp中的一切都是表达式 .
Python使用的是面向语句的句法 , 表达式中不能包含语句 , 而很多语言结构是语句--包括try / catch ,
我编写lambda表达式时最想念这个语句 .
Python为了提高句法的可读性 , 必须付出这样的代价 .
⑤Lisp有很多优点 , 可读性一定不是其中之一 .
( 注 5 : 此外 , 还有一个问题 : 把代码粘贴到Web论坛时 , 缩进会丢失 . 当然 , 这是题外话 . )
讽刺的是 , 从另一门函数式语言 ( Haskell ) 中借用列表推导式之后,
Python对map , filter , 以及lambda表达式的需求极大地减少了 .
除了匿名函数句法上的限制 ,
影响函数式编程惯用法在Python中广泛使用的最大障碍是缺少尾调用消除 ( tail-call elimination ) .
这是一项优化措施 , 在函数的主体 '末尾' 递归调用 , 从而提高计算函数的内存使用效率 .
Guido在另一篇博客文章 ( 'Tail Recursion Elimination' ) 中解释了为什么这种优化措施不适合Python .
该文章详细讨论了技术论证 , 不过前 3 个 ( 也是最重要的原因 ) 与易用性有关 .
Python作为一门易于使用 , 学习和教授的语言并非偶然 , 有Guido在为我们把关 .
综上所述 , 从设计上看 , 不管函数式语言的定义如何 , Python都不是一门函数式语言 .
它只是从函数式语言中借鉴了一些好的想法 .
'匿名函数的问题' :
除了Python独有的句法上的局限 , 对任何一门语言来说 , 匿名函数都有一个严重的缺点 : 没有名称 .
我是半开玩笑的 . 函数有名称 , 栈跟踪更易于阅读 .
匿名函数是一种便利的简洁方式 , 人们乐于使用它们 , 但是有时会忘乎所以 ,
尤其是在鼓励深层嵌套匿名函数的语言和环境中 , 例如Node . js之上的JavaScript .
匿名函数嵌套的层级太深 , 不利于调试和处理错误 .
Python中的异步编程结构更好 , 或许就是因为受lambda句法的限制 , 想滥用都不可能 , 必须使用更明确的方式 .
现代异步API开始使用promise , future和deferred等概念 .
加上协程 , 我们终于逃脱了 '回调地狱' .
我保证 , 后面会进一步讨论异步编程 , 但是必须等到第 21 章 .
* --------------------------------------------------------------------------------------------- *