读书笔记-流畅的python(1-6章)

前言:这正是本书的主要目的:着重讲解这门语言的基本惯用法,让你的代码简洁、高效且可读,把你打造成熟练的 Python 程序员。
自己总结学习作为输出,很多为了节省时间只是复制粘贴,不具有广泛意义

第一章:python数据模型

1.概述

1.通过实现特殊方法,展示python解释器是如何调用特殊方法;

2.新知

1.Python 数据模型计算机编程语言中对象的属性;
2.元对象所指的是那些对建构语言本身来讲很重要的对象,以此为前提,协议也可以看作接口。也就是说,元对象协议是对象模型的同义词,它们的意思都是构建核心语言的 API
3.len 是特殊方法,是为了让 Python 自带的数据结构可以走后门,abs 也是同理。(解释:因为如果 x 是一个内置类型的实例,len(x)的背后会用 CPython 直接从 C 结构体中读取对象的长度,不调用任何方法,以至于 len(x) 会非常快。)
这种处理方式在保持内置类型的效率和保证语言的一致性之间找到了一个平衡点,也印证了“Python 之禅”中的另外一句话:”不能让特例特殊到开始破坏既定规则。”

3.延伸阅读

Alex Martelli 的《Python 技术手册(第 2 版)》对数据模型的讲解很精彩
David Beazley 著有两本基于 Python 3 的书,其中对数据模型进行了详尽的介绍。一本是《Python 参考手册(第 4 版)》 ,另一本是与 Brian K.Jones 合著的《Python Cookbook(第 3 版)中文版》
Python 文档里总是用“Python 数据模型”这种说法,而大多数作者提到这个概念的时候会说“Python 对象模型”。Alex Martelli 的《Python技术手册(第 2 版)》和 David Beazley 的《Python 参考手册(第 4版)》是这个领域中最好的两本书。

第 2 章 序列构成的数组

1.概述

目的:深入理解 Python 中的不同序列类型,不但能让我们避免重新发明轮子,它们的 API 还能帮助我们把自己定义的 API 设计得跟原生的序列一样,或者是跟未来可能出现的序列类型保持兼容。

2.小节归纳

2.1 内置序列类型概览

  • 容器序列:list、tuple 和 collections.deque 这些序列能存放不同类型的数据。
  • 扁平序列:str、bytes、bytearray、memoryview 和 array.array,这类序列只能容纳一种类型
  • 容器序列,容器序列区别
    容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列里存放的是值而不是引用。换句话说,扁平序列其实是一段连续的内存空间。由此可见扁平序列其实更加紧凑,但是它里面只能存放诸如字符、字节和数值这种基础类型
  • 序列类型还能按照能否被修改来分类
    可变序列:list、bytearray、array.array、collections.deque 和memoryview。
    不可变序列:tuple、str 和 bytes

2.2 列表推导和生成器表达式

列表推导是构建列表(list)的快捷方式,而生成器表达式则可以用来创建其他任何类型的序列,写出可读性更好且更高效的代码的机会

2.2.1 列表推导和可读性

  • 列表推导式示例
>>> symbols = '$¢£¥€¤'
>>> codes = [ord(symbol) for symbol in symbols]
  • 列表推导式使用场景

    列表推导也可能被滥用。以前看到过有的 Python 代码用列表推导来重复获取一个函数的副作用。通常的原则是,只用列表推导来创建新的列表,并且尽量保持简短。如果列表推导的代码超过了两行,你可能就要考虑是不是得用 for 循环重写了。就跟写文章一样,并没有什么硬性的规则,这个度得你自己把握

  • 句法提示
    Python 会忽略代码里 []、{} 和 () 中的换行,因此如果你的代码里有多行的列表、列表推导、生成器表达式、字典这一类的,可以省略不太好看的续行符 \

2.2.2 列表推导同filter和map的比较

  • 示例代码可以轻松看出可读性的差异
>>> symbols = '$¢£¥€¤'
>>> beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
>>> beyond_ascii
[162, 163, 165, 8364, 164]
>>> beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
>>> beyond_ascii
[162, 163, 165, 8364, 164]

2.2.3 笛卡儿积

  • 代码示例
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> tshirts = [(color, size) for color in colors for size in sizes]

2.2.4 生成器表达式

虽然也可以用列表推导来初始化元组、数组或其他序列类型,但是生成器表达式是更好的选择。这是因为生成器表达式背后遵守了迭代器协议,可以逐个地产出元素,而不是先建立一个完整的列表,然后再把这个列表传递到某个构造函数里。前面那种方式显然能够节省内存

  • 用生成器表达式初始化元组和数组
