Fluent Python 译本 读书笔记 第5章 一等函数

这是学习《流畅的Python》的第二天,今天希望把这一章看完,之所以跳过第二部分,是因为我对第三部分兴趣更多一些,之后再看第二部分吧。笔记不是对书的重复,只是把我接触到的任何不是特别熟悉的东西记录下来,会有点散乱。

一等对象 Page 118

在Python中,函数是一等对象。编程语言理论家把“一等对象”定义为满足下述条件的程序实体:
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果

高阶函数 Page 120

接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher-order function)。例如map函数、sorted函数。
sorted函数的可选的参数key用于提供一个单参数函数。这个单参数函数的返回值是一个可比较大小的对象。

def reverse(word):
    return word[::-1]
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry']
sorted(fruits, key=reverse)

map、filter、和reduce的现代替代品 Page 121

map和filter

函数式语言通常会提供mapfilterreduce三个高阶函数(有时使用不同的名称)。在Python3中,mapfilter还是内置函数,但是由于引入了列表推导和生成器表达式,它们变得没那么重要了。列表推导生成器表达式具有mapfilter两个函数的功能,而且更易于阅读。

>>> def fact(n):
        return 1 if n < 2 else n * fact(n-1)
        # 这里的语法:(条件为真的值) if (条件) else (条件为假的值)
>>> list(map(fact, range(6)))
[1, 1, 2, 6, 24, 120]
>>> [fact(n) for n in range(6)]
[1, 1, 2, 6, 24, 120]
>>> list(map(fact, filter(lambda n: n % 2, range(6))))
[1, 6, 120]
>>> [fact(n) for n in range(6) if n % 2]
[1, 6, 120]
>>> # 这里for if连用,效果应该是if嵌套在for里面,且两者顺序不能颠倒。

这个新点子真的很棒,起初学mapfilter的时候不太好懂,现在知道,用列表推导或生成器表达式就可以完成mapfilter的所有功能了,而且还非常的直观。
下面总结一下Python中的各种推导式(comprehensions),也叫解析式。推导式是可以从一个数据序列构建另一个新的数据序列的结构体。它包括三种:列表、集合、字典推导式。
生成器表达式

(f1(i) for i in obj if f2(i))

列表推导式

[f1(i) for i in obj if f2(i)]

集合推导式

{f1(i) for i in obj if f2(i)}

字典推导式

{f1(k, v): f2(k, v) for k, v in obj if f3(k, v)}

注意:以上只是说明了推导式(或者表达式)的格式,if语句可以没有(如果不需要筛选的话),for可以是任意形式的迭代。

reduce

在Python3中,reduce不再是内置函数,放到functools模块里了。这个函数最常用于求和,但求和最好使用内置的sum函数。在可读性和性能方面,这是一项重大改善。

>>> from functools import reduce
>>> from operator import add
>>> reduce(add, range(100))
4950
>>> sum(range(100))
4950

sumreduce的通用思想是把某个操作连续应用到序列的元素上,累计之前的结果,把一系列值归约成一个值。allany也是内置的规约函数。

all(iterable)
# 如果iterable中存在元素为假,才为假。否则为真。我之所以这么说是因为all([])为True
any(iterable)
# 如果iterable中存在元素为真,才为真。否则为假。我之所以这么说是因为any([])为False

5.3 匿名函数 Page 122

lambda关键字在Python表达式内创建匿名函数。Python简单的语法限制了lambda函数的定义体只能使用纯表达式。换句话说,lambda函数的定义体中不能赋值,也不能使用trywhile等Python语句。
在参数列表中最适合使用匿名函数。

