《流畅的Python第二版》读书笔记——函数作为一等对象

引言

这是《流畅的Python第二版》抢先版的读书笔记。Python版本暂时用的是python3.10。为了使开发更简单、快捷,本文使用了JupyterLab。

函数是Python的一等(first-class)对象。一等对象作为一个程序实体可以:

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

整数、字符串和字典是Python其他的一等对象的例子。本章和第三部分的大部分内容探讨了将函数作为对象来处理的实际应用。

新内容简介

无法直接找到对应的旧版本章节,基本上以前章节内容的整合,同时加入了新内容。

将函数看成对象

下面的代码显示了Python函数是对象。这里我们创建一个函数,调用它,读取__doc__属性,然后检查函数对象本身是function类的实例。

def factorial(n): # 这是控制台session,所以我们在运行时创建函数
    """返回 n!"""
    return 1 if n < 2 else n * factorial(n - 1)
factorial(42)
1405006117752879898543142606244511569936384000000000
factorial.__doc__ # 是function对象的几个属性之一
'返回 n!'
type(factorial) # factorial是function类的一个实例
function
help(factorial) # __doc__属性用于生成对象的帮助文本
Help on function factorial in module __main__:

factorial(n)
    返回 n!

下面显示一个函数对象的一等本质。我们可以将它赋值为变量fact,然后通过该名称调用它。我们也可以传递factorial作为map函数的参数。调用map(function, iterable)返回一个可迭代对象,其中,每项是将第一个参数(函数)调用到第二个参数(可迭代)的连续元素的结果,本例中为range(10)

fact = factorial
fact
<function __main__.factorial(n)>
fact(5)
120
map(factorial, range(11))
<map at 0x1c67cda15b0>
list(map(factorial, range(11)))
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]

具有一等的函数,可以以函数式风格进行编程。函数式编程的特点之一是使用高阶函数,我们的下一个主题介绍。

高阶函数

一个函数如果它能以函数作为参数或能返回函数,那么它就是高阶函数。一个例子就是上面演示的map,另一个例子是内置的函数sorted:可选的key参数可以让你提供一个函数来应用到要排序的每项。比如,要根据单词列表中每个单词的长度排序,将len函数作为key传递:

fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=len)
['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']

任何只有一个参数的函数都能作为这个key。比如,为了创建一个押韵字典,那么能通过每个单词拼写反向排序时非常有用的。
只有它们的逆序拼写作为排序准则,所以这些浆果(berries)出现在一起。

def reverse(word):
    return word[::-1]

reverse('testing')
'gnitset'
sorted(fruits, key=reverse)
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']

在函数式编程的范式中,一些最著名的高阶函数是mapfilterreduceapply。其中apply已经从Python3中移除。
如果你需要带动态参数集的方式调用一个函数,你可以通过fn(*args, **kwargs)而不是apply(fn, args, kwargs)

mapfilterreduce高阶函数仍然可用,但,如下一节所示,大多数情况下都有更好的替代者。

map,filter和reduce的最新替代者

函数式语言通常会提供map,filter,reduce等高阶函数。mapfilter函数也是Python3内置的,但自引入了列表推导式和生成器表达式后,它们不再那么重要了。一个列表推导式或生成器表达式同时完成了mapfilter的工作,而且更加可读。

list(map(factorial, range(6))) # 构建一个阶乘 0!到5!
[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)))) # 使用map和filter实现对奇数计算阶乘,从0到5
[1, 6, 120]
[factorial(n) for n in range(6) if n % 2] # 列表推导式的实现,可替代map和filter,并且不需要lambda
[1, 6, 120]

在Python3中,mapfilter返回生成器——一种迭代器形式——所以它们的直接替代者是生成器表达式。

reduce函数从Python 2中的内置函数降级到Python 3中的functools模块。它最常见的用例是,总和(summation),通过自Python2.3引入的内置函数sum来实现更好。可以同时带来可读性和性能的提升。

from functools import reduce # 自Python3.0,reduce不再是内置函数
from operator import add # 引入add避免创建一个只进行两个数的加法函数
reduce(add, range(100)) # 为从0到99的整数求和
4950
sum(range(100)) # 通过sum来完成同样的任务——所以不需要引入和调用reduce和add
4950

