《流畅的Python》学习笔记

《流畅的Python》学习笔记,备忘录形式。

第1章 Python数据模型

第2章 序列构成的数组

一个关于+=的谜题

+=对应的方法为__iadd__,对应的指令为INPLACE_ADD,表示inplace add。对应实现了__iadd__的对象,解释器会直接调用该方法,否则退化为调用__add__,然后再赋值。

1
2
3
4
5
6
7
>>> t=(1,2,[30,50])
>>> t[2] += [50,60]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 50, 50, 60])

通过上面的代码,我们发现:t[2] += [50,60]抛出异常,但是t的值仍然被改变了,通过字节码来分析执行过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> t=(1,2,[30,50])
>>> dis.dis('t[2] += [50,60]')
1 0 LOAD_NAME 0 (t)
2 LOAD_CONST 0 (2)
4 DUP_TOP_TWO
6 BINARY_SUBSCR
8 LOAD_CONST 1 (50)
10 LOAD_CONST 2 (60)
12 BUILD_LIST 2
14 INPLACE_ADD
16 ROT_THREE
18 STORE_SUBSCR
20 LOAD_CONST 3 (None)
22 RETURN_VALUE

18 STORE_SUBSCR执行失败,因为t是个不可变对象。
三个点:

  • 不要把可变对象(列表)放在不可变对象(元组)中;
  • 增量赋值不是原子操作;
  • 多查看Python的字节码,来分析背后的运行机制。

第3章 字典和集合

子类化UserDict

创造自定义映射类型,优先使用collections.UserDict为基类,而不是以dict为基类。UserDict有个属性叫做data,是dict的实例,这个属性实际上是UserDict最终存储数据的地方。

不可变映射类型

标准库里所有的映射类型都是可变的,如果希望构造一个不可变的映射,可以使用MappingProxyType,给这个类一个映射,它会返回一个只读的映射视图。

集合字面量

构造空集合,需要写成set()的形式,{}表示构造空字典。使用字面量构造集合比使用构造函数的形式要快,因为从字面量构造时,Python会利用一个专门的字节码BUILD_SET来创建集合。

1
2
3
4
5
6
7
8
9
10
>>> dis.dis('{1}')
1 0 LOAD_CONST 0 (1)
2 BUILD_SET 1
4 RETURN_VALUE
>>> dis.dis('set([1])')
1 0 LOAD_NAME 0 (set)
2 LOAD_CONST 0 (1)
4 BUILD_LIST 1
6 CALL_FUNCTION 1
8 RETURN_VALUE

第4章 文本和字节序列

默认编码值

  • 如果打开文件时没有指定encoding参数,默认值由locale.getpreferredencoding()提供。

  • 如果设定了PYTHONIOENCODING环境变量,sys.stdout/stdin.stderr的编码使用设定的值,否则继承自所在控制台,如果输入/输出重定向到文件,则由locale.getpreferredencoding()定义。

    在Python3.6以后,Windows平台下,指定PYTHONIOENCODING的同时还需要指定PYTHONLEGACYWINDOWSSTDIO才能让sys.stdout/stdin/stderr使用指定编码

  • Python在二进制数据和字符串之间转换时,内部使用sys.getdefaultencoding()获得的编码(在Python3中无法被修改)。

  • sys.getfilesystemencoding()用于编解码文件名(不是文件内容)。把字符串参数作为文件名传给open()函数时就会使用它,如果传入的文件名参数是字节序列,那就不经改动直接传给OS API。可参考Unicode filenames一文。

第5章 一等函数

高阶函数

接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher-order function)

可调用对象

如果想判断对象能否调用,可以使用内置的callable()函数。Python数据模型文档列出了7中可调用对象。

  • 用户定义的函数

    使用def语句或lambda表达式创建。

  • 内置函数

    使用C语言(CPython)实现的函数,如lentime.strftime

  • 内置方法

    使用C语言实现的方法,如dict.get

  • 方法

    在类的定义体中定义的函数。

  • 调用类时会运行类的__new__方法创建一个实例,然后运行__init__方法,初始化实例,最后把实例返回给调用方。因为Python没有new运算符,所以调用类相当于调用函数。

  • 类的实例

    如果类定义了__call__方法,那么它的实例可以作为函数调用。

  • 生成器函数

    使用yield关键字的函数或方法。调用生成器函数返回的是生成器对象。

获取关于参数的信息

函数对象有个__defaults__属性,它的值是一个元组,里面保存着定位参数和关键字参数的默认值。仅限关键字参数的默认值在__kwdefaults__属性中。然而,参数的名称在__code__属性中,它的值是一个code对象引用,自身也有很多属性。

使用inspect模块提取函数签名,inspect.Parameter.kind值有以下5中:

  • POSITIONAL_OR_KEYWORD

    可以通过定位参数和关键字传入的形参(多数Python函数的参数属于此类)。

  • VAR_POSITIONAL

    定位参数元组。

  • VAR_KEYWORD

    关键字参数字典。

  • KEYWORD_ONLY

    仅限关键字参数(Python3新增)。

  • POSITIONAL_ONLY

    仅限定位参数。目前,Python声明函数的语法不支持,但是有些使用C语言实现且不接受关键字参数的函数(如divmod)支持。