>>> symbols = '$¢£¥€¤'
>>> tuple(ord(symbol) for symbol in symbols) 
(36, 162, 163, 165, 8364, 164)
>>> import array
>>> array.array('I', (ord(symbol) for symbol in symbols)) 
array('I', [36, 162, 163, 165, 8364, 164])
  • 使用生成器表达式计算笛卡儿积
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> for tshirt in ('%s %s' % (c, s) for c in colors for s in sizes): 
		print(tshirt)

2.3 元组不仅仅是不可变的列表

除了用作不可变的列表,它还可以用于没有字段名的记录,作为记录的功用

2.3.1 元组和记录

>>> lax_coordinates = (33.9425, -118.408056)>>> city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)>>> traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'),... ('ESP', 'XDA205856')]
>>> for passport in sorted(traveler_ids):...		print('%s/%s' % passport)  # ❺ % 格式运算符能被匹配到对应的元组元素上
...
BRA/CE342567
ESP/XDA205856
USA/31195855
>>> for country, _ in traveler_ids:# “_”占位符
... 	print(country)
...
USA
BRA
ESP

2.3.2 元组拆包

  • 元组拆包可以应用到任何可迭代对象上,唯一的硬性要求是,被可迭代对象中的元素数量必须要跟接受这些元素的元组的空档数一致。除非我们用 * 来表示忽略多余的元素,在“用 * 来处理多余的元素
  • 不使用中间变量交换两个变量的值
>>> b, a = a, b
  • 用*来处理剩下的元素,函数用 *args 来获取不确定数量的参数算是一种经典写法了,于是 Python 3 里,这个概念被扩展到了平行赋值中:
>>> a, b, *rest = range(5)
>>> a, b, rest
(0, 1, [2, 3, 4])
>>> a, b, *rest = range(3)
>>> a, b, rest
(0, 1, [2])
>>> a, b, *rest = range(2)
>>> a, b, rest
(0, 1, [])
>>> a, *body, c, d = range(5) # 这个变量可以出现在赋值表达式的任意位置
>>> a, body, c, d
(0, [1, 2], 3, 4)
>>> *head, b, c, d = range(5)
>>> head, b, c, d
([0, 1], 2, 3, 4)

2.3.3 嵌套元组拆包

  • 接受表达式的元组可以是嵌套式的,例如 (a, b, (c, d))。只要这个接受元组的嵌套结构符合表达式本身的嵌套结构,Python 就可以作出正确的对应。
metro_areas = [
('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)),
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]
print('{:15} | {:^9} | {:^9}'.format('', 'lat.', 'long.'))
fmt = '{:15} | {:9.4f} | {:9.4f}'
for name, cc, pop, (latitude, longitude) in metro_areas: # ➋我们把输入元组的最后一个元素拆包到变量构成的元组里,这样就获取了坐标。
	if longitude <= 0: # ➌
	print(fmt.format(name, latitude, longitude))
  • 在 Python 3 之前,元组可以作为形参放在函数声明中,例如def fn(a, (b, c), d):。然而 Python 3 不再支持这种格式

2.3.4 具名元组

  • collections.namedtuple 是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类——这个带名字的类对调试程序有很大帮助
  • 用 namedtuple 构建的类的实例所消耗的内存跟元组是一样的,因为字段名都被存在对应的类里面。这个实例跟普通的对象实例比起来也要小一些,因为 Python 不会用 dict 来存放这些实例的属性。
  • 如何使用:略(具体有使用场景现查)

2.3.5 作为不可变列表的元组

  • 如果要把元组当作列表来用的话,最好先了解一下它们的相似度如何。除了跟增减元素相关的方法之外,元组支持列表的其他所有方法。还有一个例外,元组没有 reversed 方法,但是这个方法只是个优化而已,reversed(my_tuple) 这个用法在没有 reversed 的情况下也是合法的。

2.4 切片

2.4.1 为什么切片和区间会忽略最后一个元素*

在切片和区间操作里不包含区间范围的最后一个元素是 Python 的风格,这个习惯符合 Python、C 和其他语言里以 0 作为起始下标的传统。这样做带来的好处如下:

  • 当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:range(3) 和 my_list[:3] 都返回 3 个元素。
  • 当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop - start)即可。
  • 这样做也让我们可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成 my_list[:x] 和 my_list[x:] 就可以了,如下所示。

2.4.2 对对象进行切片

  • s[a️c] 的形式对 s 在 a 和 b之间以 c 为间隔取值。c 的值还可以为负,负值意味着反向取值
  • slice(a, b, c)

2.4.3 多维切片和省略

2.4.4 给切片赋值

