python基础学习

python基础

数据类型和变量、字符串和编码、list和tuple、条件判断、循环、dict和set

函数

函数的参数

位置参数:def person(name,age):

默认参数(默认参数必须指向不变对象!如None)、fuc(a=0)

可变参数(*num):可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。例如 calc(1,2,3)

关键参数(name,age,**kw):关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict,例如 persion('Bob',35,city='Beijing')

命名关键字参数(name,age,*,city,job):如果要限制关键字参数的名字,就可以用命名关键字参数。*后面的参数被视为命名关键字参数。例如 person('Jack', 24, city='Beijing', job='Engineer')。如果参数中已经有了一个可变参数,后面跟着的命名关键字参数就不需要一个特殊的分隔符*了。如 def person(name, age, *args, city, job)

⚠️关键参数/命名关键字参数调用时必须给定相应的参数名

参数组合:在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。

⚠️对于任意函数,都可以通过类似func(*args, **kw)的形式调用它,无论它的参数是如何定义的。前者会自动分配参数给 位置参数 和 默认参数 以及 可变参数,后者可自动分配参数给 命名关键字参数 和 关键字参数。

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

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

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

递归函数

在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。

1
2
3
4
def fact(n):
if n==1:
return 1
return n * fact_iter(n-1)

⚠️使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。

解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。

例如:

1
2
3
4
5
6
7
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高级特性

本节包括多个python的高级特性介绍,这是与其他编程语言有明显区别的地方,主要包括slice、iteration、列表生成式、生成器、迭代器

切片(slice)

针对于list,如L = [i for i in range(10)],取最后一个元素 L[-1],取最后三个元素 L[-7:],每2个取一个 L[::2],使用L[:]可以复制一个list

针对于tuple,也可以同样操作。字符串也可以是一种list,用切片的方式去substring

迭代

对于dict,可以使用以下方法来对keys集合进行迭代

1
2
3
4
5
6
d = {'a':1,'b':2}
for i in d:
print(i)
...
a
b

若想对valu进行迭代,可以使用for value in d.values(),如果要同时对key和value进行迭代,可以使用for k,v in d.items() ,由于字符串也是可迭代对象,所以也可以用同样的方式来遍历字符串中所有的字符。

判断一个对象是否是可迭代对象:通过collections模块中的iterable类型判断:

1
2
3
from collections import Iterable
isinstance('abc',Iterable) #str是否可迭代,true
isinstance(23,Iterable) #整型是否可迭代,false

最后一个小问题,如果要对list实现类型Java中下标循环怎么办?python内置的enumerate函数可以把list变成索引-元素对,这就可以在for循环中同时迭代索引和元素本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
for i,value in enumerate(['A','B','C'])
print(i,value)
...
0 A
1 B
2 C
# 类似于上面这种引用两个变量,在python中很常见,如
for x,y in [(1,1),(2,2),(3,3)]:
print(x,y)
...
1 1
2 2
3 3

列表生成器

list(range(5))可以得到[0,1,2,3,4]

[x*x for x in range(5)]可以得到[0,1,4,9,16]

[x*x for x in range(5) if x%2!=0]可以得到[1,9]

[m + n for m in 'ABC' for n in 'XYZ']可以得到两层循环的全排列:['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']

1
2
3
4
5
#练习,如果list中既包含字符串,又包含整数,如何将list中所有的字符串全部转换为小写字母
from collections import Iterable
L = ['Hello','World',18,'Apple',None]
[s.lower() if isinstance(s,str) else s for s in L]
# [s.lower() for s in L if isinstance(s,str) else s]会报错,因为列表生成式后面只能有if,但是for前面的process(x)是可以使用if...else来写的

生成器(generator)

Motivation:通过列表生成式我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。

1
2
3
4
5
6
>>> 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
<generator object <genexpr> at 0x1022ef630>

创建Lg的区别仅在于最外层的[]()L是一个list,而g是一个generator。我们可以通过next(g)来打印generator中的每一个元素。

1
2
3
>>> g = (x * x for x in range(10))
>>> for n in g: # g也是可迭代对象
... print(n)

斐波拉契数列的实现用列表生成器无法实现,但是利用函数可以很容易打印出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def fib(num):
n,a,b=0,0,1
while n<num:
print(b)
a,b=b,a+b # 该赋值语句其实省略了中间临时变量
n += 1
return 'Done'
# fib函数其实就定义了斐波拉契数列的推算规则,其逻辑其实类似于generator,因此我们可以使用generator来实现fib这个函数:
def fib_generator(num):
n,a,b=0,0,1
while n<num:
yield b # 这是定义generator的另外一种方法,函数中包含yield关键字
a,b=b,a+b
n = n+1
return 'Done'
# 这个函数的执行过程并不是遇到return返回,而是遇到yield便中断返回,下一次继续从中断的下一位置执行。
# 由于fib_generator是一个生成器,所以我们在调用该函数时,得利用for ... in ... 的方式调用:
for n in fib_generator(6):
print(n)
# 但是该调用过程无法拿到函数return出来的结果,因为当生成器迭代完之后,会报StopIteration错误,而return的值包含在该异常的value中:
g = fib_generator(6)
while True:
try:
x = next(g)
print('g:',x)
except StopIteration as e:
print('Generator return value',e.value)
break
g:1
g:1
...
Generator return value:Done

