TiffanyBlews's Blog

我们所经历的每个平凡的日常,也许就是连续发生的奇迹。

《流畅的Python》学习笔记

《流畅的Python 第一版》非扫描版PDF电子书网址:https://github.com/silenove/python_ebook/blob/master/%E6%B5%81%E7%95%85%E7%9A%84python.pdf

(第二版已经出版)

对象引用

变量就像一个标签,a = obj就像把a这个标签贴到obj上了。

== 与 is

a == b用于比较两引用的对象(Java中的.equal()),a is b用于比较两引用 是否指向同一对象(id(a) == id(b))(Java中的==

简单区分:

is只用于a is None a is not None,剩下都用==

浅拷贝

import copy

l1 = [0, ['A']]
l2 = list(l1)
l3 = copy.copy(l1)

l2 l3都是l1的浅拷贝,l1中的引用(如列表中的列表)都被直接复制到浅拷贝中,因此其中一个改了其他的也跟着改。

l4 = copy.deepcopy(l1)

l4进行深拷贝时循环将其中所有对象引用复制一份。

共享传参

Python函数传参只支持共享传参call by sharing方式,亦即传引用call by reference,对形参对象的修改等于对实参修改。

可变类型参数

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

函数默认值在定义函数时求解(加载模块时),因此默认值变成了函数对象的属性,因此所有用到此默认值的函数都在用同一个对象,其中一个改了其他的函数也跟着改。

from typing import List
class Bad:
def __init__(self, l = []):
self.l = l

class AllRight:
def __init__(self, l:List[Any] = None):
if (l is not None):
self.l = l
else:
self.l = []

传入可变参数时小心直接修改

from collections.abc import Iterable

class Good:
def __init__(self, l:Iterable = None):
if (l is not None):
self.l = list(l)
else:
self.l = []

del

del是语句,虽然del x等价于del(x),因为Python中x等价于(x)

del删除的是引用,当对象最后一个引用也被删除(或被重新绑定到其他对象)时才销毁对象回收内存。

字符串与字节

基本概念

基本概念 解释 举例
字符character 人类可读的Unicode字符(取值为U+0000~U+10FFFF的数) ‘A’ (U+0041)
字符串string 人类可读的字符序列 ‘AB’ (U+0041 U+0042)
字节序列bytes 对字符串使用某种方式编码后得到的整数序列 b’\x41\x21’(UTF-8)

string —编码-> bytes
string <-解码— bytes

字节

对str来说,str[0] == str[:1],但对于其他任何序列[0]和[:1]都不一样。bytes对象的元素是一个range(256)的整数,bytes的切片还是bytes对象。

café = bytes('café', encoding ='utf8')
print(café) # bytes对象b'caf\xc3\xa9'
print(café[:1]) # bytes对象b'c'
print(café[0]) # 整数99

bytes对象中的部分ASCII字符显示ASCII字符本身,制表符、换行符等显示成转义序列,其他使用十六进制转义序列显示。

编码

UnicodeEncodeError

encode的参数errors指定编码遇到无法处理的字节的时候的行为,ignore跳过,replace替换为?,xmlcharrefreplace替换为对应XML实体,这是唯一不丢失信息的处理方法。

UnicodeDecodeError

使用错误的编码方式解码字节可能会得到乱码的字符串,此时需要chardet包判断字节编码

BOM

BOM是字节序标记(byte-order mark),指明编码时使用Intel CPU的小端序。

café2 = bytes('café', encoding ='utf16')
print(café2) # b'\xff\xfec\x00a\x00f\x00\xe9\x00'
list(café2) # [255, 254, 99, 0, 97, 0, 102, 0, 233, 0]

\xff\xfe (255,254 或者说U+FFFE)就是BOM,因为Unicode中没有U+FEFF,所以只要有\xff\xfe就可以判断是小端序。

UTF-16有两个变种,可以显式指明使用小端序(utf-16le)或是大端序(utf-16be)

字节序只对多字节编码如UTF-16 UTF-32起作用,因此UTF-8不用添加BOM。但有的程序还是会添加BOM,带BOM的UTF-8编码在python中的解码器叫UTF-8-SIG,它的U+FEFF是一个三字节序列b’\xef\xbb\xbf’

最佳实践

  1. Unicode三明治原则,即输入时把字节序列解码成str文本,业务逻辑只处理str,输出时编码成字节序列
  2. 在open中始终明确encoding参数,特别是在Windows系统中
  3. 除非想用chardet判断编码或打开非纯文本文件,否则不要用’b’模式打开文件
  4. 在比较字符串前应使用unicodedata.normalize()规范化字符串,将字符+组合附加符号(两个码点)和自带符号的字符(一个码点)转换成相同的形式,并且用
  5. 字符串排序可以直接使用pyuca或PyICU库,在各个语言各个系统上都能正确工作

函数

高阶函数

接受函数为参数或返回函数的函数是高阶函数,如map filter reduce。但他们都有现代替代品:

# 0 ~ 5 阶乘列表
bad1 = list(map(factorial, range(6)))

good1 = [factorial(n) for n in range(6)]

# 0 ~ 5 中的奇数的阶乘列表
bad2 = list(map(factorial, filter(lambda n : n % 2, range(6))))

good2 = [factorial(n) for n in range(6) if n % 2]

reduce在12.7节

位置参数、关键词参数

def tag(name, *content, class_=None, **attrs):
"""Generate one or more HTML tags"""
if class_ is not None:
attrs['class'] = class_
if attrs:
attr_str = ''.join(' %s="%s"' % (attr, value)
for attr, value
in sorted(attrs.items()))
else:
attr_str = ''
if content:
return '\n'.join('<%s%s>%s</%s>' %
(name, attr_str, c, name) for c in content)
else:
return '<%s%s />' % (name, attr_str)

tag中,class_只能作为关键字参数传入,content可以捕获任意数量的参数作为一个元组,attrs可以捕获任意数量的关键字参数作为一个字典。

仅限关键字参数

*后的参数只能作为关键字参数传入,如tag()*content后的class_。如果不想*content捕获任意数量的参数,可以用*替代。

def f(a, *, b):
return a, b

这里要想传入b只能用f(1, b=2)的方式

仅限位置参数

Python 3.8开始可以使用/定义仅限位置参数的函数:

def divmod(a, b, /):
return (a // b, a % b)

这样就只能用divmod(10, 3)调用,不能写成divmod(a=10, b=3)

函数式编程

operator模块

operator模块含有对象形式的运算符,可以把运算符转换成函数:

from functools import reduce
def factorial(n):
return reduce(lambda a, b: a*b, range(1, n+1))

from operator import mul
def factorial2(n):
return reduce(mul, range(1, n+1))

类似的,获取序列中的元素的[]运算符有operator.itemgetter(),获取对象属性的.operator.attrgetter(),调用对象方法的.operator.methodcaller()等。
from operator import methodcaller
str = 'foo bar'
up = methodcaller('upper')
up(str) == str.upper() # True

hyphenate = methodcaller('replace', ' ', '-')
hyphenate(str) == str.replace(' ', '-') # True

冻结部分参数

如前所述,通过operator.methodcaller()可以为函数指定默认参数,functools.partial也可以做到。

from operator import mul
from functools import partial
triple = partial(mul, 3)
list(map(triple, range(1, 10)))

可以用来把函数包装成更简单的API:
picture = partial(tag, 'img', class_='pic-frame')

装饰器和闭包

装饰器

装饰器是一种语法糖:

def deco(func):
print(f'running deco({func})')
return func

@deco
def f(): # output: running deco(<function f at 0x000001CE2C0BEAF0>)
print(f'running f')

f() # output: running f

相当于
f = deco(f)
f()

装饰器在被装饰的函数定义后执行,一般是模块被导入时。

变量作用域

b = 6
def f(a):
print(a) # 1
print(b) # UnboundLocalError: local variable 'b' referenced before assignment
b = 4
print(b)

f(1)

python不要求声明变量,但会假定在函数主体中赋值的变量是局部变量。想要在函数内引用全局变量的话需要global声明。

b = 6
def f(a):
global b
print(a) # 1
print(b) # 6
b = 4
print(b) # 4

f(1)
print(b) # 4

闭包

def make_averager():
series = []

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

return averager

avg = make_averager()
avg(1)
avg(2)
......

如果想节省内存空间,只保留total和num的话

def make_averager():
total = 0
num = 0

def averager(new_value):
num += 1 # UnboundLocalError: local variable 'num' referenced before assignment
total += new_value
return total / count

return averager

因为num += 1已经算赋值语句了,num会被视为局部变量,需要用到nonlocal num, total,将num, total从外层函数拿进当前作用域,作为自由变量导入averager的闭包中。

改版之前由于series是可变对象,没有进行赋值,所以一直是自由变量,自动导入了averager的闭包中,不会出现问题。

常用装饰器

cache

singledispatch

设计模式

符合设计模式并不表示做得对。
——Ralph Johnson

虽然设计模式与语言无关,但这并不意味着每一个模式都能在每一门语言中使用。活用语言特性可以