7.第七章 函数是一等对象

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类的实例.
# 示例7-1 创建并测试一个函数, 读取函数的__doc__属性, 再检查函数的类型

# 这是一个控制台会话, 因此是在'运行时'创建了一个函数.
>>> def factorial(n):
... 	"""返回n!"""
...		return 1 if n < 2 else n * factorial(n -1)
...

>>> factorial(42)
1405006117752879898543142606244511569936384000000000

# __doc__是函数对象众多属性中的一个.
>>> factorial._doc_
'returns n!'

# factorial是function类的实例.
>>> type(factorial)
<class 'function'>

__doc__属性用于生成对象的帮助文本.
在Python交互式控制台中, help(factorial)命令输出的内容如图7-1所示.

image-20230530141422050

7-1: factorial函数的帮助界面, 输出的文本来自函数对象的__doc__属性.
(如果有文档字符串会显示的...)
示例7-2展示了函数对象的'一等'本性.
可以把factorial函数赋值给变量fact, 然后通过变量名调用.
还可以把factorial函数作为参数传给map函数.
map(function, iterable)调用会返回一个可迭代对象, 
所含的项是把第一个参数(一个函数)应用到第二个参数(一个可迭代对象, 这里是range(11))
中各个元素上得到的结果. (意思是: 第一个参数上提供的函数作用到第二个参数的每一个元素.)
# 示例7-2 通过其他名称使用factorial函数, 再把factorial函数作为参数传递
>>> 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所示.
# 示例7-3 根据单词长度排序一个列表
>>> fruits = ['strawberry', 'fig', 'apple','cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=len)
['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']

任何单参数函数都能作为key参数的值.
例如, 为了创建押韵词典, 可以把各个单词反过来拼写, 然后排序.
注意, 在示例7-4, 列表内的单词没有变, 只是把反向拼写当作了排序条件, 因此各种浆果都排在一起了.
# 示例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所示。
# 示例7-5计算阶乘列表: map和filter与列表推导式对比

# 使用0!~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]

# 使用map和filter计算直到5!的奇数阶乘列表.
>>> list(map(factorial, filter(lambda n: n % 2, range(6))))
[1, 6, 120]

# 使用列表推导式做相同的工作, 换掉map和filter, 也无须使用lambda表达式.
>>> [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所示.
# 示例7-6 使用reduce和sum计算0~99的整数之和

# 从Python3.0开始, reduce不再是内置函数.
>>> from functools import reduce
# 导入add, 以免创建一个只求两数之和的函数.
# (意思是导入add函数后不需要自己创建一个只求两数之和的函数.)
>>> from operator import add

# 计算0~99的整数之和.
>>> reduce(add, range(100))
4950

# 使用sum执行同一个操作, 无须导入并调用reduce和add.
>>> 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函数.
# 示例7-7 使用lambda表达式反转拼写, 然后依次给单词列表排序
>>> 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绝不会返回重复结果, 当然前提是填充实例的值各不相同.)
# 示例7-8 bingocall.py: 调用BingoCage实例, 从打乱顺序的列表中取出一个元素
import random


class Bingocage:
    # __init__接受任何可迭代对象.
    # 在本地构建一个副本,, 防止传入的列表参数有什么意外的副作用.
    def __init__(self, items):
        self._items = list(items)

        # shuffle定能打乱顺序, 因为self._items是列表.
        random.shuffle(self._items)

    # 起主要作用的方法.
    def pick(self):
        try:
            return self._items.pop()
        except IndexError:
            # 如果self._items为空, 就抛出异常, 并设定错误消息.
            raise LookupError('pick from empty Bingocage')

    # bingo()是bingo.pick()的快捷方式.
    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中的测试来展示这些功能.
# 示例7-9 tag函数用于生成HTML标签. 
# 可以使用名为class_的仅限关键字参数传入'class'属性,
# 这是一种变通方法. 因为'class'是Python中的关键字.

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所示.
# 示例7-10 tag函数 (参见示例7-9) 众多调用方式中的几种

# 传入单个位置参数, 生成一个指定名称的空标签。
>>> tag('br')
'<br />'

# 第一个参数后面的任意数量的位置参数被*content捕获, 存入一个元组.
>>> tag('p', 'hello')
'<p>hello</p>'

>>> print(tag('p','hello','world'))
<p>hello</p>
<p>world</p>

# tag函数签名中没有明确指定名称的关键字参数被**attrs捕获, 存入一个字典.
>>> tag('p','hello',id=33)
'<p id="33">hello</p>'

# class_参数只能作为关键字参数传入.
>>> print(tag('p', 'hello','world', class_='sidebar'))
<p class="sidebar">hello</p>
<p class="sidebar">world</p>

# 第一个位置参数也能作为关键字参数传入(name="img").
>>> tag(content='testing', name="img")
'<img content="testing" />'

>>> my_tag = {'name':'img','title':'Sunset Boulevard',
              'src': 'sunset.jpg', 'class': 'framed'}

# 在my_tag前面加上**, 字典中的所有项作为参数依次传入, 
# 同名键绑定到对应的具名参数上, 余下的则被**attrs捕获.
# 在这个字典中可以使用'class'作为键, 因为它是字符串, 与保留字class不冲突.
>>> 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表达式解决这个问题.
# 示例7-11 使用reduce函数和匿名函数计算阶乘
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那样.
# 示例7-12 使用reduce函数和operator.mul函数计算阶乘
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更容易写出代码, 而且可读性更高.
# 示例7-13 演示使用itemgetter排序一个元组列表(数据来自示例2-8)
>>> 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

# 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))  # 输出: 2

# 映射示例
my_dict = {'a': 1, 'b': 2, 'c': 3}
get_value_c = itemgetter('c')
print(get_value_c(my_dict))  # 输出: 3


# 类示例
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))  # 输出: 3