迭代器(iterator)

我们已经知道,可以直接作用于for循环的数据类型有以下几种:

一类是集合数据类型,如listtupledictsetstr等;

一类是generator,包括生成器和带yield的generator function。

这些可直接作用于for循环对象统称为可迭代对象Iterable

可以使用from collections import Iterable,利用isinstance(object,Iterable)判断是否为可迭代对象。

而生成器(generator)不仅可以用于for循环,还可以使用next()函数不断调用并返回下一个值,直到最后抛出StopIteraction错误。可以被next()函数调用并不断返回下一个值的对象称为迭代器Iterator

可以使用from collections import Iterator,利用isinstance(object,Iterator)判断是否为迭代器对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> from collections import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('abc', Iterator)
False
# 生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。
# 把list、dict、str等Iterable变成Iterator可以使用iter()函数:
>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True

⚠️为什么listdictstr等数据类型不是Iterator

这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。

Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

函数式编程

本节主要包含高阶函数、返回函数、匿名函数、装饰器、偏函数。

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。

高阶函数

变量可以指向函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 以python内置求绝对值函数abs():
>>> abs(-10)
10
>>> abs
<built-in function abs>
>>> x = abs(-10)
>>> x
10
>> f = abs
>>> f
<built-in function abs>
# 如上面所示,f表示了abs这个函数,即函数本身也可以赋值给变量,即:变量可以指向函数。
>>> f = abs
>>> f(-10)
10

函数名就是变量:

1
2
3
4
5
6
7
8
# 数名其实就是指向函数的变量!对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数!
>>> abs = 10
>>> abs(-10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable
# ⚠️ 把abs指向10后,就无法通过abs(-10)调用该函数了!因为abs这个变量已经不指向求绝对值函数而是指向一个整数10!当然实际代码绝对不能这么写,这里是为了说明函数名也是变量。要恢复abs函数,请重启Python交互环境。
# 要想真的把所有模块中abs变量进行修改,注:由于abs函数实际上是定义在import builtins模块中的,所以要让修改abs变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10。

传入函数

1
2
3
4
# 既然变量可以指向函数,函数的参数又能接收变量,那么可以将一个函数当作参数传入另外一个函数中,这种接收函数为参数的函数为高阶函数。
def add(x, y, f):
return f(x) + f(y)
# 调用add(-5,6,abs),最终会返回11

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

map/reduce函数

map()函数接收两个参数,一个是函数,一个是Iterablemap将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。

1
2
3
4
5
6
>>> 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]

reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算。

1
2
3
4
5
6
>>> from functools import reduce
>>> def fn(x, y):
... return x * 10 + y
...
>>> reduce(fn, [1, 3, 5, 7, 9])
13579
1
2
3
4
5
6
7
8
9
10
from functools import reduce

DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}

def str2int(s):
def fn(x, y):
return x * 10 + y
def char2num(s):
return DIGITS[s]
return reduce(fn, map(char2num, s))

filter函数

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

1
2
3
4
5
def not_empty(s):
return s and s.strip()

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

注意到filter()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list。

思考:用filter求素数(埃式筛法)、筛选回数(回数是指从左向右读和从右向左读都是一样的数)

sorted

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]
# sorted()函数也是一个高阶函数,可以接收一个key函数来实现自定义的排序,如下按绝对值大小排序
>>> sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]
# 对字符串list进行排序,默认是按照字符的ASCII码进行排序,且大小写区分
>>> sorted(['bob', 'about', 'Zoo', 'Credit'])
['Credit', 'Zoo', 'about', 'bob']
# 如果要忽略大小写,我们可以传入一个函数作为参数,将每个字符串全部转换为小写
>>> sorted(['bob', 'about', 'Zoo', 'Credit'],key=str.lower)
['Credit', 'Zoo', 'about', 'bob']
# 如果想反向排序,直接传入第三个参数reverse
>>> sorted(['bob', 'about', 'Zoo', 'Credit'],key=str.lower,reverse=True)
['Credit', 'Zoo', 'about', 'bob']

练习:假设有一组tuple表示学习名字和成绩:

1
2
3
4
5
6
7
8
9
10
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
def by_name(t):
return t[0]
def by_score(t):
return t[1]
L1 = sorted(L,key=by_name) # 根据姓名字符排序
L2 = sorted(L,key=by_score) # 根据分数高低排序
print(L1)
print(L2)
# by_name,by_score这样定义的原因,因为sorted()中key所指向的方法会依次作用于L中的每一个元素,注意是依次作用,所以当以姓名字符排序时,我们只需要取每个元素的第一位,最后进行排序即可。以分数排序同理

