Python学习笔记

本文是廖雪峰Python教程的学习笔记,欢迎转载,请标明出处。(更新中。。。)

Python简介

print(‘hello’) -打印括号中的字符串
#!/usr/bin/env python3 -.py第一行注释
chmod a+x hello.py -给hello.py以执行权限
./hello.py -执行程序,也可以用python hello.py
Python交互式环境会把每一行Python代码的结果自动打印出来,但是,直接运行Python代码却不会。

Python基础

Python语法采用缩进方式,约定是4个空格。“#”开头的语句是注释。区分大小写。

数据类型和变量

整数

浮点数

字符串

字符串是以单引号或双引号括起来的任意文本。转义字符\,用r''表示''内部的字符串默认不转义。用'''...'''表示多行的内容

布尔值

TrueFalse,有and,or,not运算

空值

None。此外Python还提供列表、字典等多种数据类型,还允许自定义数据类型

变量

例:

a = 'abc'

Python 解释器干了两件事:1.在内存中创建了一个'abc'的字符串;2.在内存中创建了一个名为a的变量,并把它指向'ABC'

常量

Python中通常用全部大写的变量名表示常量。/表示除法,//表示整除,%表示取余

小结

Python支持多种数据类型,可以把任何数据都看成一个“对象”,而变量就是在程序中用来指向这些数据对象的,对变量赋值就是把数据和变量给关联起来。

注意:Python的整数没有大小限制,Python的浮点数也没有大小限制,但是超出一定范围就直接表示为inf(无限大)。

字符串和编码

字符编码

在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。ascii实际上可以看成是UTF-8编码的一部分。

Python的字符串

在Python3中,字符串是以Unicode编码的,支持多语言。对于单个字符,ord()函数获取字符的整数表示,chr()函数把编码转换为对应的字符。

str字符串类型,内存中以Unicode表示,在网络或磁盘上时为bytesbytes类型数据用带b前缀的单引号或双引号表示:x = b'abc'str通过encode()方法可以编码为bytes: 'abc'.encode('ascii');bytes通过decode()可以编码为str:b'abc'.decode('ascii')len()函数计算str包含多少个字符,计算bytes包含多少个字节。

格式化

与C语言一致,用%实现,举例:输入'hello,%s' % 'world',结果输出'hello,world'

常用占位符:

%d 整数

%f 浮点数

%s 字符串

使用list和tuple

list

list:列表,是一种有序的集合,可以随时添加和删除其中的元素。格式为:

