专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
程序员的那些事  ·  快!快!快!DeepSeek 满血版真是快 ·  2 天前  
程序员的那些事  ·  清华大学:DeepSeek + ... ·  3 天前  
OSC开源社区  ·  升级到Svelte ... ·  5 天前  
程序猿  ·  “我真的受够了Ubuntu!” ·  4 天前  
51好读  ›  专栏  ›  SegmentFault思否

《流畅的python》阅读笔记(上)

SegmentFault思否  · 公众号  · 程序员  · 2017-11-10 08:00

正文

起步

《流畅的python》是一本适合python进阶的书, 里面介绍的基本都是高级的python用法. 对于初学python的人来说, 基础大概也就够用了, 但往往由于够用让他们忘了深入, 去精通. 我们希望全面了解这个语言的能力边界, 可能一些高级的特性并不能马上掌握使用, 因此这本书是工作之余, 还有余力的人来阅读, 我这边就将其有用, 精妙的进阶内容整理出来.

这本书有21个章节, 整理也是根据这些章节过来.

第一章: python数据模型

这部分主要介绍了python的魔术方法, 它们经常是两个下划线包围来命名的(比如 __init__ , __lt__ , __len__ ). 这些特殊方法是为了被python解释器调用的, 这些方法会注册到他们的类型中方法集合中, 相当于为cpython提供抄近路. 这些方法的速度也比普通方法要快, 当然在自己不清楚这些魔术方法的用途时, 不要随意添加.

关于字符串的表现形式是两种, __str__ __repr__ . python的内置函数 repr 就是通过 __repr__ 这个特殊方法来得到一个对象的字符串表示形式. 这个在交互模式下比较常用, 如果没有实现 __repr__ , 当控制台打印一个对象时往往是 . 而 __str__ 则是 str() 函数时使用的, 或是在 print 函数打印一个对象的时候才被调用, 终端用户友好.

两者还有一个区别, 在字符串格式化时, "%s" 对应了 __str__ . 而 "%r" 对应了 __repr__ . __str__ __repr__ 在使用上比较推荐的是,前者是给终端用户看,而后者则更方便我们调试和记录日志.

更多的特殊方法: https://docs.python.org/3/reference/datamodel.html

第二章: 序列构成的数组

这部分主要是介绍序列, 着重介绍数组和元组的一些高级用法.

序列按照容纳数据的类型可以分为:

  • 容器序列 : list、tuple 和 collections.deque 这些序列能存放不同类型的数据

  • 扁平序列 : str、bytes、bytearray、memoryview 和 array.array,这类序列只能容纳一种类型.

如果按照是否能被修改可以分为:

  • 可变序列 : list、bytearray、array.array、collections.deque 和 memoryview

  • 不可变序列 : tuple、str 和 bytes

列表推导

列表推导是构建列表的快捷方式, 可读性更好且效率更高.

例如, 把一个字符串变成unicode的码位列表的例子, 一般:

  1. symbols = '$¢£¥€¤'

  2. codes = []

  3. for symbol in symbols:

  4.    codes.append(ord(symbol))

使用列表推导:

  1. symbols = '$¢£¥€¤'

  2. codes = [ord(symbol) for symbol in symbols]

能用列表推导来创建一个列表, 尽量使用推导, 并且保持它简短.

笛卡尔积与生成器表达式

生成器表达式是能逐个产出元素, 节省内存. 例如:

  1. >>> colors = ['black', 'white']

  2. >>> sizes = ['S', 'M', 'L']

  3. >>> for tshirt in ('%s %s' % (c, s) for c in colors for s in sizes):

  4. ... print(tshirt)

实例中列表元素比较少, 如果换成两个各有1000个元素的列表, 显然这样组合的笛卡尔积是一个含有100万元素的列表, 内存将会占用很大, 而是用生成器表达式就可以帮忙省掉for循环的开销.

具名元组

元组经常被作为 不可变列表 的代表. 经常只要数字索引获取元素, 但其实它还可以给元素命名:

  1. >>> from collections import namedtuple

  2. >>> City = namedtuple('City', 'name country population coordinates')

  3. >>> tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667 ))

  4. >>> tokyo

  5. City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722,

  6. 139.691667))

  7. >>> tokyo.population

  8. 36.933

  9. >>> tokyo.coordinates

  10. (35.689722, 139.691667)

  11. >>> tokyo[1]

  12. 'JP'

切片

列表中是以0作为第一个元素的下标, 切片可以根据下标提取某一个片段.