返回函数

函数作为返回值

高阶函数除了可以接受函数作为参数以为,还可以吧函数作为结果值返回,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
def calc_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
# 当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数
>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>
>>> f()
25

在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。

闭包

在闭包结构中,注意到返回的函数在其定义内部引用了外部函数的局部变量args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用。

1
2
3
4
5
6
7
8
9
10
11
12
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs

f1, f2, f3 = count()
# 每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。
# 最后调用f1(),f2(),f3(),输出的结果都是9
# 原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9。

⚠️ 返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1,4):
fs.append(f(i))
return fs

f1, f2, f3 = count()
# 输出结果为1、4、9,这是因为count()返回的是f(i)的集合,而f(i)所返回的是相应的函数g(),
# 对于每个g()来说,都有一个唯一的i被传入调用g的f函数中,且已经保存下来,所以g中保留的局部变量i不会发生变化

匿名函数

关键字lambda表示匿名函数

lambda x:x*x,其中冒号前面的x表示参数,匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:

1
2
3
4
5
6
7
8
>>> f = lambda x: x * x
>>> f
<function <lambda> at 0x101c6ef28>
>>> f(5)
25
# 匿名函数也可以作为返回值
def build(x,y):
return lambda x+y

装饰器

在代码运行期间动态增加功能的方式(也可以理解为不更改原来的函数),称之为“装饰器”(Decorator)。

1
2
3
# 例如下面的函数,现需要增强now()的函数功能,例如需要在函数调用前后自动打印日志,但是不希望更改now()函数的定义。
def now():
print('2015-3-25')

使用装饰器来实现上述需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 这里我们就需要使用到decorator,其就是一个返回函数的高阶函数。
def log(func):
def wrapper(arg,**kw):
print('call %s' % func.name)
return func(arg,**kw) # 相当于调用now()
return wrapper

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

# 这里的log就是装饰器,接受一个函数作为参数,并返回一个函数,Python的@语法,把decorator置于函数的定义处:
# 当调用now()函数,其本质是执行了now = log(now),之前的now函数作为参数传入log()中,log中的wrapper作为返回函数给新的now变量,即此时的now指向wrapper方法,执行now()即执行wrapper。

如果需要给decorator传入参数,则参考之前返回函数中的用法,需要编写一个返回decorator的高阶函数。

1
2
3
4
5
6
7
8
9
10
11
12
def log(text):
def decorator(func):
def wrapper(*arg,**kw):
print('%s %s' %(test,func.__name__))
return func(*arg,**kw) # 相当于调用func()
return wrapper
return decorator

@log('excute')
def now():
print(time.time())
# 和上面两层嵌套的decorator相比,三层嵌套相当于 now = log('execute')(now),即先执行log('execute'),返回的是decorator函数,在调用该函数,参数是now函数,返回最终的wrapper函数

以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__等属性,但你去看经过decorator装饰之后的函数,它们的__name__已经从原来的'now'变成了'wrapper',因为返回的那个wrapper()函数名字就是'wrapper',所以,需要把原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。

不需要编写wrapper.__name__ = func.__name__这样的代码,Python内置的functools.wraps就是干这个事的,所以,一个完整的decorator的写法如下:

1
2
3
4
5
6
7
8
import functools

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

练习

设计一个装饰器(decorator),它可以作用于任何函数上,并打印该函数的执行时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import functools
import time
def exe_time(func):
@functools.wraps(func)
def wrapper(*arg,**kw):
start = time.time()
result = func(*arg,**kw)
end = time.time()
print('%s,%f' %(func.__name__,end-start))
return result
return wrapper
@exe_time
def fast(x,y):
time.sleep(0.1)
return x+y

偏函数

Python的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。要注意,这里的偏函数和数学意义上的偏函数不一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# int()函数可以把字符串转换为整数,当仅当传入字符串时,int()函数默认按字符串中数字为十进制表示进行转换
>>> int('12345')
12345
# 若更改int的默认参数base,定义base=8,则说明给定的字符串是以8进制给出的,则在进行转换的过程中,即将八进制转换为十进制
>>> int('12345', base=8)
5349
# 如果存在大量的二进制字符串,每次都要设置base=2比较麻烦,所以我们可以定义一下函数:
def int2(x,base=2):
return int(x,base)
# 其实functools模块中partial高阶函数可以帮我们直接创建偏函数,不需要我们自己去定义int2()
>>> import functools
>>> int2 = functools.partial(int,base=2)
>>> int2('1000000')
64
# 注意到partial其实也就是将参数base默认设置为2,我们也可以使用int2(int,base=10)来更改原来的方法。

# 最后,创建偏函数时,实际上可以接收函数对象、*args和**kw这3个参数,当传入:
int2 = functools.partial(int, base=2)
int2('10010')
# 实际上相当于
kw = {'base':2}
int('10010',**kw)
# 如果直接传入值:
max2 = functools.partial(max, 10) # 实际上会把10当作max(*args,**kw)中*arg中的一部分,
max2(5,6,7)
# 相当于
args = (10,5,6,7)
max(*arg)
# 输出为10