>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'bananaa']
>>> sorted(fruits, key=lambda word: word[::-1])
['bananaa', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']

lambda语法

lambda 参数: 返回值

其中参数允许是单个或多个位置参数,逗号分开,也可以不提供参数。
lambda句法只是语法糖:与def语句一样,lambda表达式会创建函数对象。

5.4 可调用对象 Page 122

除了用户定义的函数,调用运算符()还可以应用在其他对象上。callable()可以用来判断对象是否可以调用。Python数据模型文档列出了7种可调用对象。

  • 用户定义的函数
    使用def语句或lambda表达式创建
  • 内置函数
    使用C语言(CPython)实现的函数,如lentime.strftime
  • 内置方法
    使用C语言实现的方法,如dict.get
  • 方法
    在类的定义体中定义的函数

  • 调用类时会运行类的__new__方法创建一个实例,然后运行__init__方法,初始化实例,最后把实例返回给调用方。因为Python中没有new运算符,所以调用类相当于调用函数。(通常,调用类会创建那个类的实例,不过覆盖__new__方法的话,也可能出现其他行为)
  • 类的实例
    如果类定义了__call__方法,那么它的实例可以作为函数调用。
  • 生成器函数
    使用yield关键字的函数或方法。调用生成器函数返回的是生成器对象。生成器函数在很多方面与其他可调用对象不同,生成器函数还可以作为协程。

shuffle Page 123

shuffle() 方法将序列的所有元素随机排序。原型:

def shuffle(self, x, random=None):
    """Shuffle list x in place, and return None.

    Optional argument random is a 0-argument function returning a
    random float in [0.0, 1.0); if it is the default None, the
    standard random.random will be used.
    """

另外从源码可以看出,不仅是list,实现了__getitem____setitem____len__方法的对象都可以应用于shuffle方法。

>>> import random
>>> a = [1,2,3]
>>> random.shuffle(a)
>>> a
[3, 1, 2]

函数的属性 Page 125

>>> def x(): pass
>>> dir(x) # 通常的函数具有的属性
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> class A: pass
>>> sorted(set(dir(x)) - set(dir(A()))) # 函数专有的而用户定义的一般对象没有的属性
['__annotations__', '__call__', '__closure__', '__code__', '__defaults__', '__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']

表5-1:用户定义的函数的属性

名称 类型 说明
__annotations__ dict 参数和返回值的注解
__call__ method-wrapper 实现()运算符;即可调用对象协议
__closure__ tuple 函数闭包,即自由变量的绑定(通常是None
__code__ code 编译成字节码的函数元数据和函数定义体
__defaults__ tuple 形式参数的默认值
__kwdefaults__ dict 仅限关键字形式参数的默认值
__get__ method-wrapper 实现只读描述符协议
__globals__ dict 函数所在模块中的全局变量
__name__ str 函数名称
__qualname__ str 函数的限定名称,如Random.choice

关于__dict__
与用户定义的常规类一样,函数使用__dict__属性存储赋予它的用户属性。这相当于一种基本形式的注解。一般来说,为函数随意赋予属性不是很常见的做法,但是Django框架这么做了。参见“The Django admin site”文档中对short_descriptionbooleanallow_tags属性的说明。下面的示例,把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'
# 我:函数是function类的实例,在Python中,一般类的实例都可以动态绑定属性,我觉得这也算是动态绑定属性了吧

5.7 从定位参数到仅限关键字参数 Page 126

Python最好的特性之一是提供了极为灵活的参数处理机制,而且Python3进一步提供了仅限关键字参数(keyword-only argument)。与之密切相关的是,调用函数时使用***“展开”可迭代对象,映射到单个参数。
下面的示例展示了这些特性,tag函数用于生成HTML标签。

def tag(name, *content, cls=None, **attrs):
    """生成一个或多个HTML标签"""
    if cls is not None:
        attrs['class'] = cla
    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' % (name, attr_str, c, name) for c in content)
    else:
        return '<%s%s />' % (name, attr_str)

tag的调用方式很多,如下:

>>> tag('br')
'
'
>>> tag('p', 'hello') '

hello

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

hello

world

>>> tag('p', 'hello', id=33) '

hello

'
>>> print(tag('p', 'hello', 'world', cls='sidebar'))

class="sidebar">hello

class="sidebar">world

>>> tag(content='testing', name='img') # 注意这里的content!!! ' >>> my_tag = {'name': 'img', 'title': 'Sunset Boulevard', 'src': 'sunset.jpg', 'cls': 'framed'} >>> tag(**my_tag) 'class="framed" src="sunset.jpg" title="Sunset Boulevard" />'

仅限关键字参数
仅限关键字参数是Python3新增的特性。上例中cls参数只能通过关键字参数指定,它一定不会捕获未命名的定位参数(因为定位参数都被*获取了)。定义函数时若想指定仅限关键字参数,要把他们放到前面有*的参数后面。如果不想支持数量不定的定位参数,但是想支持仅限关键字参数,在签名中放一个*,如下所示:

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

注:这样子,位置参数有且只能有1个,即a

5.8 获取关于参数的信息 Page 127

下面将介绍几个函数的属性:__defaults____code__.co_varnames__code__.co_argcount

def clip(text, max_len=80):
    """在max_len前面或后面的第一个空格处截断文本"""
    end = None
    if len(text) > max_len:
        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()

下面查看这个函数那些属性的值:

>>> clip.__defaults__
(80,)
>>> clip.__code__ # 我:这里应该是64位操作系统虚拟地址了,应该是函数结构体的位置吧(最近在学操作系统)
0x0000010FEA7E59C0>
>>> clip.__code__.co_argcount
2
>>> clip.__code__.co_varnames
('text', 'max_len', 'end', 'space_before', 'space_after')

可以看出,这种组织信息的方式并不是最便利的。参数名称在__code__.co_varnames中,不过里面还有函数定义体中创建的局部变量。因此参数名称是前N个字符串,N的值由__code__.co_argcount确定。顺便说一下,这里不包含前缀为***的变长参数。参数的默认值只能通过他们在__defaults__元祖中的位置确定,因此要从后向前扫描才能把参数和默认值对应起来。在这个示例中clip函数有两个参数,textmax_len,其中一个有默认值,即80,因此它必然属于最后一个参数,即max_len。这有违常理。
幸好,我们有更好的方式——使用inspect模块。

>>> from inspect import signature
>>> sig = signature(clip)
>>> sig
text, max_len=80)>
>>> str(sig)
'(text, max_len=80)'
>>> for name, param in sig.parameters.items():
        print(param.kind, ':', name, '=', param.default)
POSITIONAL_OR_KEYWORD : text = 'inspect._empty'>
POSITIONAL_OR_KEYWORD : max_len = 80

这样就好多了。inspect.signature函数返回一个inspect.Signature对象,它有一个parameters属性,这是一个有序映射,把参数名和inspect.Parameter对象对应起来。各个Parameter属性也有自己的属性,例如namedefaultkind。特殊的inspect._empty值表示没有默认值,考虑到None是有效的默认值(也经常这么做),而且这么做是合理的。
kind属性的值是_ParameterKind类中的5个值之一,列举如下:

  • POSITIONAL_OR_KEYWORD
    可以通过定位参数和关键字参数传入的形参(多数Python函数的参数属于此类)
  • VAR_POSITIONAL
    定位参数元祖。
  • VAR_KEYWORD
    关键字参数字典。
  • KEYWORD_ONLY
    仅限关键字参数(Python3新增)。
  • POSITIONAL_ONLY
    仅限定位参数;目前,Python声明函数的句法不支持,但是有些使用C语言实现且不接受关键字参数的函数(如divmod)支持。
    除了 namedefaultkindinspect_Parameter对象还有一个annotation(注解)属性,它的值通常是inspect._empty,但是可能包含Python3新的注解句法提供的函数签名元数据。
    inspect.Signature对象有个bind方法,它可以把任意个参数绑定到签名中的形参上,所用的规则与实参到形参的匹配方式一样。框架可以使用这个方法在真正调用函数前验证参数。
>>> import inspect
>>> sig = inspect.signature(tag)
>>> my_tag = {'name': 'img', 'title': 'Sunset Boulevard', 'src': 'sunset.jpg', 'cls': 'framed'}
>>> bound_args = sig.bind(**my_tag)
>>> for name, value in bound_args.arguments.items():
        print(name, '=', value)
name = img
cls = framed
attrs = {'title': 'Sunset Boulevard', 'src': 'sunset.jpg}
>>> del my_tag['name']
>>> bound_args = sig.bind(**my_tag)
Traceback (most recent call last):
    ...
TypeError: 'name' parameter lacking default value

这个示例在inspect模块的帮助下,展示了Python数据模型把实参绑定给函数调用中的形参的机制,这与解释器使用的机制相同。

5.9 函数注解 Page 131

def clip(text:str, max_len:'int > 0'=80) -> str:
    pass

函数声明中的各个参数可以在:之后增加注解表达式。如果参数有默认值,注解放在参数名和=之间。如果想注解返回值,在)和函数声明末尾的:之间添加->和一个表达式。那个表达式可以是任何类型。注解中最常用的类型是类(如strint)和字符串(如'int > 0')。
注解作为元数据存储在函数的__annotations__属性(一个字典)中,其中return键保存的是返回值注解。Python对于注解什么操作都不做,不检查,不强制,不验证。但是注解可以供IDE、框架和装饰器等工具使用。目前标准库中唯有inspect.signature()函数知道怎么提取注解。

>>> from inspect import signature
>>> sig = signature(clip)
>>> sig.return_annotation
'str'>
>>> for param in sig.parameters.value():
    note = repr(param.annotation).ljust(13)
    print(note, ':', param.name, '=', param.default)
'str'> : text = 'inspect._empty'>
'int > 0'     : max_len = 80

signature函数返回一个Signature对象,它有一个return_annotation属性和一个parameters属性,后者是一个字典,把参数名映射到Parameter对象上。每个Parameter对象自己也有annotation属性。

5.10.1 operator模块 Page 132

>>> [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']

这54个名称中大部分的作用不言而喻。以i开头、后面是另一个运算符的那些名称(如iaddiand等),对应的是增量赋值运算符(如+=&=等)。如果第一个参数是可变的,那么这些运算符函数会修改它;否则,作用与不带i的函数一样,直接返回运算结果。
利用reduce和operator.mul计算阶乘

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

这样可以避免编写lambda a, b: a*b这种平凡的匿名函数。
利用operator.itemgetter排序一个元祖列表

>>> metro_data = [
        ('Tokyo', 'JP', 36.933, (35.6, 139.6)),
        ('Delhi NCR', 'IN', 21.935, (28.6, 77.2)),
        ('Mexico City', 'MX', 20.142, (19.4, -99.1)),
        ('New York-Newark', 'US', 20.104, (40.8, -74.0)),
        ('Sao Paulo', 'BR', 19.649, (-23.5, -46.6)),
    ]
>>> from operator import itemgetter
>>> for city in sorted(metro_data, key=itemgetter(2)): # 利用元祖第3个元素排序,它是个浮点数
        print(city)
('Sao Paulo', 'BR', 19.649, (-23.5, -46.6))
('New York-Newark', 'US', 20.104, (40.8, -74.0))
('Mexico City', 'MX', 20.142, (19.4, -99.1))
('Delhi NCR', 'IN', 21.935, (28.6, 77.2))
('Tokyo', 'JP', 36.933, (35.6, 139.6))

如果把多个参数传给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', 'Sao Paulo')

itemgetter 使用[]运算符,因此它不仅支持序列,还支持映射和任何实现__getitem__方法的类。
attrgetter
attrgetteritemgetter作用类似,它创建的函数根据名称提取对象的属性。如果把多个属性名传给attrgetter,它也会返回提取的值构成的元祖。此外,如果参数名中包含.(点号),attrgetter会深入嵌套对象,获取指定的属性。
下面的示例构建了一个嵌套结构,展示了attrgetter如何嵌套获取对象属性。它按照城市的纬度排序,并输出城市名和纬度。

>>> from collections import namedtuple
>>> LatLong = namedtuple('LatLong', 'lat long')
>>> Metropolis = namedtuple('Metropolis', 'name cc pop coord')
>>> metro_data = [
        ('Tokyo', 'JP', 36.933, (35.6, 139.6)),
        ('Delhi NCR', 'IN', 21.935, (28.6, 77.2)),
        ('Mexico City', 'MX', 20.142, (19.4, -99.1)),
        ('New York-Newark', 'US', 20.104, (40.8, -74.0)),
        ('Sao Paulo', 'BR', 19.649, (-23.5, -46.6)),
    ]
>>> metro_areas = [Metropolis(name, cc, pop, LatLong(lat, long)) for name, cc, pop, (lat, long) in metro_data]
>>> metro_areas[0].coord.lat
35.6
>>> from operator import attrgetter
>>> name_lat = attrgetter('name', 'coord.lat')
>>> for city in sorted(metro_areas, key=attrgetter('coord.lat')):
        print(name_lat(city))
('Sao Paulo', -23.5)
('Mexico City', 19.4)
('Delhi NCR', 28.6)
('Tokyo', 35.6)
('New York-Newark', 40.8)

methodcaller

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

这个示例展示了methodcaller的用法。methodcaller还可以冻结某些参数,也就是部分应用(partial application),这与functools.partial函数的作用类似。

5.10.2 使用functools.partial冻结参数 Page 135

functools模块提供了一些列高阶函数,其中最为人熟知的或许是reduce。余下的函数中,最有用的是partial及其变体,partialmethod
functools.partial这个高阶函数用于部分应用一个函数。部分应用指,基于一个函数创建一个新的可调用对象,把原函数的某些参数固定。使用这个函数可以把接收一个或多个参数的函数改编成需要回调的API,这样参数更少。

>>> from operator import mul
>>> from functools import partial
>>> triple = partial(mul, 3) # 把第一个定位参数定位3
>>> triple(7)
21
>>> [triple(i) for i in range(1, 10)]
[3, 6, 9, 12, 15, 18, 21, 24, 27]

partial的第一个参数是一个可调用对象,后面跟着任意个要绑定的定位参数和关键字参数。
functools.partialmethod函数(Python3.4新增)的作用与partial一样,不过是用于处理方法的。
functools模块中的lru_cache函数令人印象深刻,它会做备忘(memorization),这是一种自动优化措施,它会存储耗时的函数调用结果,避免重新计算。第7章将会介绍它,以及旨在用作装饰器的其他高阶函数:singledispatchwraps

你可能感兴趣的:(Python)