>>> l = list(range(10))
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[2:5] = [20, 30] # 切片不会改变原有对象
>>> l
[0, 1, 20, 30, 5, 6, 7, 8, 9]
>>> del l[5:7] # del会改变原有对象
>>> l
[0, 1, 20, 30, 5, 8, 9]
>>> l[3::2] = [11, 22]
>>> l
[0, 1, 20, 11, 5, 22, 9]
>>> l[2:5] = 100  # 如果赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代对象。即便只有单独一个值
Traceback (most recent call last):
File "", line 1, in <module>
TypeError: can only assign an iterable

2.5 对序列使用+和*

  • 通常 + 号两侧的序列由相同类型的数据所构成,在拼接的过程中,两个被操作的序列都不会被修改
  • 建立列表时注意列表内的对象是否是可变数据结构(即对象引用问题)
# 每次迭代中都新建了一个列表
>>> board = [['_'] * 3 for i in range(3)] # 每次迭代中都新建了一个列表
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[1][2] = 'X' 
>>> board
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
# 3 个指向同一个列表的引用
>>> weird_board = [['_'] * 3] * 3  # 3 个指向同一个列表的引用
>>> weird_board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> weird_board[1][2] = 'O' 
>>> weird_board
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]
# 3 个指向同一个列表的引用
row=['_'] * 3
board = []
for i in range(3):
	board.append(row)
# 每次迭代中都新建了一个列表
>>> board = []
>>> for i in range(3):
... row=['_'] * 3 # 
... board.append(row)
...
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[2][0] = 'X'
>>> board # 
[['_', '_', '_'], ['_', '_', '_'], ['X', '_', '_']]

2.6 序列的增量赋值

  • += 背后的特殊方法是 iadd (用于“就地加法”)
  • *= 背后的特殊方法是__imul__(用于“就地乘法”)
  • 对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。
  • str 是一个例外,因为对字符串做 += 实在是太普遍了,所以 CPython 对它做了优化。为 str初始化内存的时候,程序会为它留出额外的可扩展空间,因此进行增量操作的时候,并不会涉及复制原有字符串到新位置这类操作
  • 改动元祖内可变对象(可以改变成功,但也会报错,所以不要去试着改变)
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
Traceback (most recent call last):
File "", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40, 50, 60])
  • 改动元祖内可变对象,可以改变成功,但也会报错,背后的机制(略)

2.7 list.sort方法和内置函数sorted

  • 是否对原有对象进行改动
  1. list.sort 方法会就地排序列表,也就是说不会把原列表复制一份。这也是这个方法的返回值是 Non的原因,提醒你本方法不会新建一个列表。在这种情况下返回 None 其实是 Python 的一个惯例:如果一个函数或者方法对对象进行的是就地改动,那它就应该返回 None,好让调用者知道传入的参数发生了变动,而且并未产生新的对象。例如,random.shuffle 函数也遵守了这个惯例
  • 不管是 list.sort 方法还是 sorted 函数,都有两个可选的关键字参数。

    • reverse
      如果被设定为 True,被排序的序列里的元素会以降序输出(也就是说把最大值当作最小值来排序)。这个参数的默认值是 False。
    • key
      一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果将是排序算法依赖的对比关键字。比如说,在对一些字符串排序时,可以用 key=str.lower 来实现忽略大小写的排序,或者是用 key=len 进行基于字符串长度的排序。这个参数的默认值是恒等函数(identity function)也就是默认用元素自己的值来排序
  • 可选参数 key 还可以在内置函数 min() 和 max() 中起作用。另外,还有些标准库里的函数也接受这个参数,像itertools.groupby() 和 heapq.nlargest() 等

  • list.sort 方法会就地排序列表,sorted,它会新建一个列表作为返回值

>>> fruits = ['grape', 'raspberry', 'apple', 'banana']
>>> sorted(fruits)
['apple', 'banana', 'grape', 'raspberry'] 
>>> fruits
['grape', 'raspberry', 'apple', 'banana']  # 原列表并没有变化。
>>> sorted(fruits, reverse=True)
['raspberry', 'grape', 'banana', 'apple'] 
>>> sorted(fruits, key=len)
['grape', 'apple', 'banana', 'raspberry']  #因为这个排序算法是稳定,它们的相对位置跟在原来的列表里是一样的。
>>> sorted(fruits, key=len, reverse=True)
['raspberry', 'banana', 'grape', 'apple']
>>> fruits
['grape', 'raspberry', 'apple', 'banana'] 
>>> fruits.sort()  # 对原列表就地排序,返回值 None 会被控制台忽略
>>> fruits
['apple', 'banana', 'grape', 'raspberry'] 

2.8 用bisect来管理已排序的序列