扩展

1
2
3
4
5
6
# 增加可变参数定义
# functools.partial的实现
def my_partial(func,*args,**kw):
def wrapper(*arg):
return func(*args+arg,**kw)
return wrapper

模块

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

使用模块的好处:1、最大的好处是大大提高了代码的可维护性。其次,编写代码不必从零开始。2、使用模块还可以避免函数名和变量名冲突。

为了避免模块名冲突,Python又引入了按目录来组织模块的方法,称为包(Package)。引入了包以后,只要顶层的包名不与别人冲突,那所有模块都不会与别人冲突。现在,abc.py模块的名字就变成了mycompany.abc,类似的,xyz.py的模块名变成了mycompany.xyz

⚠️每一个包目录下面都会有一个__init__.py的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。__init__.py可以是空文件,也可以有Python代码,因为__init__.py本身就是一个模块,而它的模块名就是mycompany

使用模块

以内建的sys模块为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/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()

导入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]

⚠️当我们在命令行运行hello模块文件时,Python解释器把一个特殊变量__name__置为__main__,而如果在其他地方导入该hello模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。如果import hello,则需要使用hello.test()才能调用test方法

关于__name__==__main__的作用:

__name__==__main__即让模块可导入到别的模块,也可以该模块自己执行测试。

当在module2中调用module1时,不会执行module1中的__name__==__main__,在python交互环境下,导入module2模块后使用,也不会执行module1中__name__==__main__的部分,只用来临时测试使用,即运行python3 module1时会执行。

作用域

在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用。在Python中,是通过_前缀来实现的。

类似__xxx__这样的变量是特殊变量,可以被直接引用,但是有特殊用途,例如__name__

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

1
2
3
4
5
6
7
8
9
10
11
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。

安装第三方模块

如果调用自己的模块显示默认路径下没有该模块,这是因为默认情况下,python解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在sys模块的path变量中。如果需要添加自己的搜索目录,有两种方法:

1
2
3
>>> import sys
>>> sys.path.append('/Users/michael/my_py_scripts')
# 在运行时修改sys中path,运行结束后失效

另外一种方法即设置环境变量PYTHONPATH,该环境变量的内容会被自动添加到模块搜索路径中。

面向对象编程(OOP)

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

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

类和实例

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

1
2
3
4
5
6
class Student(object):
pass
# object表示Student这个类是继承于object
bart = Student()
# 可以自由地给一个实例变量绑定属性,比如
bart.name = 'Bart Simpson'

在创建类的时候,可以把一些属性强制填写进去,通过一个特殊的__init__方法:

仍以Student类为例,在Python中,定义类是通过class关键字:

class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,继承的概念我们后面再讲,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。

定义好了Student类,就可以根据Student类创建出Student的实例,创建实例是通过类名+()实现的:

1
2
3
4
5
>>> bart = Student()
>>> bart
<__main__.Student object at 0x10a67a590>
>>> Student
<class '__main__.Student'>

可以看到,变量bart指向的就是一个Student的实例,后面的0x10a67a590是内存地址,每个object的地址都不一样,而Student本身则是一个类。

可以自由地给一个实例变量绑定属性,比如,给实例bart绑定一个name属性:

1
2
3
>>> bart.name = 'Bart Simpson'
>>> bart.name
'Bart Simpson'

由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法,在创建实例的时候,就把namescore等属性绑上去:

1
2
3
4
5
6
class Student(object):

def __init__(self, name, score):
self.name = name
self.score = score
# 注意到__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。

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

数据封装

面向对象编程的一个重要特点就是数据封装。在上面的Student类中,每个实例就拥有各自的namescore这些数据。我们可以通过函数来访问这些数据,比如打印一个学生的成绩:

1
2
3
4
5
6
>>> def print_score(std):
... print('%s: %s' % (std.name, std.score))
...
>>> print_score(bart)
Bart Simpson: 59
# 既然Student实例本身就拥有这些数据,要访问这些数据,就没有必要从外面的函数去访问,可以直接在Student类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。这些封装数据的函数是和Student类本身是关联起来的,我们称之为类的方法
1
2
3
4
5
6
7
8
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))

访问限制

在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。但是,从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的属性,此时将属性设置为私有(private)即可,即在变量前面加__

在Python中,变量名类似__xxx__的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用__name____score__这样的变量名。

⚠️Python中对类的私有变量保护措施:双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__name是因为Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量。

最后注意一个错误:

1
2
3
4
5
6
7
>>> bart = Student('Bart Simpson', 59)
>>> bart.get_name()
'Bart Simpson'
>>> bart.__name = 'New Name' # 设置__name变量!
>>> bart.__name
'New Name'
# 表面上看,外部代码“成功”地设置了__name变量,但实际上这个__name变量和class内部的__name变量不是一个变量!内部的__name变量已经被Python解释器自动改成了_Student__name,而外部代码给bart新增了一个__name变量。