s[a:b:c] 的形式对 s a b 之间以 c 为间隔取值。 c 的值还可以为负, 负值意味着反向取值.

  1. >>> s = 'bicycle'

  2. >>> s[::3]

  3. 'bye'

  4. >>> s[::-1]

  5. 'elcycib'

  6. >>> s[::-2]

  7. 'eccb'

第三章: 字典和集合

dict 类型不但在各种程序里广泛使用, 它也是 Python 语言的基石. 正是因为 dict 类型的重要, Python 对其的实现做了高度的优化, 其中最重要的原因就是背后的「散列表」 set(集合)和dict一样, 其实现基础也是依赖于散列表.

散列表也叫哈希表, 对于dict类型, 它的key必须是可哈希的数据类型. 什么是可哈希的数据类型呢, 它的官方解释是:

如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变 的,而且这个对象需要实现 __hash__() 方法。另外可散列对象还要有 __qe__() 方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的散列值一定是一样的……

str , bytes , frozenset 数值 都是可散列类型.

字典推导式

  1. DIAL_CODE = [

  2.    (86, 'China'),

  3.    (91, 'India'),

  4.    (7, 'Russia'),

  5.    (81, 'Japan'),

  6. ]

  7. ### 利用字典推导快速生成字典

  8. country_code = {country: code for code, country in DIAL_CODE}

  9. print(country_code)

  10. '''

  11. OUT:

  12. {'China': 86, 'India': 91, 'Russia': 7, 'Japan': 81}

  13. '''

defaultdict:处理找不到的键的一个选择

当某个键不在映射里, 我们也希望也能得到一个默认值. 这就是 defaultdict , 它是 dict 的子类, 并实现了 __missing__ 方法.

  1. import collections

  2. index = collections.defaultdict(list)

  3. for item in nums:

  4.    key = item % 2

  5.    index[key].append(item)

字典的变种

标准库里 collections 模块中,除了 defaultdict 之外的不同映射类型:

  • OrderDict : 这个类型在添加键的时候,会保存顺序,因此键的迭代顺序总是一致的

  • ChainMap : 该类型可以容纳数个不同的映射对像,在进行键的查找时,这些对象会被当做一个整体逐个查找,直到键被找到为止 pylookup=ChainMap(locals(),globals())

  • Counter : 这个映射类型会给键准备一个整数技术器,每次更行一个键的时候都会增加这个计数器,所以这个类型可以用来给散列表对象计数,或者当成多重集来用.

  1. import collections

  2. ct = collections.Counter('abracadabra')

  3. print(ct)   # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})

  4. ct.update('aaaaazzz')

  5. print(ct)   # Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})

  6. print(ct.most_common(2)) # [('a', 10), ('z', 3)]

  • UserDict : 这个类其实就是把标准 dict 用纯 Python 又实现了一遍

  1. import collections

  2. class StrKeyDict(collections.UserDict):

  3.    def __missing__(self, key):

  4.         if isinstance(key, str):

  5.            raise KeyError(key)

  6.        return self[str(key)]

  7.    def __contains__(self, key):

  8.        return str(key) in self.data

  9.    def __setitem__(self, key, item):

  10.        self.data[str(key)] = item

不可变映射类型

说到不可变, 第一想到的肯定是元组, 但是对于字典来说, 要将key和value的对应关系变成不可变, types 模块的 MappingProxyType 可以做到:

  1. from types import MappingProxyType

  2. d = {1:'A'}

  3. d_proxy = MappingProxyType(d)

  4. d_proxy[1]='B' # TypeError: 'mappingproxy' object does not support item assignment

  5. d[2] = 'B'

  6. print(d_proxy) # mappingproxy({1: 'A', 2: 'B'})

d_proxy 是动态的, 也就是说对 d 所做的任何改动都会反馈到它上面.

集合论

集合的本质是许多唯一对象的聚集. 因此, 集合可以用于去重. 集合中的元素必须是可散列的, 但是 set 本身是不可散列的, 而 frozenset 本身可以散列.

集合具有唯一性, 与此同时, 集合还实现了很多基础的中缀运算符. 给定两个集合 a 和 b, a|b 返 回的是它们的合集, a&b 得到的是交集, 而 a-b 得到的是差集.

合理的利用这些特性, 不仅能减少代码的数量, 更能增加运行效率.

  1. # 集合的创建

  2. s = set([1, 2, 2, 3])

  3. # 空集合

  4. s = set()

  5. # 集合字面量

  6. s = {1, 2}

  7. # 集合推导

  8. s = {chr(i) for i in range(23, 45)}