函数注解

Python3提供了一种句法,用于为函数声明中的参数和返回值附加元数据。

1
2
3
4
5
def clip(text: str, max_len: 'int > 0'=80) -> str:
"""
在max_len前面或后面的第一个空格处截断文本
"""
return text

函数声明中的各个参数可以在:之后附加注解表达式。如果参数有默认值,注解放在参数名和=之间。如果想注解返回值,在)和函数声明末尾的:之间添加->和一个表达式。那个表达式可以是任何类型。

注解不会做任何处理,只是存储在函数的__annotations__属性中。

Python对注解所做的唯一的事情是,把它们存储在函数的__annotations__属性里,解释器不会对注解做任何处理或验证。注解只是元数据,可供IDE、框架和装饰器等工具使用。可以使用inspect.signature()函数提取注解。

第6章 使用一等函数实现设计模式

案例分析:重构“策略”模式

在Python中,模块也是一等对象,而且标准库提供了几个处理模块的函数。

globals()返回一个字典,表示当前的全局符号表。这个符号表始终针对当前模块(对函数或方法来说,是指定义他们的模块,而不是调用他们的模块)。

inspect.getmembers函数用于获取对象的属性。

第7章 函数装饰器和闭包

装饰器基础知识

装饰器是可调用对象,其参数是另一个函数(被装饰的函数)。装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或者可调用对象。

假如有个名为decorate的装饰器

1
2
3
@decorate
def target():
print('running target()')

上述代码的效果与下述写法一致:

1
2
3
def target():
print('running target()')
target = decorate(target)

装饰器只是语法糖,有两大特性:

  • 能把被装饰的函数替换成其他函数。
  • 装饰器在加载模块时立即执行。

Python何时执行装饰器

函数装饰器在导入模块时(被装饰函数定义时)立即执行,而被装饰的函数只在明确调用时运行。

变量作用域规则

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

上述代码,执行会报错:

Traceback (most recent call last):

File “”, line 1, in

File “”, line 3, in f2

UnboundLocalError: local variable ‘b’ referenced before assignment

Python在编译函数的定义体时,它判断b是局部变量,因为在函数中给它赋值了。

这不是缺陷,而是设计选择:Python不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。如果在函数中赋值时想让解释器把b当成全局变量,要使用global声明。

闭包

闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。

示例 average_oo.py:

计算移动平均值的类

1
2
3
4
5
6
7
8
9
10
11
12
class Averager():
def __init__(self):
self.series = []

def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total/len(self.series)

avg = Averager()
avg(10)
avg(11)

计算移动平均值的高阶函数

1
2
3
4
5
6
7
8
9
10
11
12
13
def make_averager():
series = []

def averager(new_value):
series.append(new_value)
total = sum(self.series)
return total/len(self.series)

return averager

avg = make_averager()
avg(10)
avg(11)

averager函数中,series自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量。

综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍然能使用那些绑定。

nonlocal声明

用于在闭包中声明变量作用域。

标准库中的装饰器

functools.lru_cache实现了备忘(memorization)功能。这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。可用来优化递归调用。因为lru_cache使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被lru_cache装饰的函数,它的所有参数都必须是可散列的

functools.singledispatch可以使普通的函数变为泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的一组函数。singledispatch机制的一个显著特征是,你可以在系统的任何地方和任何模块中注册专门函数。如果后来在新的模块中定义了新的类型,可以轻松地添加一个新的专门函数来处理那个类型。此外,你还可以为不是自己编写的或者不能修改的类添加自定义函数。

参数化装饰器

创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。

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
26
from inspect import signature
from functools import wraps

def typeassert(*ty_args, **ty_kwargs):
def decorate(func):
# If in optimized mode, disable type checking
if not __debug__:
return func

# Map function argument names to supplied types
sig = signature(func)
bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments

@wraps(func)
def wrapper(*args, **kwargs):
bound_values = sig.bind(*args, **kwargs)
# Enforce type assertions across supplied arguments
for name, value in bound_values.arguments.items():
if name in bound_types:
if not isinstance(value, bound_types[name]):
raise TypeError(
'Argument {} must be {}'.format(name, bound_types[name])
)
return func(*args, **kwargs)
return wrapper
return decorate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import types
from functools import wraps

class Profiled:
def __init__(self, func):
wraps(func)(self)
self.ncalls = 0

def __call__(self, *args, **kwargs):
self.ncalls += 1
return self.__wrapped__(*args, **kwargs)

def __get__(self, instance, cls):
if instance is None:
return self
else:
return types.MethodType(self, instance)

示例来自python3-cookbook

第8章 对象引用、可变性和垃圾回收

==is之间选择

==比较是两个变量的值是否相等,is比较两个变量是否同一个对象,即对象标识是否相等。

is运算符比==速度快,因为它不能重载,所以Python不用寻找并调用特殊方法,而是直接比较两个整数ID。

发布时间: 2018年10月09日 - 19时48分
更新时间: 2018年10月17日 - 21时42分
原始链接: https://oaoa.me/posts/4297a92f/