FluentPythonCh10

24th August 2017 at 10:39am
Fluent Python

这章主要构建了一个教学 Vector 类,对 Python 的数据模型做了更深入的探讨。每一节的内容都比较相关,因此我不再按章节写笔记,而是总的写在一起。

Slicing

def __getitem__(self, index):
    cls = type(self)
    if isinstance(index, slice):
        return cls(self._components[index])
    elif isinstance(index, numbers.Integral):
        return self._components[index]
    else:
        msg = '{cls.__name__} indices must be integers'
        raise TypeError(msg.format(cls=cls))

调用 v1[0] 时传入的是整数,调用 v1[0:3] 这种传入的是 slice 对象。方括号中的 0:3 可以说是一种 slice literal。

S.indices(len) -> (start, stop, stride) 这个函数非常实用,它可以把 negative index 转成正的,方便你做一些操作:

>>> slice(None, 10, 2).indices(5)
(0, 5, 2)
>>> slice(-3, None, None).indices(5)
(2, 5, 1)

Dynamic Attribute Access

Python 的 instance attribute 存在类的 __dict__ 中,class attributes 存在类的 __class__ 中。当你访问某个 attributes 时,Python 会先从这两个地方找变量,如果没找到会调用 __getitem__ 函数。

所以要实现 Dynamic Attribute Access,你要实现 __getitem__ 函数,同时最好实现配套的 __setitem__ 函数。

Hashing

如果你想把一个类实例作为 set dict 的 key,你需要让他不可变 ,同时保证它的 hash() 值在整个生命周期保持不变。

一般的实现方式是,将类中的 attributes 先 hash 再异或,对于多个 attributes 的情况,map reduce 很适合这种场景:

def __hash__(self):
    hashes = map(hash, self._components)
    return functools.reduce(operator.xor, hashes, 0)

Python 3 vs Python 2

Python 3 把很多内置函数从生成一个 list,变成返回一个 iterator,比如 map zip 等。这对内存和性能都有很大帮助。

The Awesome zip

zip 内置函数可以把多个 iterable 像拉链一样重组:

>>> list(zip(range(3), 'ABC'))
[(0, 'A'), (1, 'B'), (2, 'C')]

但是它在最短的 iterable 消耗完就停止了。你可以用 itertools.zip_longest 提供默认值,使最长的 iterable 也可以被 zip:

>>> list(zip(range(3), 'ABCD'))
[(0, 'A'), (1, 'B'), (2, 'C')]
>>> from itertools import zip_longest
>>> list(zip_longest(range(3), 'ABCD', fillvalue=-1))
[(0, 'A'), (1, 'B'), (2, 'C'), (-1, 'D')]

The Search for a Pythonic Sum

Future Reading 里面有一个值得一看的故事,讲述大家在讨论 Pythonic way to sum n-th list element? 的故事。当时 Python 并没有 sum 内置函数,于是一些有 OOP 经验的人使用了 functools.reducelambda 来写:

>>> my_list = [[1, 2, 3], [40, 50, 60], [9, 8, 7]]
>>> import functools
>>> functools.reduce(lambda a, b: a+b, [sub[1] for sub in my_list])
60

但是这实在是太丑了,即有 lambda 又有 reduce,还有 list comprehension。于是大家写出了优化版本:

>>> functools.reduce(lambda a, b: a + b[1], my_list, 0)
60
>>> functools.reduce(operator.add, [sub[1] for sub in my_list], 0)
60

但是 reduce 会使得求和 (sum) 的意图不明。有人觉得这样写还不如直接:

>>> t = 0
>>> for sub in my_list:
...     total += sub[1]
>>> t
60

对此有两段评论说得好:

I like Evan Simpson’s code but I also like David Eppstein’s comment on it:
If you want the sum of a list of items, you should write it in a way that looks like “the sum of a list of items”, not in a way that looks like “loop over these items, maintain another variable t, perform a sequence of additions”. Why do we have high level languages if not to express our intentions at a higher level and let the language worry about what low-level operations are needed to implement it?
Then Alex Martelli comes back to suggest:
“The sum” is so frequently needed that I wouldn’t mind at all if Python singled it out as a built-in. But “reduce(operator.add, …” just isn’t a great way to express it, in my opinion (and yet as an old APL’er, and FP-liker, I should like it—but I don’t).

最终导致 3 个月后 Python 添加了内置的 sum 函数。于是可以:

>>> sum([sub[1] for sub in my_list])
60

这也说明了为何 Python 3 把 map reduce 放到 functools 里面而不放在内置函数中。它们缺乏表达能力。

Protocol v.s. Interface

Python 里面的 dunder 函数,只能算做一种 protocol,而非 interface。protocol 像是一种不太强的约定,而 interface 是一种强的约定。比如我实现了 __getitem__ 来取下标,但是如果对于我的类,slicing 并没有意义,那我可以不实现对 slice 的处理。而 interface 表示你必须实现它约定的一系列函数。