——— 看到这里我觉得python是非常有趣的。

继承和多态

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

继承有什么好处?最大的好处是子类获得了父类的全部功能。由于Animial实现了run()方法,因此,DogCat作为它的子类,什么事也没干,就自动拥有了run()方法。继承的第二个好处需要我们对代码做一点改进。

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

理解多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a = list() # a是list类型
b = Animal() # b是Animal类型
c = Dog() # c是Dog类型

>>> isinstance(a, list)
True
>>> isinstance(b, Animal)
True
>>> isinstance(c, Dog)
True
# 注意
>>> isinstance(c, Animal)
True
# 看来c不仅仅是Dog,c还是Animal!

多态的具体好处:

1
2
3
4
5
6
# 我们来定义一个接受animal类型的变量
def run_twice(animal):
animal.run()
animal.run()
# 当我们传入一个dog类型的变量时,该方法也是可以执行的。
# 原因就在于dog是animal的子类,有父类的地方就可以使用子类,具体调用的方法是实际类中的实现方法

对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在AnimalDogCat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则

1、对扩展开放:允许新增Animal子类;2、对修改封闭:不需要依赖Animal类型的run_twice()等函数

静态语言 vs 动态语言

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

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

1
2
3
class Timer(object):
def run(self):
print('Start...')

这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。

Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个read()方法,返回其内容。但是,许多对象,只要有read()方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。

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

获取对象信息

1、使type()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> type(abs)
<class 'builtin_function_or_method'>
>>> type(a)
<class '__main__.Animal'>
# 判断基本数据类型可以直接写出`str`,`int`等
>>> type('abc')==str
True
>>> type('abc')==type(123)
False
>>> type(abs)==types.BuiltinFunctionType
True
>>> type(lambda x: x)==types.LambdaType
True
>>> type((x for x in range(10)))==types.GeneratorType
True

2、使用isinstance()

对于class的继承关系来说,使用type()很不方便,可以使用isinstance()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> a = Animal()
>>> d = Dog()
>>> h = Husky()
>>> isinstance(h, Husky)
True
>>> isinstance(d, Husky)
False
# 能用type()判断的基本类型也可以用isinstance()判断:
>>> isinstance(b'a', bytes)
True
# 并且还可以判断一个变量是否是某些类型中的一种
>>> isinstance([1, 2, 3], (list, tuple))
True
>>> isinstance((1, 2, 3), (list, tuple))
True
⚠️ # 总是优先使用isinstance()判断类型,可以将指定类型及其子类“一网打尽”。

3、使用dir()函数

要想获得一个对象的所有属性和方法,可以使用dir()函数:

1
2
3
4
5
6
7
>>> dir('ABC')
['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']
# 类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法
>>> len('ABC')
3
>>> 'ABC'.__len__()
3

仅仅把属性和方法列出来是不够的,配合getattr()setattr()以及hasattr(),我们可以直接操作一个对象的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> class MyObject(object):
... def __init__(self):
... self.x = 9
... def power(self):
... return self.x * self.x
...
>>> obj = MyObject()
>>> hasattr(obj, 'x') # 有属性'x'吗?
True
>>> obj.x
9
>>> hasattr(obj, 'y') # 有属性'y'吗?
False
# 可以传入一个default参数,如果属性不存在,就返回默认值:
>>> getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404
404

一个正确用法的例子:

1
2
3
4
5
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None
# 根据鸭子类型,先判断对象是否存在read()。存在则说明是一个流,可以返回有效的图像数据。

实例属性和类属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> 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

在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,调利用实例对象访问属性时,只会返回实力属性。但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。

面向对象高级编程

在Python中,面向对象还有很多高级特性,允许我们写出非常强大的功能。接下来会讨论多重继承、定制类、元类等概念。

使用__slot__

对于python这种动态语言,可以在创建类的实例后,给实例绑定一个属性、方法。但是这只对当前实例有用,对类创建的其他实例无效。

1
2
3
4
5
6
7
8
9
10
11
class Student(object):
pass
>>> s = Student()
>>> 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绑定方法:

1
2
3
4
5
>>> def set_score(self, score):
... self.score = score
...
>>> Student.set_score = set_score
# 上面的set_score方法可以直接定义在class中,但动态绑定允许我们在程序运行的过程中动态给class加上功能,这在静态语言中很难实现。

但是,如果我们想要限制实例的属性怎么办?比如,只允许对Student实例添加nameage属性。

1
2
3
4
5
6
7
8
9
class Student(object):
__slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称
>>> s = Student() # 创建新的实例
>>> s.name = 'Michael' # 绑定属性'name'
>>> s.age = 25 # 绑定属性'age'
>>> s.score = 99 # 绑定属性'score'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'

使用__slots__要注意,__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的。除非在子类中也定义__slots__,这样,子类实例允许定义的属性就是自身的__slots__加上父类的__slots__

使用@property

我们在定义类的时候,如果我们把属性直接暴露在外面:

