Python 基础
编码问题
- ASCII编码是1个字节,而Unicode编码通常是2个字节
- 使用UTF-8有一个好处就是,ASCII编码实际上可以被看成是UTF-8编码的一部分。
- 计算机内存编码统一使用Unicode
- ord()取得字符的整数表示 和 chr()把相应的编码转换为字符
- 如果要在网络上传输,str类型要变成bytes类型:
- 如str通过encode()方法可以编码为指定的bytes
1 | 'ABC'.encode('ascii') |
- 使用len()可以查看str有多少个字符,或者查看bytes有多少个字节!
1 | len(b'ABC') |
tuple & list & dict & set
- t = (1) 表示1,而 t = (1,) 表示tuple
- tuple中的list可以改变
- range
1
2
3for name in names:
print(name)
list(range(5)) - 如果dict的key不存在
1
2
3d['dead'] # 会报错
d.get('dead', 55) # 55是默认值,注意不加第二个参数的话,返回None
'dead' in d # return False - dict的key只要是不可变的东西就可以,比如 数(int) 和 str
- set 本质上和dict一样,只是没有value
1
2
3
4
5s = set([1, 2, 3])
s.add(4)
s.remove(2)
s & s
s | s - 不可变对象,如 str, tuple
1
2a = 'abc'
b = a.replace('a', 'A') # a保持不变
函数
- help(abs) 可以查看abs函数的帮助信息
函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于起了个别名
1 | abs # 变量a指向abs函数 a = |
参数检查,如果参数类型不对,自定义的函数不会检查
1 | def my_abs(x): |
默认参数的大坑
1 | # bad |
原因解释如下:
Python函数在定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。定义默认参数要牢记一点:默认参数必须指向不变对象!
可变参数
1 | def calc(*numbers): # 函数内numbers是一个tuple |
如果已经有一个list或者tuple,要调用一个可变参数怎么办?
1 | 1, 2, 3] nums = [ |
关键字参数(参数名未定)
关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。
1 | def person(name, age, **kw): |
1 | def person(name, age, *, city, job): |
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:
1 | def person(name, age, *args, city, job): |
命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:
1 | 'Jack', 24, 'Beijing', 'Engineer') person( |
由于调用时缺少参数名city和job,Python解释器把这4个参数均视为位置参数,但person()函数仅接受2个位置参数。
参数组合
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。
但是请注意,参数定义的顺序必须是:必选参数(positional params)、默认参数、可变参数、命名关键字参数和关键字参数。
比如定义一个函数,包含上述若干种参数:
1 | def f1(a, b, c=0, *args, **kw): # *args 和 **kw中间可以放 d |
所以,对于任意函数,都可以通过类似func(*args,**kw)的形式调用它,无论它的参数是如何定义的。
尾递归
在计算机科学里,尾调用是指一个函数里的最后一个动作是一个函数调用的情形:即这个调用的返回值直接被当前函数返回的情形。
尾调用的重要性在于它可以不在调用栈上面添加一个新的堆栈帧——而是更新它,如同迭代一般。尾递归因而具有两个特征:
- 调用自身函数(Self-called);
- 计算仅占用常量栈空间(Stack Space)。
1 |
|
也叫做 尾调用消除、尾调用优化。这让程序员可以用递归取代循环而不丧失性能。
尾递归事实上和循环是等价的,没有循环语句的编程语言只能通过尾递归实现循环。
高级特性
1行代码能实现的功能,决不写5行代码。请始终牢记,代码越少,开发效率越高。
切片
1 | 1, 2, 3, 4, 5] L = [ |
字符串’xxx’也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串:
1 | 'ABCDEFG'[:3] |
设计一个trim函数练习
1 | def trim(s): |
迭代
默认情况下,dict迭代的是key。如果要迭代value,可以用 for value in d.values();如果要同时迭代key和value,可以用 for k, v in d.items()
1 | from collections import Iterable |
最后一个小问题,如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:
1 | for i, value in enumerate(['A', 'B', 'C']): |
也可以这样。。。
1 | for x, y in [(1, 1), (2, 4), (3, 9)]: |
列表生成式 Lisp Comprehensions
生成list可以用 list(range(1, 11))
或者:
1 |
|
生成器
如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。「解决list大量占用内存的问题」
生成方法:
- 把list生成式改成()
1 | next(g) |
- 有些复杂的就无法用1.的方法生成,但是用函数打印出来又容易:
1 | def fib(max): |
想要把上述代码变成generator,只要把 print(b) 变成 yield b 就可以了
这就是定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator.
这里,最难理解的就是generator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。
比如:
1 | def odd(): |
用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中:
1 | 3) g = fib( |
迭代器
可迭代对象:Iterable「可用于for循环」
- 集合数据类型,如list、tuple、dict、set、str等;
- generator,包括生成器和带yield的generator function。
生成器是只能遍历一次的。
生成器是一类特殊的迭代器。
完全理解Python迭代对象、迭代器、生成器:
https://foofish.net/iterators-vs-generators.html
生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。
可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。「都可以用isinstance判断」
把list、dict、str等Iterable变成Iterator可以使用iter()函数:
1 | isinstance(iter([]), Iterator) |
Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
Python的for循环本质上就是通过不断调用next()函数实现的。
函数式编程
函数式编程虽然可以归结到面向过程的程序设计,但其思想更接近数学计算。
函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。
Key Principle: All computation is the execution of mathematical functions.
Programming paradigm; A mindset
Rooted in mathematics
can’t change their inputs
can’t chagne variables (Immutability)
e.g., x = x + 1; for(i = 0; i < 10; i++) XXXXXXXXXXXXXdon’t iterate
-> Programming without variable assignment; lose some expressability
Not pure / pure func. = return value
Make programming:
- Programs are logical
- Built for distributed computing(no interference between funcitons)
高阶函数
编写高阶函数,就是让函数的参数能够接收别的函数。
变量可以指向函数
函数名也是变量
1
2
3abs = 10
abs(-10)
Traceback传入函数
1
2
3def add(x, y, f):
return f(x) + f(y)
add(-5, 6, abs)
map/reduce
我们先看map。map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。
1 | def f(x): |
由于结果r是一个Iterator,Iterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list。
再看reduce的用法。reduce把一个函数作用在一个序列[x1, x2, x3, …]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:
1 | from functools import reduce |
当然求和运算可以直接用Python内建函数sum(),没必要动用reduce。
两者配合:
1 | from functools import reduce |
求积
1 | def prod(L): |
filter
和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。
把一个序列中的空字符串删掉,可以这么写:
1 | def not_empty(s): |
注意到filter()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list。
筛选素数
先写一个从3开始的奇数序列:「生成器,是一个无限序列」
1 | def _odd_iter(): |
筛选函数:
1 | def _not_divisible(n): |
最后,定义一个生成器,不断返回下一个素数:
1 | def primes(): |
筛选回数
1 | def is_palindrome(n): |
sorted
sorted()函数式一个高阶函数,它可以接收一个key的函数来实现自定义的排序。
1 | sorted([36, 5, -12, 9, -21], key=abs) |
返回函数 ???
1 | def lazy_sum(*args): |
闭包
1 | def count(): |
匿名函数 lambda
当我们不需要显式地定义函数时,直接传入匿名函数更方便!
匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。
装饰器
在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator),本质上是一个返回函数的高阶函数。
1 | def log(func): |
观察上面的log,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:
1 |
|
由于log()是一个decorator,返回一个函数,所以,原来的now()函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。
wrapper()函数的参数定义是(*args,**kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。
如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:
1 | def log(text): |
最后,使用装饰器后,__name__已经从原来的’now’变成’wrapper’。
若代码依赖函数签名,要这样做:
1 | import functools |
在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持decorator。Python的decorator可以用函数实现,也可以用类实现。
偏函数
当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。
假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:
1 | def int2(x, base=2): |
或者用自带的工具:
1 | import functools |
原理:
创建偏函数时,实际上可以接收函数对象、*args和**kw这3个参数。
如 int2 = functools.partial(int, base=2)
实际上是 kw = {‘base’: 2}; int(‘10010’, **kw)
如 max2 = functools.partial(max, 10)
实际上是 args = (10, …); max(*args) 「max(10, …)」
模块
包 Package
引入包以解决模块间的冲突,而且自己创建的模块无论如何都不可以和自带的重名。
mycompany
├─ web
│ ├─ init.py
│ ├─ utils.py
│ └─ www.py
├─ init.py
├─ abc.py
└─ xyz.py
init.py可以是空文件,也可以有Python代码,因为__init__.py本身就是一个模块,而它的模块名就是mycompany。
导入
1 |
|
当我们在命令行运行hello模块文件时,Python解释器把一个特殊变量__name__置为__main__,而如果在其他地方导入该hello模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。
作用域
正常的函数和变量名是公开的(public),可以被直接引用,比如:abc,x123,PI等;
类似__xxx__这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的__author__,__name__就是特殊变量,hello模块定义的文档注释也可以用特殊变量__doc__访问,我们自己的变量一般不要用这种变量名;
类似_xxx和__xxx这样的函数或变量就是非公开的(private),不应该被直接引用,比如_abc,__abc等;
之所以我们说,private函数和变量“不应该”被直接引用,而不是“不能”被直接引用,是因为Python并没有一种方法可以完全限制访问private函数或变量,但是,从编程习惯上不应该引用private函数或变量。
模块搜索
默认情况下,Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在sys模块的path变量中:
1 | import sys |
修改方法:
- 运行时修改:import sys sys.path.append(‘…’)
- 设置环境变量 PYTHONPATH
面向对象编程
三大特点:数据封装、继承和多态(总是不小心忘了)
面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行。为了简化程序设计,面向过程把函数继续切分为子函数,即把大块函数通过切割成小块函数来降低系统的复杂度。
而面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。
假设我们要处理学生的成绩表,为了表示一个学生的成绩:Student这种数据类型应该被视为一个对象,这个对象拥有name和score这两个属性(Property)。如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个print_score消息,让对象自己把自己的数据打印出来。
类和实例
1 | class Student(object): |
有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去。
数据封装:面向对象编程的一个重要特点就是数据封装。在上面的Student类中,每个实例就拥有各自的name和score这些数据。这些数据和逻辑被“封装”起来了,调用很容易,但却不用知道内部实现的细节。
和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同。「有毒」
访问限制
实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问。
有些时候,你会看到以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__name是因为Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量。
错误写法:
1 | 'Bart Simpson', 59) bart = Student( |
表面上看,外部代码“成功”地设置了__name变量,但实际上这个__name变量和class内部的__name变量不是一个变量!内部的__name变量已经被Python解释器自动改成了_Student__name,而外部代码给bart新增了一个__name变量。不信试试:
1 | # get_name()内部返回self.__name bart.get_name() |
继承和多态
1 | isinstance(a, list) |
多态的好处就是,当我们需要传入Dog、Cat、Tortoise……时,我们只需要接收Animal类型就可以了,因为Dog、Cat、Tortoise……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思:
对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
对扩展开放:允许新增Animal子类;
对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。
静态语言 vs 动态语言
对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。
对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了:
1 | class Timer(object): |
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个read()方法,返回其内容。但是,许多对象,只要有read()方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。
获取对象信息
- 使用 type()
判断基本数据类型可以直接写int,str等,但如果要判断一个对象是否是函数怎么办?可以使用types模块中定义的常量:
1 | import types |
- 使用 isinstance()
对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数。(兼容继承关系)
能用type()判断的基本类型也可以用isinstance()判断:
1 | isinstance('a', str) |
总是优先使用isinstance()判断类型,可以将指定类型及其子类“一网打尽”。
- 使用 dir()
如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:
1 | dir('ABC') |
len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len()方法,所以,下面的代码是等价的:
1 | len('ABC') |
所以对于我们自己写的类,如果也想用len(myObj)的话,就自己写一个__len__()方法。
仅仅把属性和方法列出来是不够的,配合getattr()、setattr()以及hasattr(),我们可以直接操作一个对象的状态:
1 | hasattr(obj, 'x') # 有属性'x'吗? |
也可以获得对象的方法:
1 | hasattr(obj, 'power') # 有属性'power'吗? |
实例属性和类属性
类属性:
1 | class Student(object): |
注意类属性的值可以被修改!但是类和各个实例的同名类属性都是独立的!
面向对象高级编程
使用__slots__
除了动态给实例绑定方法外,还可以动态绑定方法(也可以给类动态绑定方法):
1 | def set_age(self, age): # 定义一个函数作为实例方法 |
给类动态绑定方法,会即时影响到已创建的实例,它们也会具有这个方法。
但是,如果我们想要限制实例的属性怎么办?比如,只给Student实例添加name和age属性。
1 | class Student(object): |
可以使用一个__slots__来限制这些属性。这样如果我们调用 s.score = 99 会报错。
注意其对子类不起作用。除非在子类中也定义__slots__,这样,子类实例允许定义的属性就是自身的__slots__加上父类的__slots__。
使用@property
为了限制属性的范围,我们需要手动添加get_attr和set_attr方法。
有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量呢?对于追求完美的Python程序员来说,这是必须要做到的!
Python内置的@property装饰器就是负责把一个方法变成属性调用。
1 | class Student(object): |
@property的实现比较复杂,我们先考察如何使用。把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值。
注意到这个神奇的@property,我们在对实例属性操作的时候,就知道该属性很可能不是直接暴露的,而是通过getter和setter方法来实现的。
还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性。
PS. 设置成只读的属性,需要在__init__方法里面进行初始化。
多重继承 (MixIn设计)
对于需要Runnable功能的动物,就多继承一个Runnable,例如Dog:
1 | class Dog(Mammal, Runnable): |
对于需要Flyable功能的动物,就多继承一个Flyable,例如Bat:
1 | class Bat(Mammal, Flyable): |
MixIn设计
MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。
1 | # 比如,编写一个多进程模式的TCP服务,定义如下: |
定制类
str
1 | def __str__(self): |
但是细心的朋友会发现直接敲变量不用print,打印出来的实例还是不好看!
这是因为直接显示变量调用的不是__str__(),而是__repr__(),两者的区别是__str__()返回用户看到的字符串,而__repr__()返回程序开发者看到的字符串,也就是说,repr()是为调试服务的。在后面加上这一句就好了:repr = __str__。
getattr
正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。比如定义Student类:
1 | class Student(object): |
在类中加上这个的时候,当调用不存在的属性时,Python解释器会试图调用__getattr__(self, ‘score’)
返回函数也是完全可以的:
1 | class Student(object): |
体会一下如下好处:
1 | class Chain(object): |
call
在实例自身调用,可以定义一个__call__方法。
1 | 'Happy') s = Student( |
通过callable()函数,我们就可以判断一个对象是否是“可调用”对象。
使用枚举类
当我们需要定义常量时,一个办法是用大写变量通过整数来定义,例如月份。更好的方法是为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了Enum类来实现这个功能:
1 | from enum import Enum |
如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:
1 | from enum import Enum, unique |
使用元类???
动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。
type()既可以返回一个对象的类型,又可以创建出新的类型。
1 | def fn(self, name='world'): # 先定义函数 |
要创建一个class对象,type()函数依次传入3个参数:
- class的名称;
- 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
- class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上。
通过type()函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()函数创建出class。
Metaclass
除了使用type()动态创建类以外,要控制类的创建行为,还可以使用metaclass。
metaclass,直译为元类,简单的解释就是:
当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。
但是如果我们想创建出类呢?那就必须根据metaclass创建出类,所以:先定义metaclass,然后创建类。
连接起来就是:先定义metaclass,就可以创建类,最后创建实例。
错误、调试和测试
错误处理
1 | try: |
Python的错误其实也是class,所有的错误类型都继承自BaseException,所以在使用except时需要注意的是,它不但捕获该类型的错误,还把其子类也“一网打尽”。比如:
1 | try: |
第二个except永远也捕获不到UnicodeError,因为UnicodeError是ValueError的子类,如果有,也被第一个except给捕获了。
调用栈
如果错误没有被捕获,它就会一直往上抛,最后被Python解释器捕获,打印一个错误信息,然后程序退出。
1 | $ python3 err.py |
出错的时候,一定要分析错误的调用栈信息,才能定位错误的位置。
记录错误
1 | import logging |
自定义抛出错误
1 | # err_raise.py |
调试
断言
1 | assert n != 0, 'n is zero!' |
启动Python解释器时可以用 -O 参数来关闭断言。(相当于全部变成pass)
logging
输出一段文本,不会抛出错误,而且可以输出到文件。
1 | import logging |
这就是logging的好处,它允许你指定记录信息的级别,有:
debug,info,warning,error等几个级别。
当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debug和info就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。
pdb
启动:python -m pdb err.py
单元测试
为了编写单元测试,我们需要引入Python自带的unittest模块,编写mydict_test.py如下:
1 | import unittest |
文档测试 doctest
自动执行写在注释中的这些代码。
1 | def abs(n): |
Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。
IO编程
文件读取
StringIO和BytesIO
操作文件和目录
把两个路径合成一个时,不要直接拼字符串,而要通过os.path.join()函数,这样可以正确处理不同操作系统的路径分隔符。在Linux/Unix/Mac下,os.path.join()返回这样的字符串:
part-1/part-2
同样的道理,要拆分路径时,也不要直接去拆字符串,而要通过os.path.split()函数,这样可以把一个路径拆分为两部分,后一部分总是最后级别的目录或文件名:
1 | '/Users/michael/testdir/file.txt') os.path.split( |
os.path.splitext()可以直接让你得到文件扩展名,很多时候非常方便:
1 | '/path/to/file.txt') os.path.splitext( |
但是复制文件的函数居然在os模块中不存在!原因是复制文件并非由操作系统提供的系统调用。理论上讲,我们通过上一节的读写文件可以完成文件复制,只不过要多写很多代码。
幸运的是shutil模块提供了copyfile()的函数,你还可以在shutil模块中找到很多实用函数,它们可以看做是os模块的补充。
最后看看如何利用Python的特性来过滤文件。比如我们要列出当前目录下的所有目录,只需要一行代码:
1 | for x in os.listdir('.') if os.path.isdir(x)] [x |
序列化
我们把变量从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,在其他语言中也被称之为serialization,marshalling,flattening等等,都是一个意思。
1 | import pickle |
JSON
1 | import json |
如果要对Student类序列化,写一个转换函数把对象转为字典:
1 | def student2dict(std): |
不过,下次如果遇到一个Teacher类的实例,照样无法序列化为JSON。我们可以偷个懒,把任意class的实例变为dict:
1 | json.dumps(s, default=lambda obj: obj.__dict__) |
对中文进行JSON序列化时,json.dumps()提供了一个ensure_ascii参数。
1 | obj = dict(name='小明', age=20) |
进程和线程
多进程
Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
1 | import os |
multiprocessing模块就是跨平台版本的多进程模块。multiprocessing模块提供了一个Process类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束:
1 | from multiprocessing import Process |
创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,这样创建进程比fork()还要简单。
join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
Pool
如果要启动大量的子进程,可以用进程池的方式批量创建子进程:
1 | from multiprocessing import Pool |
对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。
请注意输出的结果,task 0,1,2,3是立刻执行的,而task 4要等待前面某个task完成后才执行,这是因为Pool的默认大小在我的电脑上是4,因此,最多同时执行4个进程。这是Pool有意设计的限制,并不是操作系统的限制。
进程间通信
1 | from multiprocessing import Process, Queue |
外部子进程
多线程
由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。
Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。
启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:
1 | import time, threading |
Lock
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
1 | import time, threading |
如果我们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过threading.Lock()来实现:
1 | balance = 0 |
试试用Python写个死循环:
1 | import threading, multiprocessing |
启动与CPU核心数量相同的N个线程,在4核CPU上可以监控到CPU占用率仅有102%,也就是仅使用了一核。
但是用C、C++或Java来改写相同的死循环,直接可以把全部核心跑满,4核就跑到400%,8核就跑到800%,为什么Python不行呢?
因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。
不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
ThreadLocal
在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。
但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦。
1 | import threading |
进程 vs. 线程
多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。
多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。
多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。
进程 vs. 线程
首先,要实现多任务,通常我们会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务,因此,多任务环境下,通常是一个Master,多个Worker。
如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。
如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。
多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。
多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。
多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。
在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache现在又有多进程+多线程的混合模式,真是把问题越搞越复杂。
计算密集型 vs. IO密集型
是否采用多任务的第二个考虑是任务的类型。我们可以把任务分为计算密集型和IO密集型。
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
异步IO
考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。
现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。
分布式进程
在Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。
Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。
正则表达式
\d 一个数字
\w 一个字母或数字
. 任意字符
\s 一个空格(包括Tab等空白符)
[0-9a-zA-Z_]
要匹配变长的字符,在正则表达式中,用*表示任意个字符(包括0个),用+表示至少一个字符,用?表示0个或1个字符,用{n}表示n个字符,用{n,m}表示n~m个字符
A|B可以匹配A或B,所以(P|p)ython可以匹配’Python’或者’python’。
^表示行的开头,^\d表示必须以数字开头。
$表示行的结束,\d$表示必须以数字结束。
1 | # s = 'ABC\\-001' |
1 | test = '用户输入的字符串' |
切分字符串
1 | 'a b c'.split(' ') |
分组
除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用()表示的就是要提取的分组(Group)。比如:
^(\d{3})-(\d{3,8})$分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码:
1 | match(r'^(\d{3})-(\d{3,8})$', '010-12345') m = re. |
贪婪匹配
最后需要特别指出的是,正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。举例如下,匹配出数字后面的0:
1 | match(r'^(\d+)(0*)$', '102300').groups() re. |