其中内建的reducing函数是allany:

  • all(iteralbe) 返回True如果iterable中没有False元素,all([])返回True
  • any(iterable 返回True如果iterable中任意一个元素为Trueany([])返回False

要使用高阶函数,有时可以方便地创建一个小的、一次性的函数。这就是为什么匿名函数(anonymous function)存在。接下来我们将介绍它们。

匿名函数

lambda关键字可以通过一个Python表达式创建一个匿名函数。

然而,简单的语法限制lambda函数体只能是纯表达式。即,函数体不能包含其他语句,比如whiletry等。通过=赋值语句也是一个语句(statement),所以它也不能出现在lambda表达式中,新的赋值表达式语法——:=——可以使用,但你使用它的话,你的lambda表达式会非常复杂,从而可读性不好。因此应该使用def重构为常规的函数。

匿名函数的最佳使用是在高阶函数的参数列表上下文中。比如,下面是通过lambda重写的押韵索引例子,不需要定义一个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的重构建议:

  1. 写注释来解释一下,lambda到底在做什么
  2. 研究一下评论,想出一个能抓住评论本质的名称
  3. 使用该名称将lambda转换为def语句
  4. 删除评论

lambda语法只是语法糖:一个lambda表达式创建一个就像def语句一样的函数对象。这只是Python中几种可调用对象之一。下面的部分将回顾一下所有的可调用对象。

九种常用的可调用对象

调用操作()可以应用到其他非函数对象。为了决定一个对象是否可调用,使用内建的callable()函数。正如Python3.9 data model文档中列出的9种可调用类型:

  • 用户定义的函数 通过def语句或lambda表达式创建
  • 内建函数 基于C(CPython)实现的函数,像lentime.strftime
  • 内建方法 基于C实现的方法,像dict.get
  • 方法 定义在类中的方法
  • 类 当调用时,一个类会运行它的__new__方法去创建一个实例,然后通过__init__去初始化它,最终该实例会返回给调用者。因为Python中无new操作符,所以调用一个类就像调用一个函数
  • 类实例 如果一个类定义了__call__方法,那么它的实例可被作为函数调用
  • 生成器函数 函数或方法中使用了yield关键字。当调用时,它们会返回一个生成器对象
  • 原生协程函数 函数或方法基于async def定义。当调用时,它们会返回一个协程对象
  • 异步生成器函数 函数或方法基于async def定义并在方法体中有yield关键字。当调用时,它们返回一个异步生成器

生成器、原生协程和异步生成器函数与其他可调用对象不同,它们的返回值不是应用数据,而是需要进一步处理以生成应用程序数据或执行有用的工作的对象。生成器函数返回迭代器(iterator)。这两个都会在17章中接收。原生协程函数和异步生成器函数返回的对象只能在异步编程框架中可用,比如asyncio。这是第21章的主题。

考虑到Python中现有可调用类型的多样性,确定对象是否可调用的最安全方法是使用内建的callable()

abs, str, 'Ni!'
(<function abs(x, /)>, str, 'Ni!')
[callable(obj) for obj in (abs, str, 'Ni!')]
[True, True, False]

用户定义的可调用类型

任意的Python对象的行为都可能和函数一样,只要实现__call__实例方法。

下面的例子实现了一个BingoCage类。实例是由任何可迭代对象构建的,并以随机顺序存储内部itmes列表。调用它的实例会弹出其中一个item元素。

import random

class BingoCage:
    def __init__(self, items):
        self._items = list(items)  # __init__方法接收任何可迭代对象,构建一个本地副本
        random.shuffle(self._items)  # 对_items进行洗牌
        
    def pick(self):  # 主要的方法
        try:
            return self._items.pop()
        except IndexError:
            raise LookupError('pick from empty BingoCage')  # 如果self._item为空,则抛出异常
            
    def __call__(self):  # bingo.pick() 等同于 bingo()
        return self.pick()

下面来实验一下。注意bingo实例可像函数一样被调用,并且callable()也知道它是可调用对象:

bingo = BingoCage(range(3))
bingo.pick()
1
bingo()
0
callable(bingo)
True

实现__call__的类是一种简单的方式去创建类函数的对象,它们可具有一些在调用之间保存的内部状态,像BingoCage中剩下的items一样。
另一个__call__的用例是实现装饰器。装饰器必须是可调用的,而且如果能记住调用间的状态很方便,或将复杂的实现拆分成几个方法。

函数式途径创建具有内部状态的函数是闭包(closure),它和装饰器是第9章的主题。

从位置到仅关键字参数

Python函数的最好特性之一是及其灵活的参数处理机制。密切相关的是,当我们调用一个函数时,使用***将迭代内容和映射解包到单独的参数中。
下面通过代码来看一下该特性。

def tag(name, *content, class_=None, **attrs):
    """Generate one or more HTML tags"""
    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函数可以通过多种方式调用:

tag('br') # 单个位置位置产生该name对应的空tag
'<br />'
tag('p', 'hello') # 在第一个参数之后的任意数量参数由*content捕获为元组
'<p>hello</p>'
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', class_='sidebar')) #  class_参数只能作为关键字参数传入
<p class="sidebar">hello</p>
<p class="sidebar">world</p>
tag(content='testing', name="img") # 第一个位置参数(name)也能通过关键字的形式传入
'<img content="testing" />'
my_tag = {'name': 'img', 'title': 'Sunset Boulevard','src': 'sunset.jpg', 'class': 'framed'}
# 在字典my_tag前加**会将该字典中所有的项作为分开的参数传入,它们然后会绑定到命名参数,未匹配的会由**attrs捕获。
# 这个例子中,my_tag字典中我们可以有一个class关键字,因为它是一个字符串,并且不与类保留词发生冲突。
tag(**my_tag)
'<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />'

仅关键字(Keyword-only)参数是Python3的特性。在上面的例子中,class_参数只能通过关键字参数的形式给定——永远不会捕获未命名的位置参数。
为了在定义函数时指定仅关键字参数,将这些参数命名,且放到具有*前缀的参数之后。如果你不想支持可变位置参数,但仍想要仅关键字参数,那么将*单独放到签名中,像:

def f(a, *, b):
    return a, b
f(1, b=2)
(1, 2)
f(1,2)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

Cell In [38], line 1
----> 1 f(1,2)


TypeError: f() takes 1 positional argument but 2 were given

注意仅关键字参数不需要有默认值:它们可以是强制性的,就像上面的例子。

仅位置参数

自Python3.8以来,用户定义的函数签名可以指定仅位置(positional-only)参数。该特性已经存在于内建函数,比如divmod(a, b),它仅能通过位置参数调用。
为了定义一个需要仅位置参数,在参数列表中使用/

def divmod(a, b, /):
    return (a // b, a % b)

所有/左边的参数都是仅位置参数,在/右边,可以指定其他参数。比如,考虑上面的tag函数,如果我们想要name参数变为仅位置,我们可以增加/name后面,像:

def tag(name, /, *content, class_=None, **attrs):

用于函数式编程的包

operator模块

通常在函数式编程中,使用算术操作符作为函数是很方便的。例如,假设你想要乘以一个数字序列以计算阶乘,而不使用递归。要执行求和,可以使用sum,但是没有乘法的等价函数。你可以使用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。我们通过它来重写上面的例子。

from functools import reduce
from operator import mul
def factorial(n):
    return reduce(mul, range(1, n+1))

另一组lambda技巧是操作符替换函数从序列中选择项或从对象中读取属性:itemgetterattrgetter是实现它的构建自定义函数的工厂。

下面显示了itemgetter的常用用法:按一个字段的值对元组列表进行排序。在示例中,cities按国家代码(字段1)排序。本质上,itemgetter(1)创建一个函数,给定一个集合,该函数返回索引1处的项。这比做同样事的lambda字段更容易写和读:fields[1]

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中传递多个索引,它会以元组的形式返回对应值,这在基于多key排序时很有用:

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类似的attrgetter,它创建了按名称提取对象属性的函数。如果传递多个属性名称到attrgetter中作为参数,它还将返回一个值的元组。
此外,如果如果任何参数名称包含一个.attrgetter会通过嵌套对象来检索属性。这些行为在下面展示。

from collections import namedtuple
LatLon = namedtuple('LatLon', 'lat lon') #
Metropolis = namedtuple('Metropolis', 'name cc pop coord') 
# 通过 Metropolis实例构建metro_areas列表,注意嵌套的元组拆包抽取(lat,lon),并使用它们为Metropolis的coord属性构建LatLon
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') # 定义一个attrgetter来检索name和coord.lat嵌套属性

for city in sorted(metro_areas, key=attrgetter('coord.lat')): # 使用attrgetter基于latitude排序列表
    print(name_lat(city)) # 用上面定义的attrgetter来限定只显示name和latitude
('São Paulo', -23.547778)
('Mexico City', 19.433333)
('Delhi NCR', 28.613889)
('Tokyo', 35.689722)
('New York-Newark', 40.808611)

下面是在operator中定义的部分函数列表:

import 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',
 'ipow',
 'irshift',
 'is_',
 'is_not',
 'isub',
 'itemgetter',
 'itruediv',
 'ixor',
 'le',
 'length_hint',
 'lshift',
 'lt',
 'matmul',
 'methodcaller',
 'mod',
 'mul',
 'ne',
 'neg',
 'not_',
 'or_',
 'pos',
 'pow',
 'rshift',
 'setitem',
 'sub',
 'truediv',
 'truth',
 'xor']

i为前缀和另一个运算符的构成的名称——如iaddiand等_对应于原地赋值运算符——如+=&=等。

下面我们介绍methodcaller,它有点类似attrgetteritemgetter,可以动态地创建一个函数。创建的函数基于name调用给定对象中的方法。

from operator import methodcaller
s = 'The time has come'
upcase = methodcaller('upper')
upcase(s) # s.upper()
'THE TIME HAS COME'
hyphenate = methodcaller('replace', ' ', '-')
hyphenate(s)
'The-time-has-come'

用functools.partial冻结参数

functools模块提供了几个高阶函数。我们已经了解了reduce。另一个是partial:给定一个可调用对象,它生成一个绑定参数预定值到原来可调用对象的新的可调用对象。这对于将接受一个或多个参数的函数调整到需要具有较少参数的回调的API很有用。

from operator import mul
from functools import partial
triple = partial(mul, 3) # 从mul创建新的triple函数,并将第一个位置参数绑定到3
triple(7) # 测试
21
list(map(triple, range(1, 10))) # 在map中使用triple,在该例子中不能使用mul
[3, 6, 9, 12, 15, 18, 21, 24, 27]

一个更有用的例子涉及到Unicode。如果你使用来自多种语言的文本,你可能需要应用unicode。在比较或存储它之前,对任何字符串应用 unicode.normalize('NFC', s)。如果你经常这样做,那么要有一个nfc函数就很方便了,如示例所示。

import unicodedata, functools
nfc = functools.partial(unicodedata.normalize, 'NFC') # 把NFC绑定到normalize第一个参数

s1 = 'café'
s2 = 'cafe\u0301'
s1, s2
('café', 'café')
s1 == s2
False
nfc(s1) == nfc(s2)
True

partial以可调用对象作为第一个参数,接着是要绑定的任意数量的位置和关键字参数。

下面显示使用partial在之前的tag函数上,来固定一个位置参数和一个关键字参数。

from functools import partial
picture = partial(tag, 'img', class_='pic-frame') # 通过固定第一个位置参数为`img`,和`class_`关键字参数为`pic-frame`来创建picture函数
picture(src='wumpus.jpeg') # 正如期望般的运行
'<img class="pic-frame" src="wumpus.jpeg" />'
picture # partial() 返回一个 functools.partial 对象
functools.partial(<function tag at 0x000001C67D6D69D0>, 'img', class_='pic-frame')
picture.func # functools.partial有访问原始函数和固定参数的属性
<function __main__.tag(name, *content, class_=None, **attrs)>
picture.args
('img',)
picture.keywords
{'class_': 'pic-frame'}

functools.partialmethod做的事情类似partial,但被设计成应用于方法。

参数总结

上文介绍了很多种参数类型,在结束之前,这里做一个总结。参考 https://docs.python.org/3/glossary.html#term-parameter

有五种类型的参数:

  • 位置或关键词(positional-or-keyword): 指定一个可以作为 位置参数 传入也可以作为 关键字参数 传入的实参。这是默认的形参类型,例如下面的 foobar:
def func(foo, bar=None): ...
  • 仅位置(positional-only): 指定一个只能通过位置传入的参数。 仅限位置形参可通过在函数定义的形参列表中为它们之后包含一个 / 字符来定义,例如下面的 posonly1posonly2:
def func(posonly1, posonly2, /, positional_or_keyword): ...
  • 仅关键字(keyword-only): 指定一个只能通过关键字传入的参数。仅关键字形参可通过在函数定义的形参列表中包含单个可变位置形参或者在多个可变位置形参之前放一个*来定义,例如下面的 kw_only1kw_only2:
def func(arg, *, kw_only1, kw_only2): ...
  • 可变位置(var-positional): 指定可以提供由一个任意数量的位置参数构成的序列(附加在其他形参已接受的位置参数之后)。这种形参可通过在形参名称前加*来定义,例如下面的 args:
def func(*args, **kwargs): ...
  • 可变关键字(var-keyword): 指定可以提供任意数量的关键字参数(附加在其他形参已接受的关键字参数之后)。这种形参可通过在形参名称前加 ** 来定义,例如上面的 kwargs
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愤怒的可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值