第四章: 文本和字节序列

本章讨论了文本字符串和字节序列, 以及一些编码上的转换. 本章讨论的 str 指的是python3下的.

字符问题

字符串是个比较简单的概念: 一个字符串是一个字符序列. 但是关于 "字符" 的定义却五花八门, 其中, "字符" 的最佳定义是 Unicode字符 . 因此, python3中的 str 对象中获得的元素就是 unicode 字符.

把码位转换成字节序列的过程就是 编码 , 把字节序列转换成码位的过程就是 编码 :

  1. >>> s = 'café'

  2. >>> len(s)

  3. 4

  4. >>> b = s.encode('utf8')

  5. >>> b

  6. b'caf\xc3\xa9'

  7. >>> len(b)

  8. 5

  9. >>> b.decode('utf8') #'café

码位可以认为是人类可读的文本, 而字符序列则可以认为是对机器更友好. 所以要区分 .decode() .encode() 也很简单. 从字节序列到人类能理解的文本就是解码(decode). 而把人类能理解的变成人类不好理解的字节序列就是编码(encode).

字节概要

python3有两种字节序列, 不可变的 bytes 类型和可变的 bytearray 类型. 字节序列中的各个元素都是介于 [0,255] 之间的整数.

处理编码问题

python自带了超过100中编解码器. 每个编解码器都有一个名称, 甚至有的会有一些别名, 如 utf_8 就有 utf8 , utf-8 , U8 这些别名.

如果字符序列和预期不符, 在进行解码或编码时容易抛出 Unicode*Error 的异常. 造成这种错误是因为目标编码中没有定义某个字符(没有定义某个码位对应的字符), 这里说说解决这类问题的方式.

  • 使用python3, python3可以避免95%的字符问题.

  • 主流编码尝试下: latin1, cp1252, cp437, gb2312, utf-8, utf-16le

  • 留意BOM头部 b'\xff\xfe' , UTF-16编码的序列开头也会有这几个额外字节.

  • 找出序列的编码, 建议使用 codecs 模块

规范化unicode字符串

  1. s1 = 'café'

  2. s2 = 'caf\u00e9'

这两行代码完全等价. 而有一种是要避免的是, 在Unicode标准中 é e\u0301 这样的序列叫 "标准等价物" . 这种情况用NFC使用最少的码位构成等价的字符串:

  1. >>> s1 = 'café'

  2. >>> s2 = 'cafe\u0301'

  3. >>> s1, s2

  4. ('café', 'café')

  5. >>> len(s1), len(s2)

  6. (4, 5)

  7. >>> s1 == s2

  8. False

改进后:

  1. >>> from unicodedata import normalize

  2. >>> s1 = 'café' # 把"e"和重音符组合在一起

  3. >>> s2 = 'cafe\u0301' # 分解成"e"和重音符

  4. >>> len(s1), len(s2)

  5. ( 4, 5)

  6. >>> len(normalize('NFC', s1)), len(normalize('NFC', s2))

  7. (4, 4)

  8. >>> len(normalize('NFD', s1)), len(normalize('NFD', s2))

  9. (5, 5)

  10. >>> normalize('NFC', s1) == normalize('NFC', s2)

  11. True

  12. >>> normalize('NFD', s1) == normalize('NFD', s2)

  13. True

unicode文本排序

对于字符串来说, 比较的码位. 所以在非 ascii 字符时, 得到的结果可能会不尽人意.

第五章: 一等函数

在python中, 函数是一等对象. 编程语言把 "一等对象" 定义为满足下列条件:

  • 在运行时创建

  • 能赋值给变量或数据结构中的元素

  • 能作为参数传给函数

  • 能作为函数的返回结果

在python中, 整数, 字符串, 列表, 字典都是一等对象.

把函数视作对象

Python即可以函数式编程,也可以面向对象编程. 这里我们创建了一个函数, 然后读取它的 __doc__ 属性, 并且确定函数对象其实是 function 类的实例:

  1. def factorial(n):

  2.    '''

  3.    return n

  4.    '''

  5.    return 1 if n < 2 else n * factorial(n-1)

  6. print(factorial.__doc__)

  7. print(type(factorial))

  8. print(factorial(3))

  9. '''

  10. OUT

  11.    return n

  12. function'>

  13. 6

  14. '''

高阶函数

高阶函数就是接受函数作为参数, 或者把函数作为返回结果的函数. 如 map , filter , reduce 等.