与itemgetter的作用类似, attrgetter创建的函数会根据名称来提取对象的属性.
如果传给attrgetter多个属性名, 那么它也会返回由提取的值构成的元组.
此外, 如果参数名中包含.(点号), 那么attrgetter就会深入嵌套对象, 检索属性.
这些行为如示例7-14所示.
这个控制台会话不短, 因为我们要构建一个嵌套结构, 以展示attrgetter如何处理包含点号的属性名.
# 示例7-14 演示使用attrgetter 处理前文定义的具名元组metro_data(会用到示例7-13定义的列表)

>>> from collections import namedtuple
# 使用namedtuple定义LatLon.
>>> LatLon = namedtuple('LatLon', 'lat lon')
# 再定义Metropolis.
>>> Metropolis = namedtuple('Metropolis','name cc pop coord')

# 使用Metropolis实例构建metro_areas列表.
# 注意, 这里会使用嵌套的元组拆包提取(lat, lon),
# 然后使用它们构建LatLon, 作为Metropolis的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], 获取它的纬度.
>>> metro_areas[0].coord.lat 
35.689722

>>> from operator import attrgetter
# 定义一个attrgetter, 获取name属性和嵌套的coord.lat 属性.
>>> name_lat = attrgetter('name','coord.lat') 

# 再次使用attrgetter, 按照纬度排序城市列表.
>>> for city in sorted(metro_areas, key=attrgetter('coord.lat')): 
        # 使用↑上面中定义的attrgetter, 只显示城市名和纬度.
... 	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所示.
# 示例7-15 methodcaller使用示例: 第二个测试会展示绑定额外参数的方式
>>> 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是一个简单演示.
# 示例7-16 使用partial 把个双参数函数改造成只需要一个参数的可调用对象
>>> from operator import mul
>>> from functools import partial
# 使用mul创建triple函数, 把第一个位置参数绑定为3.
>>> triple = partial(mul, 3)
# 测试triple函数.
>>> triple(7) 
21

# 在map中使用triple, 在这个示例中不能使用mul(因为mul需要两个参数, 结合partial才能使用).
>>> 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所示.

# 示例7-17 使用partial构建一个便利的Unicode规范化函数
>>> 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冻结了一个位置参数和一个关键字参数.
# 示例7-18 把partial应用到示例7-9中定义的tag函数上

>>> from tagger import tag
# 从示例7-9中导入tag函数, 查看它的ID.
>>> tag
<function tag at 0x10206d1e0>

>>> from functools import partial
# 使用tag创建picture函数, 把第一个位置参数固定为'img', 把关键字参数class_固定为'pic-frame'.
>>> picture = partial(tag, 'img', class_='pic-frame')
# picture的行为符合预期.
>>> picture(src='wumpus.jpeg')
'<img class="pic-frame" src="wumpus.jpeg" />'

# partial()返回一个functools.partial对象. ④
# (注4: functools.py的源码表明, functools.partial是使用C语言实现的, 而且默认使用这个实现.
# 如果这个实现不可用, 则可以使用从Python3.4起functools模块为partial提供的纯Python实现.)
>>> picture
functools.partial(<function tag at 0x10206d1e0>, 'img', class_='pic-frame')

# functools.partial 对象提供了访问原函数和固定参数的属性.
>>> 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.

*---------------------------------------------------------------------------------------------*
  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值