放假的电话

Fluent Python读书笔记

Chapter 1: The Python Data Model

Python的数据模型可以理解为一些实现了特殊方法的对象。这些特殊方法一般是__xxx__的形式(行话叫dunder method)。Python通过调用这些特殊函数,实现了一系列的操作。

例如,obj[key]方法实际是obj.__getitem__(key)的调用(dunder getitem)。

实现了__getitem__方法的对象obj,可以进行如下面的操作:

1
2
3
4
for item in obj:
pass
a = item[:3]


内置方法的调用并不一定是明确的。

对于内置的数据类型如list,str, len()方法并不是调用__len__方法,而是读取CPython实现PyVarObject C 结构里的的ob_size,这样比较快。

对于for i in x,实际调用的是iter(x),而后者又会调用x.__iter__(),如果定义了的话。

内置方法

  1. __len__: len()调用
  2. __getitem__: obj[idx], obj[key], for _ in obj等等
  3. __repr__: is called by the repr built-in to get the string represen‐ tation of the object for inspection. The string returned by __repr__ should be unambiguous and, if possible, match the source code necessary to re-create the object being represented.
  4. __str__: print以及str()调用,跟__repr__区别微妙。Difference between __str and __repr
  5. __bool__: bool(x) or if x. 如果 __bool__ 没有实现,那么会试图调用x.__len__(),非0则返回True

更多见书39页。

Chapter 2: An Array of Sequences

Python有2种内置的sequence类型:

  1. Container sequences
    list, tuple, collections.deque, 包含了对象的引用,可以使任何的对象
  2. Flat sequences
    str, bytes, bytearray, memoryview, array.array, 存储的是实际的值,会比较节省空间,但是只能是同一类型的、内置的数据类型如character, byte, number。

List comprehensions and Generator Expressions

在python2里,listcomps的变量有可能会leak,在python3里,这个问题被修复了,现在它们有自己的local作用域。

1
2
3
x = 'hello'
l = [x for x in 'ABC']
print x # 'C'

Generator expression跟list comprehension很接近,区别是放在()里。Genexp并不会生成完整的list,而是用yield产生每个值,所以比较节省空间。

Tuples Are Not Just Immutable Lists

Using * to grab excess items when unpacking tuples:

1
2
3
>>> a, b, *rest = range(5)
>>> a, b, rest
(0, 1, [2, 3, 4])

namedtuple

namedtuple有比常规的class占用空间小,三个非常有用的方法:

1
2
3
4
City = namedtuple('City', 'name state')
City._fileds
foster_city = City._make(('Foster City', 'CA'))
foster_city._asdict()

slice

slice调用了seq.__getitem__(slice(start, stop, step)),因此,可以命名slice,使得代码更可读。例如:

1
2
3
4
s = 'hello world'
hello = slice(0, 5)
world = slice(6, 11)
print s[hello], s[world]

using += with sequences

+=调用了__iadd(in-place addition)。因此,在sequence上使用+=时,会修改当前的sequence。但是,如果是一个immutable的sequence,如tuple,它并没有实现__iadd方法,那么调用a += b相当于a = a + b。看似还是a在变,实际发生的是完全不同的事,a的id会变化。

python对str的+=进行了优化,为str分配了额外的空间,所以,+=可以直接在后面append,是in-place的,而不用生成新的str,复制原有str的所有内容。

bisect

python内置的实现了二分搜索的模块。之前我都没听说过,发现某些场合下可能会很好用。书中提供了一个grade函数的非常优雅的实现:

1
2
3
def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'):
i = bisect.bisect(breakpoints, score)
return grades[i]

array.array

这又是一个没怎么听过的模块。针对array的优化,类似list,但是在存储空间跟某些操作上,都优于list。

deque and other queues

deque可以设置maxlen,会自动drop多余的元素。这个操作之前不知道,一些情况下感觉会很方便。我觉得overuse list确实不太好,应该要多注意使用python内置的其他一些数据结构。deque还有一个rotate方法,可以一次性移动一段数据。