比如调用 sorted 时, 将 len 作为参数传递:

  1. fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']

  2. sorted(fruits, key=len)

  3. # ['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']

匿名函数

lambda 关键字是用来创建匿名函数. 匿名函数一些限制, 匿名函数的定义体只能使用纯表达式. 换句话说, lambda 函数内不能赋值, 也不能使用while和try等语句.

  1. fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']

  2. sorted(fruits, key=lambda word: word[::-1])

  3. # ['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']

可调用对象

除了用户定义的函数, 调用运算符即 () 还可以应用到其他对象上. 如果像判断对象能否被调用, 可以使用内置的 callable() 函数进行判断. python的数据模型中有7种可是可以被调用的:

  • 用户定义的函数: 使用def语句或lambda表达式创建

  • 内置函数:如len

  • 内置方法:如dict.get

  • 方法:在类定义体中的函数

  • 类的实例: 如果类定义了 __call__ , 那么它的实例可以作为函数调用.

  • 生成器函数: 使用 yield 关键字的函数或方法.

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

就是可变参数和关键字参数:

  1. def fun(name, age, *args, **kwargs):

  2.    pass

其中 *args **kwargs 都是可迭代对象, 展开后映射到单个参数. args是个元组, kwargs是字典.

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

虽然设计模式与语言无关, 但这并不意味着每一个模式都能在每一个语言中使用. Gamma 等人合著的 《设计模式:可复用面向对象软件的基础》 一书中有 23 个模式, 其中有 16 个在动态语言中"不见了, 或者简化了".

这里不举例设计模式, 因为书里的模式不常用.

第七章: 函数装饰器和闭包

函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。这是一项强大的功 能,但是若想掌握,必须理解闭包。

修饰器和闭包经常在一起讨论, 因为修饰器就是闭包的一种形式. 闭包还是回调式异步编程和函数式编程风格的基础.

装饰器基础知识

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

  1. @decorate

  2. def target():

  3.    print('running target()')

这种写法与下面写法完全等价:

  1. def target():

  2.     print('running target()')

  3. target = decorate(target)

装饰器是语法糖, 它其实是将函数作为参数让其他函数处理. 装饰器有两大特征:

  • 把被装饰的函数替换成其他函数

  • 装饰器在加载模块时立即执行

要理解立即执行看下等价的代码就知道了, target=decorate(target) 这句调用了函数. 一般情况下装饰函数都会将某个函数作为返回值.

变量作用域规则

要理解装饰器中变量的作用域, 应该要理解闭包, 我觉得书里将闭包和作用域的顺序换一下比较好. 在python中, 一个变量的查找顺序是 LEGB (L:Local 局部环境,E:Enclosing 闭包,G:Global 全局,B:Built-in 内建).

  1. base = 20

  2. def get_compare():

  3.     base = 10

  4.    def real_compare(value):

  5.        return value > base

  6.    return real_compare

  7. compare_10 = get_compare()

  8. print(compare_10(5))

在闭包的函数 real_compare 中, 使用的变量 base 其实是 base=10 的. 因为base这个变量在闭包中就能命中, 而不需要去 global 中获取.

闭包

闭包其实挺好理解的, 当匿名函数出现的时候, 才使得这部分难以掌握. 简单简短的解释闭包就是:

名字空间与函数捆绑后的结果被称为一个闭包(closure).

这个名字空间就是 LEGB 中的 E . 所以闭包不仅仅是将函数作为返回值. 而是将名字空间和函数捆绑后作为返回值的. 多少人忘了理解这个 "捆绑" , 不知道变量最终取的哪和哪啊. 哎.

标准库中的装饰器

python内置了三个用于装饰方法的函数: property classmethod staticmethod . 这些是用来丰富类的.

  1. class A(object):

  2.    @property

  3.     def age():

  4.        return 12

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

变量不是盒子

很多人把变量理解为盒子, 要存什么数据往盒子里扔就行了.

  1. a = [ 1,2,3]

  2. b = a

  3. a.append(4)

  4. print(b) # [1, 2, 3, 4]

变量 a b 引用同一个列表, 而不是那个列表的副本. 因此赋值语句应该理解为将变量和值进行引用的关系而已.

标识、相等性和别名

要知道变量a和b是否是同一个值的引用, 可以用 is 来进行判断:

  1. >>> a = b = [4,5,6]

  2. >>> c = [ 4,5,6]

  3. >>> a is b

  4. True

  5. >>> x is c

  6. False

如果两个变量都是指向同一个对象, 我们通常会说变量是另一个变量的 别名 .