1
2
s = Student()
s.score = 9999

但是这种方法无法检查属性值是否有效,所以我们常设置该属性为私有属性,定义相应的get和set方法来分别获取和设置该属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
# 这样的实现方法就可以检查属性参数
>>> a = Student()
>>> a.set_score(90)
>>> a.get_score()
90

但是,上面的方法在调用的时候又太过繁琐,设置一个属性需要调用其对应的方法。有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量呢?在之前我们学过decorator(装饰器),可以在不改变方法实现的情况下,给函数动态的添加功能。对于类的方法,装饰器一样起作用。Python内置的@property装饰器就是负责把一个方法变成属性调用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
>>>s.score
60
# 一旦注意到这个神奇的@property,我们在对实例属性操作的时候,就知道该属性很可能不是直接暴露的,而是通过getter和setter方法来实现的。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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

@property广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查

多重继承

对动物animal创建一个类,然后dog、bat等都可以继承于animal这个类。如果我们还考虑“能跑”这个功能,此时定义好Runable,对于需要该功能的动物,就多继承一个。例如:

1
2
class Dog(Mammal, Runnable):
pass

这就是多重继承,一个子类就可以同时获得多个父类的所有功能。

在设计类的继承关系时,通常,主线都是单一继承下来的。但是,如果需要“混入”额外的功能,通过多重继承就可以实现。这种设计通常称之为MixIn。由于python允许使用多重继承,因此,MixIn就是一种常见的设计,只允许单一继承的语言(如Java)不能使用MixIn的设计。

MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。Python自带了TCPServerUDPServer这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixInThreadingMixIn提供。通过组合,我们就可以创造出合适的服务来。

定制类

自定义__slots____str____iter____getitem____getattr____call__等方法

使用枚举类

1
2
3
from enum import Enum

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

使用元类

type()、定义metaclass

错误、调试和测试

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

错误处理

使用try…except…finally…

和其他高级语言一样,python也内置了一套try...except...finally...的错误处理机制。其中,finally后面的语句一定执行,如:

1
2
3
4
5
6
7
8
9
try:
print('try...')
r = 10 / 0
print('result:', r)
except ZeroDivisionError as e:
print('except:', e)
finally:
print('finally...')
print('END')

Python的错误其实也是class,所有的错误类型都继承自BaseException,所以在使用except时需要注意的是,它不但捕获该类型的错误,还把其子类也“一网打尽”。

使用try...except捕获错误还有一个巨大的好处,就是可以跨越多层调用,比如函数main()调用foo()foo()调用bar(),结果bar()出错了,这时,只要main()捕获到了,就可以处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
def foo(s):
return 10 / int(s)

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

def main():
try:
bar('0')
except Exception as e:
print('Error:', e)
finally:
print('finally...')

调用栈

在程序出错后,如果错误没有被捕获,它会一直往上抛,最后被python解释器捕获,打印一个错误信息,然后程序退出。如果不捕获错误,自然可以让Python解释器来打印出错误堆栈,但程序也被结束了。既然我们能捕获错误,就可以把错误堆栈打印出来,然后分析错误原因,同时,让程序继续执行下去。

Python内置的logging模块可以非常容易地记录错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 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')

# 运行上面的py文件,同样是出错,但是程序打印完错误信息后会继续执行,并正常退出
$ python3 err_logging.py
ERROR:root:division by zero
Traceback (most recent call last):
File "err_logging.py", line 13, in main
bar('0')
File "err_logging.py", line 9, in bar
return foo(s) * 2
File "err_logging.py", line 6, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
END

抛出错误

如果要抛出错误,首先根据需要,可以定义一个错误的class,选择好继承关系,然后,用raise语句抛出一个错误的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# err_raise.py
class FooError(ValueError):
pass

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

foo('0')

$ python3 err_raise.py
Traceback (most recent call last):
File "err_throw.py", line 11, in <module>
foo('0')
File "err_throw.py", line 8, in foo
raise FooError('invalid value: %s' % s)
__main__.FooError: invalid value: 0

只有在必要的时候才定义我们自己的错误类型。如果可以选择Python已有的内置的错误类型(比如ValueErrorTypeError),尽量使用Python内置的错误类型。

调试

1、直接print()把可能有问题的变量打印出来。

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

程序中如果到处充斥着assert,和print()相比也好不到哪去。不过,启动Python解释器时可以用-o参数来关闭assert,关闭后,你可以把所有的assert语句当成pass来看。

3、logging:该方法不会抛出错误,而且可以输出到文件

4、pdb:启动python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态;或者import pdb,在可能出错的地方放一个pdb.set_trace(),就可以设置一个断点,程序会自动在该位置暂停并进入pdb调试环境,可以用命令p查看变量,或者用命令c继续运行。

单元测试

“测试驱动开发(TDD:Test-Driven Development)”

文档测试

……

IO编程