deque的append以及popleft方法是原子的,因此不需要lock就是thread safe。

Memory View

可以用来实现类似c的内存操作。

Chapter 3: Dictionaries and Sets

dict有个方法叫setdefault,虽然不常用,但是需要的时候很方便。

1
2
3
if key not in my_dict:
my_dict[key] = []
my_dict[key].append(new_value)

等价于

1
my_dict.setdefault(key, []).append(new_value)

如果不使用dict,上面的代码也能写成:

1
2
my_dict = collections.defaultdict(list)
my_dict[key].append(new_value)

只读的dict

python3.3添加了一个types.MappingProxyType,可以生成一个只读的dict对象。

set

found = len(set(needles) & set(haystack))可以找出needles中同时属于haystack的元素,非常pythonic。

set和dict的实现

dict内部通过一个hashtable实现,一般这个hashtable总是有大约三分之一的空闲。如果小于这个值,会重新分配更大的空间,并把现有的值拷贝进新空间。在进行dict[key]的时候,首先算出hash(key)或者key.__hash__(),用结果的hash值的后几位(取决于当前hashtable的大小)最为index,在hashtable中查找。如果对应的index为空,则KeyError。如果不为空,且对应index的(key, value)match当前的key,hit。如果key不match,则根据特定算法取hash中的某几位作为index,再次搜索,直到KeyError或者hit。

由于用了稀疏的hashtable,dict在空间上不是很efficient。

所以,如果要存储大量数据,list of dict的空间会比list of namedtuple多很多。如果保证key是一定的,用namedtuple会比价好。

python3里, .keys(), .items(), .values()返回的是view,行为更接近set, 而是不是list。同时,它们也可以直接反应dict变化后的值。

Chapter 4: Text versus Bytes

python3里的str返回的是类似python2里unicode的东西。python3里的str经过encoding后,返回一个bytes。但是这个bytes跟python2里的str不太一样。

使用memory view以及struct模块处理bytes是个好思路。

python的IO部分的encoding是不一定的,特别是在windows上。文件名读写/文件内容读写/stdin/stdout/stderr的默认encoding可能都不一样,一定要注意。书P114有详细的讲解。

unicodedata提供了normalize方法,接受4种参数’NFC’,’NFD’,’NFKC’,’NFKD’,用于处理unicode中一个字符可能有多种表达方式的情况。一般虽然不用考虑,但是如果要处理拉丁语或者特殊符号,就不得不考虑了。

str.casefold() (new in python 3.3)能把所有text转成小写,不过,它在某些情况下跟str.lower()返回的值不一样,不过情况比较少,只占unicode字符集的0.11%。

Sorting Unicode Text

pyuca可以实现unicode的排序。特别是拉丁语之类的,非常好用。

Chapter 5: First-Class Functions

Python里的funciton也是first-class object。

Higher-Order Functions

如果一个function接收一个function作为参数或者返回一个function,拿它就是高阶函数。内置的高阶函数有filter, map, reduce,all, any。尽管map之类很好用,但是listcomp或者dictcomp可能可读性更好。

Function Introspection

可以向function添加属性,例如:

1
2
3
def upper_case_name(obj):
return ("%s %s" % (obj.first_name, obj.last_name)).upper()
upper_case_name.short_description = 'Customer name'

这种操作不太常见,但是在Django中被用到了。另外,可以通过user_casename.\_dict__来查看所有的属性。

inspect模块可以提供很多检查function的函数。

Function Annotation

在python3中,可以对函数进行标注。例如:

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

可以通过clip.__annotation__查看标注。标注并不会帮助检查数据类型。

Functional Programming

operator模块当中的几个方法,itemgetter,attrgetter作为高阶函数,帮助python实现了函数式编程。attrgetter有点类似nodejs里的ramda模块。

functools.partial可以绑定部分函数参数。

Function Decorators and Closures

1
2
3
4
5
6
@deco
def target():
pass
#等价于
target = deco(target)