在==和is之间选择 运算符 == 是用来判断两个对象值是否相等(注意是对象值). 而 is 则是用于判断两个变量是否指向同一个对象, 或者说判断变量是不是两一个的别名, is 并不关心对象的值. 从使用上, == 使用比较多, 而 is 的执行速度比较快.

默认做浅复制

  1. l1 = [ 3, [55, 44], (7, 8, 9)]

  2. l2 = list(l1) # 通过构造方法进行复制

  3. l2 = l1[:]  #也可以这样想写

  4. >>> l2 == l1

  5. True

  6. >>> l2 is l1

  7. False

尽管 l2 是 l1 的副本, 但是复制的过程是先复制(即复制了最外层容器,副本中的元素是源容器中元素的引用). 因此在操作 l21 时, l11 也会跟着变化. 而如果列表中的所有元素是不可变的, 那么就没有这样的问题, 而且还能节省内存. 但是, 如果有可变元素存在, 就可能造成意想不到的问题.

python标准库中提供了两个工具 copy deepcopy . 分别用于浅拷贝与深拷贝:

  1. import copy

  2. l1 = [3, [55 , 44], (7, 8, 9)]

  3. l2 = copy.copy(l1)

  4. l2 = copy.deepcopy(l1)

函数的参数做引用时

python中的函数参数都是采用共享传参. 共享传参指函数的各个形式参数获得实参中各个引用的副本. 也就是说, 函数内部的形参 是实参的别名.

这种方案就是当传入参数是可变对象时, 在函数内对参数的修改也就是对外部可变对象进行修改. 但这种参数试图重新赋值为一个新的对象时则无效, 因为这只是相当于把参数作为另一个东西的引用, 原有的对象并不变. 也就是说, 在函数内, 参数是不能把一个对象替换成另一个对象的.

不要使用可变类型作为参数的默认值

参数默认值是个很棒的特性. 对于开发者来说, 应该避免使用可变对象作为参数默认值. 因为如果参数默认值是可变对象, 而且修改了它的内容, 那么后续的函数调用上都会收到影响.

del和垃圾回收

在python中, 当一个对象失去了最后一个引用时, 会当做垃圾, 然后被回收掉. 虽然python提供了 del 语句用来删除变量. 但实际上只是删除了变量和对象之间的引用, 并不一定能让对象进行回收, 因为这个对象可能还存在其他引用.

在CPython中, 垃圾回收主要用的是引用计数的算法. 每个对象都会统计有多少引用指向自己. 当引用计数归零时, 意味着这个对象没有在使用, 对象就会被立即销毁.

符合Python风格的对象

得益于 Python 数据模型,自定义类型的行为可以像内置类型那样自然。实现如此自然的 行为,靠的不是继承,而是鸭子类型(duck typing):我们只需按照预定行为实现对象所 需的方法即可。

对象表示形式

每门面向对象的语言至少都有一种获取对象的字符串表示形式的标准方式。Python 提供了 两种方式。

  • repr() : 以便于开发者理解的方式返回对象的字符串表示形式。

  • str() : 以便于用户理解的方式返回对象的字符串表示形式。

classmethod 与 staticmethod

这两个都是python内置提供了装饰器, 一般python教程都没有提到这两个装饰器. 这两个都是在类 class 定义中使用的, 一般情况下, class 里面定义的函数是与其类的实例进行绑定的. 而这两个装饰器则可以改变这种调用方式.

先来看看 classmethod , 这个装饰器不是操作实例的方法, 并且将类本身作为第一个参数. 而 staticmethod 装饰器也会改变方法的调用方式, 它就是一个普通的函数,

classmethod staticmethod 的区别就是 classmethod 会把类本身作为第一个参数传入, 其他都一样了.

看看例子:

  1. >>> class Demo:

  2. ... @classmethod

  3. ... def klassmeth(*args):

  4. ...     return args

  5. ... @staticmethod

  6. ... def statmeth(*args):

  7. ...     return args

  8. ...

  9. >>> Demo.klassmeth()

  10. (<class '__main__.Demo'>,)

  11. >>> Demo.klassmeth('spam')

  12. (< class '__main__.Demo'>, 'spam')

  13. >>> Demo.statmeth()

  14. ()

  15. >>> Demo.statmeth('spam')

  16. ( 'spam',)

格式化显示

内置的 format() 函数和 str.format() 方法把各个类型的格式化方式委托给相应的 .__format__(format_spec) 方法. format_spec 是格式说明符,它是:

  • format(my_obj,format_spec)







请到「今天看啥」查看全文