由于cpu和内存的速度远远高于外设的速度,所以在IO编程中,就存在速度严重不匹配的问题。1、同步IO(cpu等待磁盘写操作,完成后继续执行后面的操作);2、异步IO(cpu不等待)

很明显,异步IO的程序性能要远远高于同步IO,但是异步IO的缺点是编程模型复杂。要判断IO操作何时执行完,需要采用回调模式(执行完之后主动到程序);或者是轮询模式(间断性的询问执行状态)。

文件读写

Python内置了读写文件的函数,用法和C是兼容的。

读写文件前,我们先必须了解一下,在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后,通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。

读文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> f = open('filename.txt'.txt', 'r')
# 'r'表示只读,如果文件不存在,open()函数会抛出一个IOError的错误,如果打开成功,则可以使用read()方法一次读取文件的全部内容,python把内容读到内存,用一个str对象表示:
>>> f.read()
'Hello, world!'
# 最后一步是调用close()方法关闭文件。
>>> f.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()会一次性读取文件的全部内容,如果文件有10G,内存就爆了,所以,要保险起见,可以反复调用read(size)方法,每次最多读取size个字节的内容。另外,调用readline()可以每次读取一行内容,调用readlines()一次读取所有内容并按行返回list。因此,要根据需要决定怎么调用。

file-like Object

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

二进制文件

之前默认都是读取文本文件,并且是UTF-8编码的文本文件。要读取二进制文件,比如图片、视频等等,用'rb'模式打开文件即可:

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

字符编码

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

1
2
3
4
5
>>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk')
>>> f.read()
'测试'
# 遇到有些编码不规范的文件,可能会遇到UnicodeDecodeError,这种情况可以给open()传入errors参数,表示遇到编码错误后的处理,最简单的方式是直接忽略:
>>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk', errors='ignore')

写文件

写文件和读文件是一样的,唯一区别是调用open()函数时,传入标识符'w'或者'wb'表示写文本文件或写二进制文件:

1
2
3
4
5
6
7
>>> f = open('/Users/michael/test.txt', 'w')
>>> f.write('Hello, world!')
>>> f.close()
# 可以反复调用write()来写入文件,但是务必要调用f.close()来关闭文件。当我们写文件时,操作系统往往不会立刻把数据写入磁盘,而是放到内存缓存起来,空闲的时候再慢慢写入。只有调用close()方法时,操作系统才保证把没有写入的数据全部写入磁盘。忘记调用close()的后果是数据可能只写了一部分到磁盘,剩下的丢失了。
with open('/Users/michael/test.txt', 'w') as f:
f.write('hello world')
# 要写入特定编码的文本文件,请给open()函数传入encoding参数,将字符串自动转换成指定编码。以'w'模式写入文件时,如果文件已存在,会直接覆盖(相当于删掉后新写入一个文件)。如果我们希望追加到文件末尾怎么办?可以传入'a'以追加(append)模式写入。

StringIO和BytesIO

StringIO是file-like object,其是在内存中读写str,要把str写入StringIO,我们需要先创建一个StringIO:

1
2
3
4
5
6
7
8
9
10
11
>>> from io import StringIO
>>> f = StringIO()
>>> f.write('hello')
5
>>> f.write(' ')
1
>>> f.write('world!')
6
>>> print(f.getvalue())
hellow world!
# getvalue()方法用于获得写入后的str。

要想读取StringIO,可以用一个str初始化StringIO,然后axing读文件一样读取:

1
2
3
4
5
6
7
8
9
10
11
>>> from io import StringIO
>>> f = StringIO('hello!\nhi!\nworld!')
>>> while True:
... s = f.readline()
... if s == '':
... break
... print(s.strip())
...
hello!
hi!
world!

StringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO。

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

操作文件和目录

Python内置的os模块也可以直接调用操作系统提供的接口函数。

1
2
3
4
5
6
7
>>> import os
>>> os.name # 操作系统类型
'posix'
>>> os.environ
.... # 操作系统中定义的环境变量
>>> os.environ.get('PATH')
... # 获取某个环境变量的值

操作系统和目录的函数一部分放在os模块中,一部分放在os.path模块中。

1
2
3
4
5
6
7
8
9
10
# 查看当前目录的绝对路径:
>>> 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.join()函数,这样可以正确处理不同操作系统的路径分隔符。同样的道理,要拆分路径时,也不要直接去拆字符串,而要通过os.path.split()函数。

对于文件操作,使用以下函数:

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

但是复制文件的函数在os中不存在。原因是复制文件并非由操作系统提供的系统调用。在shutil模块提供了copyfile()的函数,你还可以在shutil模块中找到很多实用函数,它们可以看做是os模块的补充。

1
2
3
>>> import os
>>> [x for x in os.path.listdir('.') if os.path.isdir(x)]
# 将当前目录下所有目录列出来

序列化

在程序运行的过程中,所有的变量都是在内存中,例如

1
d = dict(name='Bob',age=20,score=80)

可以随时修改变量,比如把name改成'Bill',但是一旦程序结束,变量所占用的内存就被操作系统全部回收。如果没有把修改后的'Bill'存储到磁盘上,下次重新运行程序,变量又被初始化为'Bob'

我们把变量从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,在其他语言中也被称之为serialization,marshalling,flattening等等,都是一个意思。

把变量内容从序列化的对象重新读到内存里称之为反序列化,即unpickling。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> import pickle
>>> d = dict(name='Bob',age=24,score=80)
>>> pickle.dumps(d)
b'\x80\x03}q\x00(X\x03\x00\x00\x00ageq\x01K\x14X\x05\x00\x00\x00scoreq\x02KXX\x04\x00\x00\x00nameq\x03X\x03\x00\x00\x00Bobq\x04u.'
# pickle.dumps()方法把任意对象序列化为一个bytes,然后就可以把这个bytes写入文件。或者可以用另外一个方法pickle.dump()直接把对象序列化后写入一个file-like object:
>>> f = open('test.txt','wb')
>>> pickle.dump(d,f)
>>> f.close()
# 对于反序列化,可以先把内容读到一个bytes,然后用pickle.loads()方法反序列化出对象;也可以用pickle.load()方法从一个file-like object中直接反序列化出对象。
>>> f = open('test.txt','rb')
>>> d = pickle.load(f)
>>> f.close()
>>> d
{'age':24,'score':80,'name':'Bob'}
# 或者使用pickle.loads()方法
>>> f = open('test.txt','rb')
>>> d = pickle.loads(f)
>>> d
{'age':24,'score':80,'name':'Bob'}

通过序列化和反序列化操作,可以在程序重新启动后,将变量内容重新加载,只是此时的变量和原来的变量完全不相干的对象,它们只是内容相同而已。

但是,pickle序列化存在的问题和别的编程语言特有的序列化问题一样,它只能作用于python,并且不同版本的python彼此都不兼容。因此,只能使用pickle保存不重要的数据,反序列化不成功也没关系。

JSON

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

1
2
3
4
5
6
7
8
>>> import json
>>> d = dict(name='Bob',age=20,score=80)
>>> json.dumps(d)
'{"age": 20, "score": 88, "name": "Bob"}'
# dumps()返回一个str,内容就是辨准的JSON,类似的,dump()方法可以直接把JSON写入一个file-like object。
>>> f = open('test','w')
>>> json.dump(d,f)
>>> f.close()

JSON反序列化为python对象,使用loads()或者load()方法,前者把JSON的字符串反序列化,后者从file-like object中读取字符串并反序列化:

1
2
3
4
5
6
7
8
9
>>> json_str = '{"age": 20, "score": 88, "name": "Bob"}'
>>> json.loads(json_str)
{'age':20,'score':80,'name':'Bob'}
### 或者使用json.load()方法
>>> f = open('test','w')
>>> d = json.load(line)
>>> f.close()
>>> d
{'age': 20, 'score': 88, 'name': 'Bob'}

python的dict可以直接序列化为JSON的{},但是,很多时候我们用class表示对象,直接使用dumps()无法将closs进行序列化。

1
2
3
4
5
# 需要先将student实例转换为dict,然后再序列化
def stu2dict(std):
return {'name':std.name,'age':std.age,'score':std.score}
>>> print(json.dumps(s,default=stu2dict))
'{"age": 20, "name": "Bob", "score": 88}'

但是,按照这样的方法,如果遇到一个其他class,照样无法序列化为JSON,所以,可以把任意class的实例变为dict:

1
2
3
>>> print(json.dumps(s,default=lambda obj:obj.__dict__))
# 因为class的实例都有一个__dict__属性,它就是一个dict,用来存储实例变量。
# 但是也有例外,比如定义了__slots__的class

同样道理,如果需要将JSON反序列化为一个student对象实例,loads()方法先转换为一个dict对象,然后,传入object_hook函数负责把dict转换为student实例:

1
2
3
4
5
def dict2stu(d):
return student(d['name'],d['age'],d['score'])
>>> json_str = '{"age": 20, "name": "Bob", "score": 88}'
>>> print(json.loads(json_str,object_hook=dict2stu))
<__main__.Student object at 0x10cd3c190> # 打印出反序列的student实例对象

进程和线程

现代操作系统如linux、windows、Mac OS等,都支持“多任务”。

所谓的多任务,就是操作系统可以同时运行多个任务。单核CPU轮流让各个任务交替执行来实现“多任务”同时执行。

真正的并行执行多任务只能在多核CPU上实现,由于任务数远远多于CPU的核心数量,所以,操作系统也会自动把多个任务轮流调度到每个核心执行。

对于操作系统来说,每一个任务就是一个进程(process),而进程中的“子任务”称为线程(thread)

前面编写的所有的Python程序,都是执行单任务的进程,也就是只有一个线程。如果我们要同时执行多个任务怎么办?

1、启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。

2、种方法是启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务。

3、启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。

多进程

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

多线程

ThreadLocal

进程 vs. 线程

分布式进程