decorator的作用发生于import time,也就是module被第一次import的时候。

1
2
3
4
5
6
7
8
9
registry = []
def register(func):
registry.append(func)
return func
@register
def target():
pass

decorator函数不一定要定义一个inner函数。只要它返回的是一个函数就行。

closure

1
2
3
4
5
6
7
b=6
def f(a):
print(a)
print(b)
b=9
f(3)

这段代码会报错local variable 'b' referenced before assignment,而不是输出3,6。原因是, b=9在函数声明中,使得python认为b是个local variable。解决办法是在函数的一开始假上global b

python3里引入了nonlocal,可以声明变量是free variable,也就是closure里绑定的变量。

1
2
3
4
5
6
7
8
9
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager

上面的代码中,如果没有nonlocal,count和total都会变成local variable,程序会报错。

decorator

functools.wrap用在decorator函数声明里,修改inner函数的__doc__等,确保跟被修饰的函数一致。functools.lru_cache可以方便得实现缓存。

Chapter 6: Object References, Mutability, and Recycling

Choosing between is and ==

is 比较的是object的identity,也就是id。而==比较的是值, a == b调用a.__eq__(b),对于继承object的类来说,通常__eq__比较的是id,跟is结果相同。但是对多数python内置类型来说,它们会重写__eq__,进行更合理的比较。相应的,开销也会更大。比如,比较list会遍历list,甚至是比较嵌套的list。

shallow copy and deep copy

shallow copy往往不是我们想要的。copy模块中的copy.copy()copy.deepcopy()分别实现了shallow/deep copy。deepcopy还可以处理循环引用。

mutable object as function defaults

不要用mutable object作函数参数省缺的默认值。所有的函数都会share这同一个mutable object,如果修改了这个默认值,所有的函数都会受到影响。

del and garbage collection

del不会删除对象,只是删除一个引用。__del__在CPython实现里,会在object被删除时调用,但是并不保证在其他实现里也会被调用。如果要在一个对象被删除时调用某个callback,可以使用weakref.finalize

weak references

weak reference不会增加数据的reference count,适合用来做cache。weakref模块提供了一系列数据结构来操作weak reference。

list和dict不能直接被weak reference引用,但是它们的子类可以。set可以被weak reference。int和tuple完全无法被weak reference。自定义的类可以被weak reference。

Chapter 9: A Python Object

classmethod Versus staticmethod

classmethod的第一个参数是该class,而staticmethod就相当于一个普通的函数,除了定义在class内。作者认为,staticmethod相比classmethod,没什么用。跟在module定义一个函数的效果其实是一样的,顶多就是方便点。另一篇文章提出了不同的观点,解释了staticmethod, classmethod,abstractmethod的用法。其中,提出了一点,对于class中一般的函数,每个instance都会有自己对应于该方法的的object,有额外开销。而他们share staticmethod,节约了开销。

__slots__

把instance的属性存储在tuple中,而不是__dict__,节约空间。不过,这样一来,instance的属性就无法动态添加了。

Chapter 11: Interfaces: From Protocols to ABCs

因为python的duck typing的缘故,用isinstance或者type(foo) is bar来判断类型是不太合适的。一个python object,只要它实现了某些特定的方法,它就是“某个”类。duck typing的精髓就在这里。

Chapter 12: Inheritance: For Good or For Worse

直接继承由C实现的built-in type是有问题的,因为多数定义的方法其实不会被调用。例如,如果继承dict,重写了__getitem__,这个方法也不会被调用。如果继承python实现的如UserDict 或者 MutableMapping, 就不会有这个问题。

Multiple Inheritance and Method Resolution Order

多继承里,如果父类同时实现了一个同名的方法,那么子类调用时,搜索的顺序在__mro__里,这个叫做Method Resolution Order。当然,也可以用下面的方法指定调用哪个父类的方法:

1
2
3
c = Child()
Super.method(c)

Chapter 14: Iterables, Iterators, and Generators

Iterable:可迭代的对象,实现了__iter__或者接受从0开始参数的__getitem__即可。