>>> classmates = ['Michael', 'Bob', 'Tracy']`
>>> classmates
['Michael', 'Bob', 'Tracy']

len()函数可以获取list元素的个数len(classmates)
用索引来访问list中的每一个位置的元素,从0开始,最后一个元素的索引可以是len(classmates)-1,也可以是-1

list是一个可变的有序表
往list中追加元素到末尾,用append(),例:classmates.append('adam')
把元素插入到指定索引号的位置,用insert(),例:calssmates.insert(1,'jack')

删除list 末尾的元素,用pop(),例:classmates.pop()
删除指定位置元素,用pop(i),例:classmates.pop(1)

把某个元素替换成别的元素,可以直接赋值给对应索引位置,例:classmates[1] = 'sarah'

list 中的元素数据类型可以不同,也可以是另一个list,类似与二维数组。
list可以为空:L = []

tuple

另一种有序列表叫元组:tuple。和list类似,但是tuple 一旦初始化就不能修改,例:
>>> classmates = ('Michael', 'Bob', 'Tracy')

可以为空:t = (),但只有一个元素时,必须加逗号,t = (1,)

可变的tuple:当tuple中的元素本身可变的时(如list),tuple就可变了:

>>> t = ('a', 'b', ['A', 'B'])
>>> t[2][0] = 'X'
>>> t[2][1] = 'Y'
>>> t
('a', 'b', ['X', 'Y'])

小结

list和tuple是Python内置的有序集合,一个可变,一个不可变。

条件判断

条件判断

if语句,

age = 20
if age >= 18:
    print('your age is', age)
    print('adult')

冒号下一行根据缩进规则,为一个代码块。

还有if-elseif-elif-elseelifelse if的简写,if语句的完整形式是:

if <条件判断1>:
    <执行1>
elif <条件判断2>:
    <执行2>
elif <条件判断3>:
    <执行3>
else:
    <执行4>

注意:冒号:不能忘记。

再议input

birth = input('birth: ')
其中birthstr类型,需要用int()函数来转换:

s = input('birth: ')
birth = int(s)

循环

for…in循环,依次把list或tuple中的每个元素迭代出来。例:

names = ['Michael', 'Bob', 'Tracy']
for name in names:
    print(name)

while循环,只要条件满足,就不断循环,条件不满足时退出循环。例:

sum = 0
n = 99
while n > 0:
    sum = sum + n
    n = n - 2
print(sum)

使用dict和set

dict

dict:字典,全称dictionary,使用键-值(key-value)存储,查找速度快(hash)。例:

>>> d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
>>> d['Michael']
95

把数据放入dict的方法,除了初始化时指定外,还可以通过key放入:

>>> d['Adam'] = 67
>>> d['Adam']
67

一个key只能对应一个value,多次对一个key放入value,后面的值会把前面的值冲掉。

如果key不存在,dict就会报错,要避免key不存在的错误,有两种办法,一是通过in判断key是否存在:

>>> 'Thomas' in d
False

二是通过dict提供的get方法,如果key不存在,可以返回None,或者自己指定的value:

>>> d.get('Thomas')
>>> d.get('Thomas', -1)
-1

要删除一个key,用pop(key)方法:

>>> d.pop('Bob')
75

注意:dict的key必须是不可变对象。所以,list不能作为key对象。

set

set和dict类似,有一组key的集合,但不存储value,且key不重复。要创建一个set,需要提供一个list作为输入集合:

>>> s = set([1, 2, 3])
>>> s
{1, 2, 3}

通过add(key)方法可以添加元素到set中,

>>> s.add(4)

通过remove(key)方法可以删除元素:

>>> s.remove(4)

函数

函数是最基本的一种代码抽象的方式。

调用函数

官方函数文档:
http://docs.python.org/3/library/functions.html#abs

abs()求绝对值函数:

>>> abs(100)
100
>>> abs(-20)
20
>>> abs(12.34)
12.34

max()求最大值函数,可以接受任意多个参数,并返回最大的那个:

>>> max(1, 2)
2
>>> max(2, 3, 1, -5)
3

数据类型转换

int(),float(),str(),bool()

函数名是指向一个函数对象的引用,可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:

>>> a = abs # 变量a指向abs函数
>>> a(-1) # 所以也可以通过a调用abs函数
1

定义函数

使用def语句,依次写出函数名、括号、括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回。例:

def my_abs(x):
    if x >= 0:
        return x
    else:
        return -x

没有return语句,函数执行完毕后会返回Nonereturn None可以简写为return

可以将函数定义在.py文件中,然后在该文件的目录下启动Python解释器,用from 文件名 import 函数名来导入函数。

空函数

pass语句,作为占位符,但可以让代码先运行起来:

def nop():
    pass

参数检查

调用函数时,如果参数个数或类型不对,Python解释器会自动检查出来,并抛出TypeError

返回多个值

其实就是返回一个tuple,函数返回可以不加括号。

小结

定义函数时,需要确定函数名和参数个数;

如果有必要,可以先对参数的数据类型做检查;

函数体内部可以用return随时返回函数结果;

函数执行完毕也没有return语句时,自动return None。

函数可以同时返回多个值,但其实就是一个tuple。

函数的参数

位置参数

个数和位置都严格规定的参数,
例:

def power(x):
    return x * x

调用power函数时,必须且只能传入一个参数。
例:

def power(x, n):
    s = 1
    while n > 0:
        n = n - 1
        s = s * x
    return s

调用这个power函数时,必须且只能传入两个参数,且会依次赋值给x,n

默认参数

例:

def power(x, n=2):
    s = 1
    while n > 0:
        n = n - 1
        s = s * x
    return s

power函数的第二个参数n就是默认参数,默认值为2。如调用power(5)时,n就为2;而调用power(5,3)时n为3。
注意
1.必选参数在前,如x;
2.当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面;
3.有多个默认参数,不按顺序提供部分默认参数时,需要把参数名写上;
4.默认参数必须指向不变对象。

在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。

可变参数

传入的参数个数可变。
例:

def calc(*numbers):
    sum = 0
    for n in numbers:
        sum = sum + n * n
    return sum

仅仅在参数前面加了一个*号,在函数内部,参数numbers接收到的是一个tuple。

Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去,如:

>>> nums = [1, 2, 3]
>>> calc(*nums)
14

关键字参数

允许传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。例:

def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)

当一个dict作为一个整体传入时,可以在在dict之前加上**,例:

>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数,kw将获得一个dict,注意kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra

命名关键字参数

要限制关键字参数的名字,可以用命名关键字参数,例如,只接收city和job作为关键字参数:

def person(name, age, *, city, job):
    print(name, age, city, job)

和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符**后面的参数被视为命名关键字参数。命名关键字参数必须传入参数名,调用方式如下:

>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer

命名关键字参数可以有缺省值。

参数组合

5种参数组合可以使用,但可变参数无法和命名关键字参数混合。参数定义的顺序必须是:必选参数、默认参数、可变参数/命名关键字参数和关键字参数。

对于任意函数,都可以通过类似func(*args, **kw)的形式调用它,无论它的参数是如何定义的。

小结

Python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。

默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误!

要注意定义可变参数和关键字参数的语法:

*args是可变参数,args接收的是一个tuple;

**kw是关键字参数,kw接收的是一个dict。

以及调用函数时如何传入可变参数和关键字参数的语法:

可变参数既可以直接传入:func(1, 2, 3),又可以先组装list或tuple,再通过*args传入:func(*(1, 2, 3))

关键字参数既可以直接传入:func(a=1, b=2),又可以先组装dict,再通过**kw传入:func(**{'a': 1, 'b': 2})

使用*args**kw是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。

命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。

定义命名的关键字参数不要忘了写分隔符*,否则定义的将是位置参数。

递归函数

定义:一个函数在内部调用自己本身。
例:阶乘函数

def fact(n):
    if n==1:
        return 1
    return n * fact(n - 1)

递归函数可能会栈溢出。

尾递归:在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。例:

def fact(n):
    return fact_iter(n, 1)

def fact_iter(num, product):
    if num == 1:
        return product
    return fact_iter(num - 1, num * product)

但Python标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题。

高级特性

代码越少越好,越简单越好。代码越少,开发效率越高。1行代码能实现的功能,决不写5行代码。

切片

取一个list、tuple或字符串的部分元素。

例:

>>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']
>>> L[0:3]
['Michael', 'Sarah', 'Tracy']

L[0:3]表示,从索引0开始取,直到索引3为止,但不包括索引3
第一个索引是0的话可以省略,可以写成L[:3]
支持倒数切片如:

>>> L[-2:]
['Bob', 'Jack']
>>> L[-2:-1]
['Bob']

可以每n个元素,取一个数,如L[:10:2]表示前10个数,每2个取一个。
[:]表示原样复制


创建一个0-99的数列:

>>> L = list(range(100))
>>> L
[0, 1, 2, 3, ..., 99]

前10个数:

>>> L[:10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

后10个数:

>>> L[-10:]
[90, 91, 92, 93, 94, 95, 96, 97, 98, 99]

前11-20个数:

>>> L[10:20]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

所有数,每5个取一个:

>>> L[::5]
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]

原样复制一个list:

>>> L[:]
[0, 1, 2, 3, ..., 99]

倒序:

>>> L[::-1]
[99, 98, 97, 96, ..., 0]

字符串、tuple可以有同样的操作,切片的结果还是字符串、tuple。

小结

切片减少了循环的使用,减少了代码量。

迭代

遍历一个list或tuple叫做迭代。
可以通过for...in语句来完成,如:

>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> for key in d:
...     print(key)
...
a
c
b

dict迭代的是key;若要迭代value,可以用for value in d.value;若要同时迭代key和value,可以用for k, v in d.iterms()
字符串也可用for循环来迭代。

只要是一个可迭代的工具都可以用for来迭代。
collection模块的Iterable类型可以判断一个对象是否可迭代:

>>> from collections import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整数是否可迭代
False

enumerate函数可以把一个list变成索引-元素对,可以在for循环中同时迭代索引和元素本身,如:

>>> for i, value in enumerate(['A', 'B', 'C']):
...     print(i, value)
...
0 A
1 B
2 C

小结

任何可迭代对象都可以作用于for循环,包括我们自定义的数据类型,只要符合迭代条件,就可以使用for循环。

列表生成式

Python内置的非常简单却强大的可以用来创建list的生成式。
例:

>>> [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

还可以加上if判断:

>>> [x * x for x in range(1, 11) if x % 2 == 0]
[4, 16, 36, 64, 100]

可以使用两层排序:注意 是两层循环

>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']

可以使用两个变量来生成list:

>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> [k + '=' + v for k, v in d.items()]
['y=B', 'x=A', 'z=C']

os.listdir()可以列出文件和目录

小结

运用列表生成式,可以快速生成list,可以通过一个list推导出另一个list,而代码却十分简洁。

生成器

列表生成式直接在内存中创建了一个列表,有时显得浪费内存。

生成器(generator)提供一种一边循环一遍计算生成列表中数据的方法。生成器中保存着生成列表中数据的算法。

创建generator:
一种方法:只要把一个列表生成式的[]改成():

>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
 at 0x1022ef630>

可以用next()函数获得generator的下一个返回值:

>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9

也可以调用for循环:

>>> g = (x * x for x in range(10))
>>> for n in g:
...     print(n)
... 
0
1
4
9
16
25
36
49
64
81

另一种创建generator的方法跟创建函数的方法类似:用yield代替return,例如:

斐波拉契数列函数定义是:

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        print(b)
        a, b = b, a + b
        n = n + 1
    return 'done'

斐波拉契数列的generator定义是:

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator:

>>> f = fib(6)
>>> f
0x104feaaa0>

不同点:函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

小结

generator是非常强大的工具,可以简单地把列表生成式改成generator,也可以通过函数实现复杂逻辑的generator。

要理解generator的工作原理,它是在for循环的过程中不断计算出下一个元素,并在适当的条件结束for循环。对于函数改成的generator来说,遇到return语句或者执行到函数体最后一行语句,就是结束generator的指令,for循环随之结束。

请注意区分普通函数和generator函数,普通函数调用直接返回结果:

>>> r = abs(6)
>>> r
6

generator函数的“调用”实际返回一个generator对象:

>>> g = fib(6)
>>> g
0x1022ef948>

迭代器

可用for循环的数据类型:
- 集合数据类型,如listtupledictsetstr
- generator,包括生成器和带yield的generator function
直接作用于for循环的对象统称为可迭代对象:Iterable,可用isinstance() 判断:

>>> from collections import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance((x for x in range(10)), Iterable)
True
>>> isinstance(100, Iterable)
False

生成器可被next()函数不断调用并返回下一个值。可被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator,可用isinstance()判断:

>>> from collections import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('abc', Iterator)
False

生成器都是Iterator对象,但listdictstr虽然是Iterable,却不是Iterator。但iter() 函数可使Iterable变成Iterator:

>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True

Iterator对象表示的是一个数据流,可以看做是一个有序序列,但不能提前知道序列的长度,只能通过next()函数实现按需计算下一个数据,所以Iterator计算是惰性的,只有在需要返回下一个数据时它才会计算。Iterator可以表示一个无限大的数据流。

小结

凡是可作用于for循环的对象都是Iterable类型;

凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;

集合数据类型如listdictstr等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。

for循环本质上是通过不断调用next()函数实现的,例如:

for x in [1, 2, 3, 4, 5]:
    pass

实际上完全等价于:

# 首先获得Iterator对象:
it = iter([1, 2, 3, 4, 5])
# 循环:
while True:
    try:
        # 获得下一个值:
        x = next(it)
    except StopIteration:
        # 遇到StopIteration就退出循环
        break

函数式编程

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用

函数式编程的一个特点是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数

Python对函数式编程提供部分支持。Python允许使用变量,Python不是纯函数式编程语言。

高阶函数

  • 变量可以指向函数
    函数本身可以赋值给变量,且可以通过该变量来调用此函数

  • 函数名也是变量
    当函数名被被赋值后不能再调用原函数

  • 函数可以作为参数传给另一个函数
    拥有 函数参数 的函数称为高阶函数

小结

把函数作为参数传入,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式。

map / reduce

Python内建了map()reduce()函数。

  • map()函数
    接收两个参数,一个是函数,一个是Iterablemap将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。
    例:list中的数平方
>>> def f(x):
...     return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]

例:list中的数转成字符串

>>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
['1', '2', '3', '4', '5', '6', '7', '8', '9']
  • reduce()函数
    reduce把一个函数作用在一个序列[x1, x2, x3, …]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:

    reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)

    例:序列求和:(可以用sum())

    >>> from functools import reduce
    >>> def add(x, y):
    ...     return x + y
    ...
    >>> reduce(add, [1, 3, 5, 7, 9])
    25

    例:str2int函数

    from functools import reduce
    
    def str2int(s):
    def fn(x, y):
        return x * 10 + y
    def char2num(s):
        return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[s]
    return reduce(fn, map(char2num, s))

filter

filter()函数用于过滤序列,接收一个函数和一个序列,filter()把传入的函数依次作用于每个元素,根据返回值是True还是False决定保留还是丢弃该元素。

例:去掉偶数

def is_odd(n):
    return n % 2 == 1

list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
# 结果: [1, 5, 9, 15]

例:去掉空字符串

def not_empty(s):
    return s and s.strip()

list(filter(not_empty, ['A', '', 'B', None, 'C', '  ']))
# 结果: ['A', 'B', 'C']

用filter求素数

算法是艾氏筛法:

首先,列出从2开始的所有自然数,构造一个序列:

2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …

取序列的第一个数2,它一定是素数,然后用2把序列的2的倍数筛掉:

3, 5, 7, 9, 11, 13, 15, 17, 19, …

取新序列的第一个数3,它一定是素数,然后用3把序列的3的倍数筛掉:

5, 7, 11, 13, 17, 19, …

取新序列的第一个数5,然后用5把序列的5的倍数筛掉:

7, 11, 13, 17, 19, …

不断筛下去,就可以得到所有的素数。

先构造一个从3开始的奇数序列生成器:

def _odd_iter():
    n = 1
    while True:
        n = n + 2
        yield n

再定义一个筛选函数:

def _not_divisible(n):
    return lambda x: x % n > 0

最后,定义一个生成器,不断返回下一个素数:

def primes():
    yield 2
    it = _odd_iter() # 初始序列
    while True:
        n = next(it) # 返回序列的第一个数
        yield n
        it = filter(_not_divisible(n), it) # 构造新序列

由于primes()也是一个无限序列,所以调用时需要设置一个退出循环的条件:

# 打印1000以内的素数:
for n in primes():
    if n < 1000:
        print(n)
    else:
        break

小结

filter()的作用是从一个序列中筛出符合条件的元素。由于filter()使用了惰性计算,所以只有在取filter()结果的时候,才会真正筛选并每次返回下一个筛出的元素。

sorted

排序算法,核心是比较两个元素的大小。数字,可以直接比较,但如果是字符串或者两个dict,比较的过程必须通过函数抽象出来。
对list就行排序:

>>> sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]

sorted()函数是一个高阶函数,可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序:

>>> sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]

key指定的函数将作用于list的每一个元素上,然后再根据key函数返回的结果进行排序。
反向排序,可以传入第三个参数reverse=True:

>>> sorted([36, 5, -12, 9, -21], key=abs)
[36, -21, -12, 9, 5]

高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。

小结

sorted()是一个高阶函数。用sorted()排序的关键在于实现一个映射函数。

返回函数

高阶函数可以把函数作为结果值返回
例:求和函数

def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum

返回的不是求和结果,而是求和函数,调用函数f时,才真正计算求和的结果。

>>> f()
25

注意:当调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数。

闭包:当函数lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中。
闭包注意点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

小结

一个函数可以返回一个计算结果,也可以返回一个函数。
返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量。

匿名函数

在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便。例:

>>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
[1, 4, 9, 16, 25, 36, 49, 64, 81]

匿名函数lambda x: x * x实际上就是:

def f(x):
    return x * x

关键字lambda表示匿名函数,冒号前面的x表示函数参数。匿名函数只能有一个表达式,不用return,返回值就是该表达式的结果。

匿名函数可以作为返回值返回。

小结

Python对匿名函数的支持有限,只有一些简单的情况下可以使用匿名函数。

装饰器

函数是一个对象,可以被赋值给一个变量,所以可以通过变量调用该函数,例:

>>> def now():
...     print('2015-3-25')
...
>>> f = now
>>> f()
2015-3-25

函数对象有一个__name__属性,函数的名字:

>>> now.__name__
'now'
>>> f.__name__
'now'

“装饰器”(Decorator)能在代码运行期间动态增加功能,本质上,是一个返回函数的高阶函数。
例:定义一个在调用函数时自动打印日志的decorator:

def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

将decorator置于函数的定义处:

@log
def now():
    print('2015-3-25')

调用函数now():

>>> now()
call now():
2015-3-25

@log放到now()函数的定义处,相当于执行了语句:

now = log(now)

log()是一个decorator,返回一个函数,原来的now()函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。

若decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数。比如,要自定义log的文本:

def log(text):
    def decorator(func):
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

这个3层嵌套的decorator用法如下:

@log('execute')
def now():
    print('2015-3-25')

执行结果:

>>> now()
execute now():
2015-3-25

和两层嵌套的decorator相比,3层嵌套的效果是这样的:

>>> now = log('execute')(now)

经过decorator装饰之后的函数,它们的__name__已经从原来的'now'变成了'wrapper'

>>> now.__name__
'wrapper'

Python内置的functools.wraps可以保持原函数名不变,一个完整的decorator的写法如下:

import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

或者针对带参数的decorator:

import functools

def log(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

小结

在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持decorator。Python的decorator可以用函数实现,也可以用类实现。

偏函数

functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单,可以接收函数对象、*args**kw这3个参数。
例:

>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85

小结

当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。

模块

一个.py文件就称之为一个模块(Module)。

模块可提高代码的可维护性;写好的模块可以被其他地方引用;我们写程序时可以引用Python内置的模块和第三方的模块。

相同名字的函数和变量完全可以分别存在不同的模块中,但是也要注意,尽量不要与内置函数名字冲突。

包(Package)可以用来按目录组织模块。

一个abc.py的文件就是一个名字叫abc的模块,当abc.py在目录mycompany下存放时,就是一个名字叫mycompany.abc的模块。

每一个包目录下面都会有一个__init__.py的文件,且必须存在,__init__.py本身就是一个模块,而它的模块名就是包目录的名字。

可以有多级目录,组成多级层次的包结构。

使用模块

Python内置了很多非常有用的模块,只要安装完毕,这些模块就可以立刻使用。
以内建的sys模块为例,编写一个hello的模块:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

' a test module '

__author__ = 'Michael Liao'

import sys

def test():
    args = sys.argv
    if len(args)==1:
            print('Hello, world!')
    elif len(args)==2:
        print('Hello, %s!' % args[1])
    else:
        print('Too many arguments!')

if __name__=='__main__':
    test()

第4行是一个字符串,表示模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释;

第6行使用__author__变量把作者写进去

使用sys模块的第一步,就是导入该模块:

import sys

导入sys模块后,有变量sys指向该模块,可利用sys变量访问sys模块的所有功能。

sys模块有argv变量,用list存储了命令行的所有参数。argv至少有一个元素,因为第一个参数永远是该.py文件的名称,例:
运行python3 hello.py获得的sys.argv就是['hello.py']
运行python3 hello.py Michael获得的sys.argv就是['hello.py', 'Michael]

最后两行代码:

if __name__=='__main__':
    test()

在命令行运行hello模块文件时,Python解释器把一个特殊变量__name__置为__main__,而在其他地方导入hello模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。

命令行运行hello.py:

$ python3 hello.py
Hello, world!
$ python hello.py Michael
Hello, Michael!

启动Python交互环境,再导入hello模块:

$ python3
Python 3.4.3 (v3.4.3:9b73f1c3e601, Feb 23 2015, 02:52:03) 
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import hello
>>>

调用hello.test()时,才能打印出Hello, word!:

>>> hello.test()
Hello, world!

作用域

正常的函数和变量名是公开的(public),可以被直接引用,比如:abc,x123,PI等;

类似__xxx__这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的__author____name__就是特殊变量

类似_xxx__xxx这样的函数或变量就是非公开的(private),不应该被直接引用,比如_abc__abc等;

例:

def _private_1(name):
    return 'Hello, %s' % name

def _private_2(name):
    return 'Hi, %s' % name

def greeting(name):
    if len(name) > 3:
        return _private_1(name)
    else:
        return _private_2(name)

模块里公开greeting()函数,把内部逻辑用private函数隐藏起来,调用greeting()函数不用关心内部的private函数细节,这也是一种非常有用的代码封装和抽象的方法:

外部不需要引用的函数全部定义成private,只有外部需要引用的函数才定义为public。

安装第三方库

安装第三方模块,是通过包管理工具pip完成的。
注意:Mac或Linux上有可能并存Python 3.x和Python 2.x,因此对应的pip命令是pip3
例:安装Pillow

pip install Pillow

模块搜索路径

默认情况下,Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块

添加自己的搜索目录,有两种方法:

一是直接修改sys.path,添加要搜索的目录,在运行时修改,运行结束后失效。:

>>> import sys
>>> sys.path.append('/Users/michael/my_py_scripts')

第二种方法是设置环境变量PYTHONPATH,该环境变量的内容会被自动添加到模块搜索路径中。设置方式与设置Path环境变量类似。注意只需要添加你自己的搜索路径,Python自己本身的搜索路径不受影响。

面向对象编程

面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。

面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行。

面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。

在Python中,所有数据类型都可以视为对象,也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。

面向对象的设计思想是抽象出Class(类),根据Class创建Instance(实例)。

数据封装、继承和多态是面向对象的三大特点。

类和实例

面向对象最重要的概念就是 类(Class)实例(Instance),类是抽象的模板,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。

在Python中,定义类是通过class关键字:

class Student(object):
    pass

创建实例是通过类名+()实现的:

>>> bart = Student()
>>> bart
<__main__.Student object at 0x10a67a590>
>>> Student
<class '__main__.Student'>

可以自由地给一个实例变量绑定属性:

>>> bart.name = 'Bart Simpson'
>>> bart.name
'Bart Simpson'

也通过定义一个特殊的__init__方法,在创建实例的时候,就把namescore等属性绑上去:

class Student(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score

注意到__init__方法的第一个参数永远是self,表示创建的实例本身。

有了__init__方法,在创建实例的时候,必须传入与__init__方法匹配的参数,但self不需要传:

>>> bart = Student('Bart Simpson', 59)
>>> bart.name
'Bart Simpson'
>>> bart.score
59

和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。

数据封装

面向对象编程的一个重要特点就是数据封装。

在上面的Student类中,每个实例就拥有各自的namescore这些数据,可以直接在Student类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。这些封装数据的函数是和Student类本身是关联起来的,我们称之为类的方法:

class Student(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score

    def print_score(self):
        print('%s: %s' % (self.name, self.score))

要定义一个方法,除了第一个参数是self外,其他和普通函数一样。要调用一个方法,只需要在实例变量上直接调用,除了self不用传递,其他参数正常传入:

小结

类是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;

方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据;

通过在实例上调用方法,我们就直接操作了对象内部的数据,但无需知道方法内部的实现细节。

和静态语言不同,Python允许对实例变量绑定任何数据。对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同:

>>> bart = Student('Bart Simpson', 59)
>>> lisa = Student('Lisa Simpson', 87)
>>> bart.age = 8
>>> bart.age
8
>>> lisa.age
Traceback (most recent call last):
  File "", line 1, in 
AttributeError: 'Student' object has no attribute 'age'

访问限制

在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问:

class Student(object):

    def __init__(self, name, score):
        self.__name = name
        self.__score = score

    def print_score(self):
        print('%s: %s' % (self.__name, self.__score))

这样就无法从外部访问实例变量.__name实例变量.__score

>>> bart = Student('Bart Simpson', 98)
>>> bart.__name
Traceback (most recent call last):
  File "", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'

外部代码可以给Student类增加get_nameget_score的方法来获取namescore,也可以通过set_scoreset_name的方法来修改,在方法中可以对参数做检查,避免传入无效的参数:

class Student(object):
    ...

    def set_score(self, score):
        if 0 <= score <= 100:
            self.__score = score
        else:
            raise ValueError('bad score')

__xxx__的变量名,以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量。

_name的变量名,以一个下划线开头,外部是可以访问的,但约定俗成是不要访问的。

双下划线开头的实例变量,如__name,可以通过_Student__name来访问__name变量:

>>> bart._Student__name
'Bart Simpson'

Python本身没有任何机制阻止你干坏事,一切全靠自觉。

继承和多态

在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。

例:
一个名为Animal的class,有一个run()方法可以直接打印:

class Animal(object):
    def run(self):
        print('Animal is running...')

编写Dog和Cat类时,就可以直接从Animal类继承:

class Dog(Animal):

    def run(self):
        print('Dog is running...')

class Cat(Animal):

    def run(self):
        print('Cat is running...')

子类获得了父类的全部功能,

但当子类和父类都存在相同的run()方法时,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。继承的另一个好处:多态。

当定义一个class的时候,实际上就定义了一种数据类型。定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样。

在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。

多态真正的威力:调用方只管调用,不管细节,而当新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:

对扩展开放:允许新增Animal子类;

对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。

静态语言 vs 动态语言

对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。

对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了

Python中许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。

小结

继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写。

动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。

获取对象信息

使用type()

使用type()函数判断对象类型

  • 基本类型判断:
>>> type(123)
<class 'int'>
>>> type('str')
<class 'str'>
>>> type(None)
<type(None) 'NoneType'>
  • 指向函数或者类的变量:
>>> type(abs)
<class 'builtin_function_or_method'>
>>> type(a)
<class '__main__.Animal'>

使用types模块中定义的常量来判断一个对象是否是函数:

>>> import types
>>> def fn():
...     pass
...
>>> type(fn)==types.FunctionType
True
>>> type(abs)==types.BuiltinFunctionType
True
>>> type(lambda x: x)==types.LambdaType
True
>>> type((x for x in range(10)))==types.GeneratorType
True

使用isinstance()

对于class的继承关系来说,使用type()就很不方便。

  • 判断class的类型,可以使用isinstance()函数。
>>> h = Husky()
>>>> isinstance(h, Husky)
True
  • 能用type()判断的基本类型也可以用isinstance()判断:
>>> isinstance('a', str)
True
>>> isinstance(123, int)
True
>>> isinstance(b'a', bytes)
True
  • 并且还可以判断一个变量是否是某些类型中的一种
>>> isinstance([1, 2, 3], (list, tuple))
True
>>> isinstance((1, 2, 3), (list, tuple))
True

使用dir()

使用dir()函数,可以获得一个对象的所有属性和方法,它返回一个包含字符串的list:

>>> dir('ABC')
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

类似__xxx__的属性和方法有特殊用途,如__len__方法返回长度,调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法。

剩下的都是普通属性或方法,比如lower()返回小写的字符串。

getattr()setattr()以及hasattr(),可以直接操作一个对象的状态:

可以测试该对象的属性:

>>> hasattr(obj, 'x') # 有属性'x'吗?
True
>>> obj.x
9
>>> hasattr(obj, 'y') # 有属性'y'吗?
False
>>> setattr(obj, 'y', 19) # 设置一个属性'y'
>>> hasattr(obj, 'y') # 有属性'y'吗?
True
>>> getattr(obj, 'y') # 获取属性'y'
19
>>> obj.y # 获取属性'y'
19

可以获得对象的方法:

>>> hasattr(obj, 'power') # 有属性'power'吗?
True
>>> getattr(obj, 'power') # 获取属性'power'
0x10077a6a0>>
>>> fn = getattr(obj, 'power') # 获取属性'power'并赋值到变量fn
>>> fn # fn指向obj.power
0x10077a6a0>>
>>> fn() # 调用fn()与调用obj.power()是一样的
81

实例属性和类属性

由于Python是动态语言,根据类创建的实例可以任意绑定属性。

给实例绑定属性的方法是通过实例变量,或者通过self变量:

class Student(object):
    def __init__(self, name):
        self.name = name

s = Student('Bob')
s.score = 90

如果Student类本身需要绑定一个属性,可以直接在class中定义属性,是类属性,归Student类所有,但类的所有实例都可以访问到:

class Student(object):
    name = 'Student'
>>> class Student(object):
...     name = 'Student'
...
>>> s = Student() # 创建实例s
>>> print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
Student
>>> print(Student.name) # 打印类的name属性
Student
>>> s.name = 'Michael' # 给实例绑定name属性
>>> print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
Michael
>>> print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
Student
>>> del s.name # 如果删除实例的name属性
>>> print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student

相同名称的实例属性将屏蔽掉类属性,但是当删除实例属性后,再使用相同的名称,访问到的将是类属性。

面向对象高级编程

数据封装、继承和多态只是面向对象程序设计中最基础的3个概念。在Python中,面向对象还有很多高级特性,允许我们写出非常强大的功能。

我们会讨论多重继承、定制类、元类等概念。

使用__slots__

正常情况下,我们可以给一个创建后的实例绑定任何的属性和方法:

先定义class:

class Student(object):
    pass

给实例绑定一个属性:

>>> s = Student()
>>> s.name = 'Michael' # 动态给实例绑定一个属性
>>> print(s.name)
Michael

给实例绑定一个方法:

>>> def set_age(self, age): # 定义一个函数作为实例方法
...     self.age = age
...
>>> from types import MethodType
>>> s.set_age = MethodType(set_age, s) # 给实例绑定一个方法
>>> s.set_age(25) # 调用实例方法
>>> s.age # 测试结果
25

但是,给一个实例绑定的属性和方法,对另一个实例是不起作用的。

为了给所有实例都绑定方法,可以给class绑定方法:

>>> def set_score(self, score):
...     self.score = score
...
>>> Student.set_score = set_score

使用__slots__

Python允许在定义class的时候,定义一个特殊的__slots__变量,来限制该class实例能添加的属性:

class Student(object):
    __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称

使用@property

当给实例的属性赋值时,应该限制参数的范围,能够检查参数,不能随意更改属性:

s = Student()
s.score = 9999

可以通过set_score()方法来检查参数,设置成绩,再通过一个get_score()来获取成绩:

class Student(object):

    def get_score(self):
         return self._score

    def set_score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

但是,上面的调用方法又略显复杂,没有直接用属性这么直接简单。

Python内置的@property装饰器就是负责把一个方法变成属性调用的:

class Student(object):

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作:

>>> s = Student()
>>> s.score = 60 # OK,实际转化为s.set_score(60)
>>> s.score # OK,实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性:

class Student(object):

    @property
    def birth(self):
        return self._birth

    @birth.setter
    def birth(self, value):
        self._birth = value

    @property
    def age(self):
        return 2015 - self._birth

上面的birth是可读写属性,而age就是一个只读属性。

小结

@property广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。

多重继承

继承是面向对象编程的一个重要的方式,因为通过继承,子类就可以扩展父类的功能。Python允许多重继承,让子类拥有多重功能:

class Dog(Mammal, Runnable):
    pass

MixIn

在设计类的继承关系时,通常,主线都是单一继承下来的,如果需要“混入”额外的功能,通过多重继承就可以实现,这种设计通常称之为MixIn:

class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
    pass

小结

由于Python允许使用多重继承,因此,MixIn就是一种常见的设计。

只允许单一继承的语言(如Java)不能使用MixIn的设计。

定制类

形如__xxx__的变量或者函数名就要注意,这些在Python中是有特殊用途的。

__str__()

定义打印实例时的输出:

>>> class Student(object):
...     def __init__(self, name):
...         self.name = name
...     def __str__(self):
...         return 'Student object (name: %s)' % self.name
...
>>> print(Student('Michael'))
Student object (name: Michael)

直接显示变量调用的是__repr__(),但是通常__str__()__repr__()代码都是一样的,可以这样:

class Student(object):
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return 'Student object (name=%s)' % self.name
    __repr__ = __str__

__iter__()

一个类想被用于for ... in循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。如:斐波那契数列

class Fib(object):
    def __init__(self):
        self.a, self.b = 0, 1 # 初始化两个计数器a,b

    def __iter__(self):
        return self # 实例本身就是迭代对象,故返回自己

    def __next__(self):
        self.a, self.b = self.b, self.a + self.b # 计算下一个值
        if self.a > 100000: # 退出循环的条件
            raise StopIteration();
        return self.a # 返回下一个值

__getitem__

上面的Fib实例虽然能作用于for循环,但不能用下标取出元素。要实现__getitem__()方法:

class Fib(object):
    def __getitem__(self, n):
        a, b = 1, 1
        for x in range(n):
            a, b = b, a + b
        return a

有:

>>> f = Fib()
>>> f[0]
1
>>> f[1]
1
>>> f[2]
2

当然list还有切片等功能,也可以通过定义__getitem__()来实现

总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。

__getattr__

实现这个方法后,当我们调用实例的属性或方法不存在时,会调用__getattr__()
例子是一个链式调用:

class Chain(object):

    def __init__(self, path=''):
        self._path = path

    def __getattr__(self, path):
        return Chain('%s/%s' % (self._path, path))

    def __str__(self):
        return self._path

    __repr__ = __str__
>>> Chain().status.user.timeline.list
'/status/user/timeline/list'

__call__

想要直接通过调用实例来调用方法,而不是instance.method()来调用,就需要定义__call__()方法。
例:

class Student(object):
    def __init__(self, name):
        self.name = name

    def __call__(self):
        print('My name is %s.' % self.name)

调用方式:

>>> s = Student('Michael')
>>> s() # self参数不要传入
My name is Michael.

这模糊了对象和函数的界限,因为这两者之间本来就没啥根本的区别

Callable()函数可以用来判断一个对象能否被调用:

>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('str')
False

小结

Python的class允许定义许多定制方法,可以让我们非常方便地生成特定的类。

使用枚举类

枚举类型定义一个class类型,,每个常量都是class的一个唯一实例。Python提供了Enum类来实现这个功能:

from enum import Enum

Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))

这样定义了Month类型的枚举类,可以直接使用Month.Jan来引用一个常量,或者枚举它的所有成员:

>>> for name, member in Month.__members__.items():
...     print(name, '=>', member, ',', member.value)
...
Jan => Month.Jan , 1
Feb => Month.Feb , 2
Mar => Month.Mar , 3
Apr => Month.Apr , 4
May => Month.May , 5
Jun => Month.Jun , 6
Jul => Month.Jul , 7
Aug => Month.Aug , 8
Sep => Month.Sep , 9
Oct => Month.Oct , 10
Nov => Month.Nov , 11
Dec => Month.Dec , 12

value属性则是自动赋给成员的int常量,默认从1开始计数。

如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:

from enum import Enum, unique

@unique
class Weekday(Enum):
    Sun = 0 # Sun的value被设定为0
    Mon = 1
    Tue = 2
    Wed = 3
    Thu = 4
    Fri = 5
    Sat = 6

@unique装饰器可以帮助我们检查保证没有重复值。

小结

Enum可以把一组相关常量定义在一个class中,且class不可变,而且成员可以直接比较。

使用元类

动态语言的中,函数和类的定义是在运行时动态创建的:

class Hello(object):
    def hello(self, name='world'):
        print('Hello, %s.' % name)

class的定义是运行时动态创建的,而创建class的方法就是使用type()函数:

>>> def fn(self, name='world'): # 先定义函数
...     print('Hello, %s.' % name)
...
>>> Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class
>>> h = Hello()
>>> h.hello()
Hello, world.

要创建一个class对象,type()函数依次传入3个参数:

  • class的名称;
  • 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
  • class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上。

metaclass

元类,先定义metaclass,就可以创建类,最后创建实例。
略复杂,没懂,一般也用不到。

错误、调试和测试

Python内置了一套异常处理机制,来帮助我们进行错误处理。

Python的pdb可以让我们以单步方式执行代码。跟踪程序的执行,查看变量的值是否正确,进行调试。

有了良好的测试,就可以在程序修改后反复运行,确保程序输出符合我们编写的测试。

错误处理

程序运行时出错,可以返回事先约定的错误代码。如操作系统调用中打开文件的函数open(),成功时返回文件描述符(就是一个整数),出错时返回-1。但有容易和函数原本的返回值混淆等诸多不便。

try

高级语言通常都内置了一套try…except…finally…的错误处理机制:

try:
    print('try...')
    r = 10 / int('2')
    print('result:', r)
except ValueError as e:
    print('ValueError:', e)
except ZeroDivisionError as e:
    print('ZeroDivisionError:', e)
else:
    print('no error!')
finally:
    print('finally...')
print('END')

try代码块内有错误则执行对应的except,再执行final;若没有错误,则执行else,再执行finalelsefinal可有可无

Python的错误其实也是class,所有的错误类型都继承自BaseException

使用try...except捕获错误还有一个巨大的好处,就是可以跨越多层调用。

调用堆栈

如果错误没有被捕获,它就会一直往上抛,最后被Python解释器捕获,打印一个错误信息,然后程序退出。

记录错误

既然我们能捕获错误,就可以把错误堆栈打印出来,然后分析错误原因,同时,让程序继续执行下。
Python内置的logging模块可以非常容易地记录错误信息:

# err_logging.py

import logging

def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')
    except Exception as e:
        logging.exception(e)

main()
print('END')

同样是出错,但程序打印完错误信息后会继续执行,并正常退出。
通过配置,logging还可以把错误记录到日志文件里,方便事后排查。

抛出错误

因为错误是class,捕获一个错误就是捕获到该class的一个实例。可以自定义一个错误,然后通过raise语句抛出错误实例。尽量使用Python内置的错误类型。

可以一边打印错误,一边抛出错误:

# err_reraise.py

def foo(s):
    n = int(s)
    if n==0:
        raise ValueError('invalid value: %s' % s)
    return 10 / n

def bar():
    try:
        foo('0')
    except ValueError as e:
        print('ValueError!')
        raise

bar()

捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理。

raise语句如果不带参数,就会把当前错误原样抛出。此外,在except中raise一个Error,还可以把一种类型的错误转化成另一种类型:

try:
    10 / 0
except ZeroDivisionError:
    raise ValueError('input error!')

小结

Python内置的try...except...finally用来处理错误十分方便。出错时,会分析错误信息并定位错误发生的代码位置才是最关键的。

程序也可以主动抛出错误,让调用者来处理相应的错误。但是,应该在文档中写清楚可能会抛出哪些错误,以及错误产生的原因。

调试

调试bug的方法有很多,比如直接把可能有问题的变量打印出来print(),但用print()最大的坏处是将来还得删掉它。

断言(assert)

凡是用print()来辅助查看的地方,都可以用断言(assert)来替代:

def foo(s):
    n = int(s)
    assert n != 0, 'n is zero!'
    return 10 / n

def main():
    foo('0')

assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。

如果断言失败,assert语句本身就会抛出AssertionError:

$ python3 err.py
Traceback (most recent call last):
  ...
AssertionError: n is zero!

启动Python解释器时可以用-O参数来关闭assert:

$ python3 -O err.py
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero

logging

logging不会抛出错误,而且可以输出到文件:

import logging

s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)

logging.info()就可以输出一段文本,可以指定记录信息的级别,有debuginfowarningerror等几个级别,debug最强,error最弱,指定了弱的,强的就无效了:

import logging
logging.basicConfig(level=logging.INFO)

pdg

启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态:

python3 -m pdb err.py

输入命令l可以查看代码,输入命令n可以单步执行代码,任何时候都可以输入命令p 变量名来查看变量,输入命令q结束调试,退出程序。

pdb.set_trace()

设置一个断点:

# err.py
import pdb

s = '0'
n = int(s)
pdb.set_trace() # 运行到这里会自动暂停
print(10 / n)

运行代码,程序会自动在pdb.set_trace()暂停并进入pdb调试环境,可以用命令p查看变量,或者用命令c继续运行。

IDE

目前比较好的Python IDE有PyCharm,另外,Eclipse加上pydev插件也可以调试Python程序

单元测试

单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。

如果单元测试通过,说明我们测试的这个函数能够正常工作。

编写单元测试,我们需要引入Python自带的unittest模块。

运行单元测试

最简单的运行方式是在测试文件的最后加上两行代码:

if __name__ == '__main__':
    unittest.main()

这样可以当做正常的python脚本运行。

另一种方法是在命令行通过参数-m unittest直接运行单元测试:

$ python3 -m unittest mydict_test

setUp与tearDown

可以在单元测试中编写两个特殊的setUp()和tearDown()方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。

小结

单元测试可以有效地测试某个程序模块的行为,是未来重构代码的信心保证。

单元测试的测试用例要覆盖常用的输入组合、边界条件和异常。

单元测试代码要非常简单,如果测试代码太复杂,那么测试代码本身就可能有bug。

单元测试通过了并不意味着程序就没有bug了,但是不通过程序肯定有bug。

文档测试

Python的官方文档,可以看到很多文档都有示例代码。Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。

小结

doctest非常有用,不但可以用来测试,还可以直接作为示例代码。通过某些文档生成工具,就可以自动把包含doctest的注释提取出来。用户看文档的时候,同时也看到了doctest。

IO编程

IO在计算机中指Input/Output,就是输入和输出。指的是数据从内存输出到磁盘或网络等外设,以及从磁盘网络等外设输入到内存,

同步IO:CPU等待io完成后再执行
异步IO:CPU不等待io,继续干别的的事,io完成后,在过来执行

文件读写

读写文件是最常见的IO操作。Python内置了读写文件的函数,用法和C是兼容的。

读文件

要以读文件的模式打开一个文件对象,使用Python内置的open()函数,传入文件名和标示符:

>>> f = open('/Users/michael/test.txt', 'r')

标示符 ‘r’表示读,如果文件不存在,open()函数就会抛出一个IOError的错误。

调用read()方法可以一次读取文件的全部内容,Python把内容读到内存,用一个str对象表示:

>>> f.read()
'Hello, world!'

最后一步是调用close()方法关闭文件。

文件读写时都有可能产生IOError,一旦出错,后面的f.close()就不会调用。所以,为了保证无论是否出错都能正确地关闭文件,可以使用try ... finally来实现:

try:
    f = open('/path/to/file', 'r')
    print(f.read())
finally:
    if f:
        f.close()

为了简便,Python提供with语句来调用close()的简便写法:

with open('/path/to/file', 'r') as f:
    print(f.read())

read()一次性读取文件的全部内容,文件过大就有隐患了,read(size)每次最多读取size个字节的内容,readline()每次读取一行,readlines()一次读取所有内容并按行返回list

file-like Object

open()函数返回的这种有个read()方法的对象,在Python中统称为file-like Object。。除了file外,还可以是内存的字节流,网络流,自定义流等等。file-like Object不要求从特定类继承,只要写个read()方法就行。

StringIO就是在内存中创建的file-like Object,常用作临时缓冲。

二进制文件

要读取二进制文件,比如图片、视频等等,用'rb'模式打开文件即可:

>>> f = open('/Users/michael/test.jpg', 'rb')
>>> f.read()
b'\xff\xd8\xff\xe1\x00\x18Exif\x00\x00...' # 十六进制表示的字节

字符编码

要读取非UTF-8编码的文本文件,需要给open()函数传入encoding参数,例如,读取GBK编码的文件:

>>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk')
>>> f.read()
'测试'

写文件

调用open()函数时,传入标识符’w’或者’wb’表示写文本文件或写二进制文件:

>>> f = open('/Users/michael/test.txt', 'w')
>>> f.write('Hello, world!')
>>> f.close()

同样close()函数必须调用,用with语句来保险:

with open('/Users/michael/test.txt', 'w') as f:
    f.write('Hello, world!')

要写入特定编码的文本文件,请给open()函数传入encoding参数,将字符串自动转换成指定编码。

小结

在Python中,文件读写是通过open()函数打开的文件对象完成的。使用with语句操作文件IO是个好习惯。

StringIO和BytesIO

StringIO

在内存中读写str。
把str写入StringIO:

>>> from io import StringIO
>>> f = StringIO()
>>> f.write('hello')
5
>>> f.write(' ')
1
>>> f.write('world!')
6
>>> print(f.getvalue())
hello world!

getvalue()方法用于获得写入后的str。

读取StringIO:

>>> from io import StringIO
>>> f = StringIO('Hello!\nHi!\nGoodbye!')
>>> while True:
...     s = f.readline()
...     if s == '':
...         break
...     print(s.strip())
...
Hello!
Hi!
Goodbye!

BytesIO

在内存中读写bytes。
写入bytes:

>>> from io import BytesIO
>>> f = BytesIO()
>>> f.write('中文'.encode('utf-8'))
6
>>> print(f.getvalue())
b'\xe4\xb8\xad\xe6\x96\x87'

读取bytes:

>>> from io import StringIO
>>> f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
>>> f.read()
b'\xe4\xb8\xad\xe6\x96\x87'

小结

StringIO和BytesIO是在内存中操作str和bytes的方法,使得和读写文件具有一致的接口。

操作文件和目录

Python内置的os模块可以直接调用操作系统提供的接口函数。
os.name变量:操作系统类型
os.uname()函数:获取详细的系统消息(windows无)

环境变量

os.environ变量:保存环境变量
os.environ.get('key'):获取某个环境变量的值

操作文件和目录

操作文件和目录的函数一部分放在os模块中,一部分放在os.path模块:

# 查看当前目录的绝对路径:
>>> os.path.abspath('.')
'/Users/michael'
# 在某个目录下创建一个新目录,首先把新目录的完整路径表示出来:
>>> os.path.join('/Users/michael', 'testdir')
'/Users/michael/testdir'
# 然后创建一个目录:
>>> os.mkdir('/Users/michael/testdir')
# 删掉一个目录:
>>> os.rmdir('/Users/michael/testdir')

os.path.split()函数:把一个路径拆分为两部分,后一部分总是最后级别的目录或文件名。
os.path.splitext()函数:直接得到文件扩展名。

文件操作:

# 对文件重命名:
>>> os.rename('test.txt', 'test.py')
# 删掉文件:
>>> os.remove('test.py')

复制文件:shutil模块提供了copyfile()的函数,你还可以在shutil模块中找到很多实用函数,它们可以看做是os模块的补充。

小结

Python的os模块封装了操作系统的目录和文件操作,要注意这些函数有的在os模块中,有的在os.path模块中。

序列化

序列化:把变量从内存中变成可存储或传输的过程,Python中叫pickling
反序列化:把变量内容从序列化的对象重新读到内存里,即unpickling

Python提供了pickle模块来实现序列化。

JSON

在不同的编程语言之间传递对象,就必须把对象序列化为标准格式,比如XML,但更好的方法是序列化为JSON,因为JSON表示出来就是一个字符串,可以被所有语言读取,也可以方便地存储到磁盘或者通过网络传输。JSON不仅是标准格式,并且比XML更快,而且可以直接在Web页面中读取,非常方便。

Python内置的json模块提供了非常完善的Python对象到JSON格式的转换。

JSON进阶

class表示的对象序列化,需要专门写一个转换函数,反序列化也需要写一个转换函数。

小结

Python语言特定的序列化模块是pickle,但如果要把序列化搞得更通用、更符合Web标准,就可以使用json模块。

json模块的dumps()loads()函数是定义得非常好的接口的典范。当我们使用时,只需要传入一个必须的参数。但是,当默认的序列化或反序列机制不满足我们的要求时,我们又可以传入更多的参数来定制序列化或反序列化的规则,既做到了接口简单易用,又做到了充分的扩展性和灵活性。

进程和线程

一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,进程内的这些“子任务”称为线程(Thread)

多任务的实现有3种方式:

  • 多进程模式;
  • 多线程模式;
  • 多进程+多线程模式。

小结

线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。

多进程和多线程的程序涉及到同步、数据共享的问题,编写起来更复杂。

多进程

Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。子进程永远返回0,而父进程返回子进程的ID。

multiprocessing

Windows没有fork调用,但Python是跨平台的,提供一个跨平台的多进程支持。multiprocessing模块就是跨平台版本的多进程模块。

multiprocessing模块提供了一个Process类来代表一个进程对象:

from multiprocessing import Process
import os

# 子进程要执行的代码
def run_proc(name):
    print('Run child process %s (%s)...' % (name, os.getpid()))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    p = Process(target=run_proc, args=('test',))
    print('Child process will start.')
    p.start()
    p.join()
    print('Child process end.')

创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。

Pool

要启动大量的子进程,可以用进程池的方式批量创建子进程:

from multiprocessing import Pool
import os, time, random

def long_time_task(name):
    print('Run task %s (%s)...' % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()
    print('Task %s runs %0.2f seconds.' % (name, (end - start)))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    p = Pool(4)
    for i in range(5):
        p.apply_async(long_time_task, args=(i,))
    print('Waiting for all subprocesses done...')
    p.close()
    p.join()
    print('All subprocesses done.')

代码解读:
Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。

p = Pool(4)

限制了同时只能跑4个进程。Pool的默认大小是CPU的核数。

子进程

subprocess模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。可以通过communicate()方法输入。

进程间通信

Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了QueuePipes等多种方式来交换数据。

小结

在Unix/Linux下,可以使用fork()调用实现多进程。

要实现跨平台的多进程,可以使用multiprocessing模块。

进程间通信是通过QueuePipes等实现的。

多线程

多任务可以由多进程完成,也可以由一个进程内的多线程完成。
Python的标准库提供了两个模块:_threadthreading_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:

import time, threading

# 新线程执行的代码:
def loop():
    print('thread %s is running...' % threading.current_thread().name)
    n = 0
    while n < 5:
        n = n + 1
        print('thread %s >>> %s' % (threading.current_thread().name, n))
        time.sleep(1)
    print('thread %s ended.' % threading.current_thread().name)

print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)

Lock

多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。

通过锁来对共享变量的访问进行控制,创建一个锁就是通过threading.Lock()来实现。

多核CPU

Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁。多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。

小结

多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。

Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。

ThreadLocal

在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。

但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦,

小结

一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。

进程 VS 线程

多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。
多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的。

多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。

线程切换

无论是多进程还是多线程,只要数量一多,效率肯定上不去。切换线程、进程时会有消耗。

计算密集型 vs. IO密集型

计算密集型的任务同时进行的数量不该超过CPU的核心数。最好用C语言编程而不是Python。

IO密集型任务,任务越多,CPU效率越高。Python这样的脚本语言是首选,C语言不是很适合。

异步IO

Python语言中,单进程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。

分布式进程

在Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。

Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信

小结

Python的分布式进程接口简单,封装良好,适合需要把繁重任务分布到多台机器的环境下。

注意Queue的作用是用来传递任务和接收结果,每个任务的描述数据量要尽量小。比如发送一个处理日志文件的任务,就不要发送几百兆的日志文件本身,而是发送日志文件存放的完整路径,由Worker进程再去共享的磁盘上读取文件。

正则表达式

正则表达式是一种用来匹配字符串的强有力的武器。它的设计思想是用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的。

在正则表达式中,如果直接给出字符,就是精确匹配。用\d可以匹配一个数字,\w可以匹配一个字母或数字,所以:

  • ‘00\d’可以匹配’007’,但无法匹配’00A’;

  • ‘\d\d\d’可以匹配’010’;

.可以匹配任意字符,所以:

  • ‘py.’可以匹配’pyc’、’pyo’、’py!’等等。

要匹配变长的字符,在正则表达式中,用*表示任意个字符(包括0个),用+表示至少一个字符,用?表示0个或1个字符,用{n}表示n个字符,用{n,m}表示n-m个字符:

进阶

要做更精确地匹配,可以用[]表示范围,比如:

  • [0-9a-zA-Z\_]可以匹配一个数字、字母或者下划线;

  • [0-9a-zA-Z\_]+可以匹配至少由一个数字、字母或者下划线组成的字符串,比如'a100''0_Z''Py3000'等等;

  • [a-zA-Z\_][0-9a-zA-Z\_]*可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串,也就是Python合法的变量;

  • [a-zA-Z\_][0-9a-zA-Z\_]{0, 19}更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)。

A|B可以匹配A或B,所以[P|p]ython可以匹配'Python'或者'python'

^表示行的开头,^\d表示必须以数字开头。

$表示行的结束,\d$表示必须以数字结束。

你可能注意到了,py也可以匹配'python',但是加上^py$就变成了整行匹配,就只能匹配'py'了。

re模块

Python提供re模块,包含所有正则表达式的功能。

由于Python的字符串本身也用\转义,所以强烈建议使用Python的r前缀,就不用考虑转义的问题了

判断正则表达式是否匹配:

>>> import re
>>> re.match(r'^\d{3}\-\d{3,8}$', '010-12345')
<_sre.SRE_Match object; span=(0, 9), match='010-12345'>
>>> re.match(r'^\d{3}\-\d{3,8}$', '010 12345')
>>>

match()方法判断是否匹配,如果匹配成功,返回一个Match对象,否则返回None

切分字符串

>>> re.split(r'\s+', 'a b   c')
['a', 'b', 'c']

分组

除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。

>>> m = re.match(r'^(\d{3})-(\d{3,8})$', '010-12345')
>>> m
<_sre.SRE_Match object; span=(0, 9), match='010-12345'>
>>> m.group(0)
'010-12345'
>>> m.group(1)
'010'
>>> m.group(2)
'12345'

贪婪匹配

正则匹配默认是贪婪匹配,在后面加个?就可以设置采取非贪婪匹配。

编译

当我们在Python中使用正则表达式时,re模块内部会干两件事情:

编译正则表达式,如果正则表达式的字符串本身不合法,会报错;

用编译后的正则表达式去匹配字符串。

如果一个正则表达式要重复使用几千次,出于效率的考虑,我们可以预编译该正则表达式,接下来重复使用时就不需要编译这个步骤了,直接匹配:

>>> import re
# 编译:
>>> re_telephone = re.compile(r'^(\d{3})-(\d{3,8})$')
# 使用:
>>> re_telephone.match('010-12345').groups()
('010', '12345')
>>> re_telephone.match('010-8086').groups()
('010', '8086')

小结

正则表达式非常强大,要在短短的一节里讲完是不可能的。要讲清楚正则的所有内容,可以写一本厚厚的书了。如果你经常遇到正则表达式的问题,你可能需要一本正则表达式的参考书。

Python常用函数

int():可以将字符串转化为整数,用法int(string)

float():可以将字符串转化为浮点数,用法float(string)

str():可以将整数、浮点数转化为字符串

sum():求和函数,sum(itrable,start),start非字符串,默认为0

isinstance():判断一个变量是否是某个类型

你可能感兴趣的:(Python)