bisect 模块包含两个主要函数,bisect 和 insort,两个函数都利用二分查找算法来在有序序列中查找或插入元素(待补充)

2.9 当列表不是首选时

  1. 列表背后操作的是字符串,性能差
  2. 数组存储只包含数字的列表
  3. 内存视图,在不复制内容的情况下操作同一个数组的不同切片
  4. NumPy 和 SciPy 提供的高阶数组和矩阵
  5. 双向队列和其他形式的队列(collections.deque),因为列表删除或新增第一个元素涉及所以元素的移动

本章总结

  1. 容器类型的数据结构的相关操作及背后原理
  2. 其他优质类型和操作和选取(避免列表和元组的自身缺点)

第 4 章 文本和字节序列

目前没遇到过编码问题,唯一值得有用的就是Unicode 三明治——目前处理文本的最佳实践

第 5 章 一等函数

概述:函数本身就是对象,本质就是将函数对象的应用
一等对象定义:

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

5.1 把函数视作对象

5.2 高阶函数

接受函数为参数,或者把函数作为结果返回的函数是高阶函数

>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=len)
['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']
>>> def reverse(word):
... return word[::-1]
>>> reverse('testing')
'gnitset'
>>> sorted(fruits, key=reverse)
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']

使用列表生成是代替map 和 filter等
使用sum代替reduce
all 和 any 也是内置的归约函数。
为了使用高阶函数,有时创建一次性的小型函数更便利。这便是匿名函数存在的原因

5.3 匿名函数

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

5.4 可调用对象

除了用户定义的函数,调用运算符(即 ())还可以应用到其他对象上。如果想判断对象能否调用,可以使用内置的 callable() 函数

5.5 用户定义的可调用类型

5.6 函数内省

  1. python对象
>>> dir(factorial)
['__annotations__', '__call__', '__class__', '__closure__', '__code__',
'__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__get__', '__getattribute__', '__globals__',
'__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__',
'__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__']
__dict__  			#与用户定义的常规类一样,函数使用 __dict__ 属性存储赋予它的用户属性
__annotations__  	#dict 参数和返回值的注解
__call__ 			#method-wrapper 实现 () 运算符;即可调用对象协议
__closure__  		#tuple 函数闭包,即自由变量的绑定(通常是 None)
__code__          	#code 编译成字节码的函数元数据和函数定义体
__defaults__ 		#tuple 形式参数的默认值
__get__				#method-wrapper 实现只读描述符协议(参见第 20 章)
__globals__ 		#dict 函数所在模块中的全局变量
__kwdefaults__ 		#dict 仅限关键字形式参数的默认值
__name__ 			#str 函数名称
__qualname__ 		#str函数的限定名称,如 Random.choice

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

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

5.9 函数注解

注解不会做任何处理,只是存储在函数的 annotations 属性(一个字典)中:

>>> from clip_annot import clip
>>> clip.__annotations__
{'text': <class 'str'>, 'max_len': 'int > 0', 'return': <class 'str'>}

5.10 支持函数式编程的包

Python 的目标不是变成函数式编程语言,但是得益于 operator 和 functools 等包的支持,函数式编程风格也可以信手拈来

5.10.1 operator模块

在函数式编程中,经常需要把算术运算符当作函数使用。例如,不使用递归计算阶乘。求和可以使用 sum 函数,但是求积则没有这样的函数。我们可以使用 reduce 函数(5.2.1 节是这么做的),但是需要一个函数计算序列中两个元素之积。示例 5-21 展示如何使用 lambda 表达式解决这个问题

from functools import reduce
def fact(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 fact(n):
	return reduce(mul, range(1, n+1))

operator 模块中还有一类函数,能替代从序列中取出元素或读取对象属性的 lambda 表达式:因此,itemgetter 和 attrgetter 其实会自行构建函数。(评价)

5.10.2 使用functools.partial冻结参数

functools.partial 这个高阶函数用于部分应用一个函数。部分应用是指,基于一个函数创建一个新的可调用对象,把原函数的某些参数固定。使用这个函数可以把接受一个或多个参数的函数改编成需要回调的API,这样参数更少

>>> from operator import mul
>>> from functools import partial
>>> triple = partial(mul, 3)>>> triple(7)21
>>> list(map(triple, range(1, 10)))[3, 6, 9, 12, 15, 18, 21, 24, 27]

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

作者从策略模式开始,讨论了一等函数在设计模式中的角色,并用一等函数简化了设计模式的实现方式,以此来展示 Pythonic 的设计模式应该是什么样子的
没有什么新的内容,对python的设计模式请看我自己将要写的python设计模式的博客

你可能感兴趣的:(读书笔记,python,python)