Iterator: 从可迭代的对象获得的迭代器。实现了__next__方法,并会在终止时抛出StopIteration。由于iterator也实现了__iter__,它其实也是个iterable。

Generator: 生成器

迭代器从一个已有的集合当中取值,而生成器可以凭空生成值。

绝对不应该把iterable实现成iterator。不要给iterable实现__next__。最简单的原因,就是为了可以迭代多次。由于iterable的__iter__每次都返回一个新的iterator,这使得我们可以多次遍历。这也解释了,由于实现了__next__,我们是没有办法reset一个iterator的。

functools和itertools内置了很多方法生成generator,需要了解一下,免得重复造轮子。

A Closer Look at the iter Function

python内置的iter()方法有个不为人知的小技巧。iter(func, sentinel),iter可以接受第二个参数,此时,第一个参数必须是个可以直接callable的对象,比如一个无参数的函数。这样生成的iterator,会不断调用第一个参数,直到返回的值等于sentinel时,抛出StopIteration.

Chapter 15: Context Managers and else Blocks

else block beyond if

else可以出现在除了if以外的其他地方。例如for/else, while/else, try/else

for/else: else会在for正常执行到结束时执行(没有break)

while/else: 会在while语句正常执行到falsy的condition时执行(没有break)

try/else: 会在try语句没有任何exception的情况下执行。 值得注意,else语句里如果有exception,是不会被之前的try/except语句抓住的。

with statement

with open('test.py') as fp在这个statement里,context manager object是with后as前的语句执行的返回结果,这里是open('test.py')返回的一个TextIOWrapper,而as后面的fp绑定的,是context manager执行__enter__之后返回的结果。尽管TextIOWrapper执行__enter__,返回的是self,它也不是绝对的。其实可以返回其他任何值。

with语句结束时,调用context manager的__exit__方法。

def __exit__(self, exc_type, exc_value, traceback) exit方法接受3个参数,代表exception的信息。如果with语句中有exception,__exit__就会接收到对应的参数,否则就是3个None。

exit方法返回True,表示所有异常都正确处理了。如果返回其他值,表示exception没有处理好,会传递给上层函数。

The contextlib Utilities

contextlib提供了很多实用的方法和装饰器,其中最值得注意的就是@contextmanager,它可以通过一个generator函数构造context manager。

@contextmanager

1
2
3
4
5
6
7
8
9
@contextmanager
def foo():
print 'enter'
yield 'Test'
print 'exit'
with foo() as f:
print f

在上面的例子里,foo中yield之前的内容,相当于__enter__, 而yeild的值,相当于__enter__返回的as部分的值, yield之后的部分相当于__exit__。 这里,有两个明显的跟__exit__的不同:1. 这种写法是没办法处理异常的。2. generator函数默认认为所有传入的exception都是处理了的。如果要向上层函数抛出这个异常,必须显示raise。

通过以下的修改,我们可以处理可能发生的异常:

1
2
3
4
5
6
7
8
9
10
11
@contextmanager
def foo():
print 'enter'
try:
yield 'Test'
except Exception:
# do something or
# if you want to raise it
raise Exception
finally:
print 'exit'

Chapter 16: Coroutines

对于一个coroutine来说

1
2
3
4
5
6
7
8
def simple_coroutine():
print 'start'
x = yield
print 'receive', x
my_coro = simple_coroutine()
next(my_coro)
my_coro.send(20)

第一步一定是next(my_coro),以便启动一个coroutine。此时,coroutine会执行到第一个yield并停止。由于很容易忘记第一步,我们可以用coroutil模块的coroutine装饰器来装饰我们的coroutine,省去next()。

yield from

可以实现类似pipe line的东西。具体好像很高深的样子。多通过看具体的例子学习,可以单独写一篇文章。

Chapter 17: Concurrency with Futures

我们可以用concurrent.future里的ThreadPoolExecutor或者ProcessPoolExecutor来实现并行,非常方便使用。

注意,Thread适合异步IO或者network bound的并发,而Process适合CPU bound的并发,例如大量计算。

我们可以用Executor的map方法方便得实现并发。map方法返回一个generator,如果要获得具体结果,遍历这个generator是阻塞的,也就是说,map返回的结果跟提交的函数的顺序一致。如果第一个函数要10s,而之后的都只要1s,那么可能会等10s才能得到第一个结果,而之后的结果都能马上得到。

如果不在乎结果的顺序,那么可以使用executor.submit()配合future.as_completed。

Chapter 18: Concurrency with asyncio

太高级了。感觉ayncio实现了类似javascript的事件机制。具体的,书里例子很多,觉得应该在需要的时候,边看边写比较好。单看例子太可惜了。

Chapter 19: Dynamic Attributes and Properties

动态访问属性是由__getattr__这个方法实现的。

由于可以使用一个instance的__dict__方法来获得属性,在__init__,通过重写__dict__,可以非常容易定义属性。这算是个常见的python hack。

Using a Property for Attribute Validation

property不仅仅可以实现属性的只读,还可以进行validation。

重名时的属性读取顺序:

property > instance attribute > class attribute

Coding a Property Factory

可以用下面的方法实现Property Factory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Item():
weight = quantity('weight')
price = quantity('price')
def __init__(self):
pass
def quantity(storage_name):
def qty_getter(instance):
return instance.__dict__[storage_name]
def qty_setter(instance, value): if value > 0:
instance.__dict__[storage_name] = value
return property(qty_getter, qty_setter)

这里,quantity就是一个property工厂。在@property出现之前,用的就是property()这种方法。

Chapter 20: Attribute Descriptors

实现了__get__, __set__ 或者__del__方法的class就是decriptor class。
descriptor class的instance作为managed class的attribute,并通过managed instance的一个managed attribute联系起来。

感觉书里的代码并不行, 比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Quantity:
def __init__(self, storage_name):
self.storage_name = storage_name
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.storage_name] = value
else:
raise ValueError('value must be > 0')
class LineItem:
weight = Quantity('weight')
price = Quantity('price')
def __init__(self, weight, price):
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
banana = LineItem(3, -1)
print banana.weight, banana.price

照书里说的,应该会报错,但是实际上却没有。估计在self.weight = weight这里,python某个版本的实现改变了。估计如果是赋值,并不会向上寻找到class level的weight,而是会直接在__dict__添加instance的属性weight。

这个比较蛋疼了,还是看一下python的官方doc,研究下class discriptor。不过,书里这段一些关于class descriptor的继承之类的想法还是不错的。之前代码里想给Django的model实现history,估计可能可以通过这里的某些思想实现。

Chapter 21: Class Metaprogramming

除非写framework,99%情况没有必要使用。

type其实不是个函数,而是个class,下面的代码可以动态生成class:

1
2
3
myClass = type('MyClass',
(MySuperClass, MyMixin),
{'x': 42, 'x2': lambda self: self.x * 2})

生成的class跟下面的定义是一样的:

1
2
3
4
class MyClass(MySuperClass, MyMixin):
x = 42
def x2(self):
return self.x * 2

What Happens When: Import Time Versus Runtime

在import的时候,python会执行所有’top level code’,并且有可能递归import其他文件,run很多其他代码。所有import过的模块会被cahce,不会重复import。

这里,需要解释下什么是‘top level code’:

  1. python执行def语句,编译函数,但是不会执行函数
  2. 对于类来说,python运行整个class,包括定义属性和方法。

Metaclass 101

所有class都是type的instance。但是metaclass不仅仅是type的instance,也是type的subclass(magic!)因此,metaclass可以跟type一样,生成class。

指定了metaclass的class在import时,运行完class的body内容,获得class的所有属性,之后会调用metaclass的__init__方法,生成class。

在python3,metaclass有一个特殊的classmethod叫__prepare__,在__new__之前调用,用于生成给__new__使用的class的属性mapping。在这里,我们可以控制属性的顺序。