0%

Python/廖雪峰Pyhton3教程/Pyhton3



基础

字符串和编码

字符编码

8个bit(比特)作为一个byte(字节),一个字节能表示的最大整数就是255(二进制11111111=255)

可变长编码的UTF-8

总结: 在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-编码。

用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件

浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器

ord()函数: 用于获取字符的整数表示,chr()把编码转换为对应的字符

Python中的字符类型是str,在内存中以Unicode表示,一个字符对应若干个字节。在网络上传输或者保存到磁盘上,就需要将str变为以字节为单位的bytes。

Python对bytes类型的数据用带b前缀的单引号或双引号表示。

以Unicode表示的str通过encode()方法可以编码为指定的bytes。

纯英文的str可以用ascii编码为bytes;含有中文的str可以用utf-8编码为bytes;含有中文的str无法用ascii编码。

要把bytes变为str,可以用到decode()方法。

len()函数计算的是str的字符数,如果换成bytes,len()函数就计算字节流书。

1个中文3个字节,一个英文字符只占用1个字节

当Python解释器读取源代码时,为了让它按UTF-8编码读取,通常在文件开头写上这两行

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

%x: 十六进制整数

使用%%来表示一个%

使用list和tuple

list

插入: insert(index, content)

删除:

  • pop —- 删除末尾的元素
  • pop(i)删除指定元素

元素替换: 直接用索引表示

list里面的元素的数据类型也可以不同

list元素也可以是另一个list

tuple: 元组

元组中除开appened(),insert()这样的方法,其他获取元素的方法和list是一样的。

元组不可变所以更安全,如果可能,尽量使用tuple代替list

特别注意:

1
2
3
# 像下面这样定义的不是元组而是一个整数
t = (1)
# 所以tuple中只有一个元素时,必须加上一个逗号, 用来消除歧义

特殊的tuple

image-20200723000207152

使用dict和set

dict

要避免key不存在的错误,有两种方法,一种是通过in判断key是否存在,一种是通过dict提供的get方法,如果key不存在,可以返回None,或者自己指定的value。

注意:返回None的时候Python的交互命令行不显示结果

删除一个key,使用pop(key)

dicet的缺点:需要占用大量的内存,内存浪费多

dict是用空间来换取时间的方法。

关于dict,需要牢记的一条就是dict的key必须是不可变对象

通过key计算位置的算法称为哈希算法

Python中:整数、字符串都是不可变的。而list是可变的

set

set也是一组key的集合,但是不存储value。set中,由于key不能重复,所以没有重复的key

创建一个set,需要提供一个list作为输入集合:

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

想set中添加元素使用add(key),可以重复添加,但是不会有效果。

使用remove(key)方法可以删除元素

注意: set也需要放入不可变的对象

再议不可变对象

image-20200723002519612

高级特性

切片

L[start: end] : 索引包括start但是不包括end

倒数的第一个元素是-1

高级玩法:

L[start:end :space ]

L[:]用于复制一个原样list

tuple仍然可以使用切片,但是因为tuple是不可变的,所以使用切片后得到的还是tuple

字符串也可以使用切片,使用之后得到的仍然是字符串

迭代

给定一个list或者tuple,使用for循环来遍历这个list或tuple,这种遍历我们称之为遍历(lteration)

Python中,迭代是通过for … in 来完成的,相比java,python的for循环抽象程度要更高,因为Python中的for循环不仅可以在list或tuple上,还可以作用在其他可迭代对象上。

只要是可迭代对象,不管它有没有下标,都可以使用迭代:

例如dict就可以迭代,但是dict的存储不是按照list的方式顺序排列,所以,迭代出的结果的顺序很可能不一样。默认情况下,dict迭代的是key,如果要迭代value,可以用for value in d.values(),如果同时迭代key和value,可以使用for k, v in d.items():

如何判断一个对象是可迭代对象呢? 可通过collections模块的lterable类型判断:

1
2
from collection.abc import Iterable 
isinstance("123", Iterable)

如何让list实现类似java那样的下标循环?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:

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

小结: 任何可迭代对象都可以作用于for循环,包括我们自定义的数据类型

列表生成式

列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。

生成器

在Python中,这种一边循环一边计算大的机制,称为生成器:generator

创建一个generator有很多种方法,第一种方法就是把一个列表生成式的[]改成()。

使用next()可以获得geneartor的下一个返回值,当没有更多元素时,抛出StopIteration的错误。

Generator 也是可迭代对象

定义generator的另一种方法:

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

1
2
3
4
5
6
7
8
9
# generator生成的另一种方法
def fib1(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n += 1

return "done"

较难理解的就是generator和函数的执行流程不一样。generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

使用for循环调用generator时,发现拿不到generator的return语句中的返回值。如果想要拿到返回值,必须捕获到StopIteration错误,返回值包含在StopIteration的value中

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
# Exercise 写出一个关于杨辉三角形的generator
def yH(max):
list9 = []
list8 = []
index = 1
while index < max:
if index == 1:
list8 = [1]
yield list8
index = index + 1
elif index == 2:
list8 = [1, 1]
yield list8
index = index + 1
else:
list9 = [None] * index
for I in range(1, index - 1):
list9[I] = list8[I - 1] + list8[I]
list9[0] = 1
list9[index - 1] = 1
yield list9
index += 1
list8 = list9


g1 = yH(11)
for n in g1:
print(n)

迭代器

可以直接作用于for循环的对象统称为可迭代对象: Iterable

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

可以使用isinstance()判断一个对象是不是Iterator对象

生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。

Python的Iterator对象是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。

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

Iterator甚至可以表示一个无限大的数据流,例如全体自然数。

image-20200724141020012

函数式编程 - Functional Programming

通过把大段代码拆成函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元

理解计算机和计算的概念

在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是最接近计算机的语言。

而计算则指的是数学意义上的计算,越是抽象的计算,离计算机硬件越远

对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言。

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

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

Python不是纯函数式编程语言。

高阶函数

高阶函数英文名: High-order function

abs(-10)是函数调用,abs是函数本身

1
2
3
4
5
6
7
f = abs
print(f(-10))

"""
输出:
10
"""

函数本身也可以赋值给变量,即:变量可以指向函数

传入函数

一个函数可以接受另一个函数作为参数,这种函数就称之为高阶函数

一个最简单的高阶函数:

1
2
def add(x, y, f):
return f(x) + f(y)

高阶函数中作为参数的函数只需要函数名,不需要后面的括号

map/reduce

map

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

map()作为高阶函数,事实上它把运算规则抽象了。

下面将一个list中的所有数字转为字符串:

1
list(map(str, [1, 2, 3, 4, 5, 6, 7, 9]))

reduce

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 关于reduce的用法
# 实现累加
def add(x, y):
return x + y

redce_detail = reduce(add, [1, 2, 3, 4, 5, 6, 7, 8, 9])
print(redce_detail)

# 实现将一个序列中的所有数字变成一个整数
def changeToInteger(x, y):
return x * 10 + y
list_for_str = [1, 2, 3, 4, 5, 6, 7]
integer = reduce(changeToInteger, list_for_str)
print(integer)

# 使用reduce和map()函数,写出将str转换为int的函数:
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]
# 上一行末尾的[s]是什么意思?(大概意思我是了解了)
str2int = reduce(fn, map(char2num, "13579"))
print(str2int)

filter

Python内建的filter()函数用于过滤序列

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

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

用filter求素数:但是没有怎么看懂

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
# 用filter求素数 -- 没有怎么看懂
# 首先定义一个从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) # 构造新序列

# 输出结果:
print("1000以内的所有素数: ")
for n in primes():
if n< 1000:
print(n)
else:
break

匿名函数

Python中对匿名函数提供了有限支持

1
2
lambda x : x * x
# 上述匿名函数中,第一个x表示函数参数

匿名函数有一个限制,就是只能有一个表达式,不用谢retrun,返回值就是该表达式的结果

匿名函数没有名字,所以不用担心函数名冲突。匿名函数也是一个对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数;

也可以把匿名函数作为一个返回值返回。

sorted – 排序算法

排序的核心是比较两个元素的大小

对于字符串或者两个dict如何比较呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。

list排序

Python内置的sorted()函数就可以对list进行排序

sorted()是一个高阶函数,它可以接受一个key函数来实现自定义的排序:

key函数使用方法: key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。

字符串排序

默认情况下,对字符串进行排序,是按照ASCII的大小进行比较的。

如何实现排序时忽略大小写,按照字母序排序?实现这个算法,只需要将所有字符串都改成大写或者小写

如果要进行反向排序,不必改动key函数,只需要传入第三个参数: reverse=True

下面的练习题虽然我做出来了,但是我还是很蒙,有时间记得复习复习:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 请用sorted()对上述列表分别按名字排序
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]

def by_name(t):

return t[0]

L2 = sorted(L, key=by_name)
print(L2)
# 请用sorted()对上述列表分别按成绩高低排序
def by_score(t):
return t[1]

L3 = sorted(L, key=by_score)
print(L3)

返回函数

函数作为返回值

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

对于一个求和函数,如果不需要立刻求和,而是在后面的代码中,根据需要再计算。这时就可以不返回求和的结果,而是返回求和的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'''
对于一个求和函数,如果不需要立刻求和,而是在后面的代码中,根据需要再计算。这时就可以不返回求和的结果,而是返回求和的函数
'''
def lazy_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)
print(type(f))
print(f())

注意 :返回一个函数时,不需要加上函数名后面的括号

何为闭包?

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

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

1
2
3
4
5
6
f1 = lazy_sum(1, 3, 5, 7, 9)
f2 = lazy_sum(1, 3, 5, 7, 9)
print(f1 == f2)
'''输出结果
False
'''

f1()f2()的调用结果互不影响。

闭包–Closure

需要注意的是:返回的函数并没有立刻执行,而是直到它被调用之后才执行

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

如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论循环变量后续如何更改,已绑定到函数参数的值不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 在闭包中使用循环变量
def count():
def f(j):
def g():
return j * j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
return fs


f1, f2, f3 = count()
print(f1(), '', f2(), '', f3()

装饰器

函数对象有一个name属性,可以拿到函数的名字:

什么是装饰器

在代码运行期间动态增加功能的方式,称之为”装饰器”(Decorator)

本质上,decoator就是一个返回函数的高阶函数。

总感觉装饰器decorator有点难

小结

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

*这一小节的练习题答案参见大神做法: *

https://blog.csdn.net/GBA_Eagle/article/details/80764749

偏函数– Partial function

对于函数,通过设定参数的默认值,可以降低函数调用的难度。偏函数也可以做到这点

int()函数

int()函数可以把字符串转换为整数,默认为十进制转换

int()函数中有个base参数,默认为10,修改base可以让int()函数按照你希望的进制进行转换

转换大量二进制字符串的时候,每次都传入int(x, base=2)非常麻烦,这时候,可以自定义一个int2()函数,默认把base=2传进去,像下面这样:

1
2
def int2(x, base=2):
return int(x, base)

functools.partial就是帮助我们创建一个偏函数的,不需要自定义int2(),可以直接使用下面的代码创建一个新的函数int2():

1
2
3
import functools
int2 = functools.partial(int, base=2)
int2('1000000')

functools.partial的作用就是,把一个函数的某些参数固定住,返回一个新的函数

小结:

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

模块

sys模块

任何模块的第一个字符串,都被视为模块的文档注释

使用author变量可以添加作者姓名

1
__author__ = "YourName"

sys模块有一个argv变量,用list存储了命令行的所有参数。argv至少有一个元素,第一个元素永远是该.py文件的文件名

在一个模块中,我们可能会定义很多函数和变量。对于其中一些函数和变量,我们是想用来给别人使用的,对于另外一些函数和变量,我们只想在模块内部使用。实现上述目标是通过_前缀来实现的。

正常的函数和变量名是公开的(public),可以直接被使用

下面的一段话没有读太懂:
类似**xxx**这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的**author****name**就是特殊变量,hello模块定义的文档注释也可以用特殊变量**doc**访问,我们自己的变量一般不要用这种变量名;

类似_abc这样的函数或变量就是非公开的(private),不应该被直接使用。

需要注意的是: private函数和变量是不应该被直接引用,而不是“不能”被直接引用,是因为Python并没有一种方法可以完全限制访问private函数和变量,但是,从编程习惯上不应该引用private函数或变量

小结

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

安装第三方模块

安装使用pip或者pip3

模块搜索路径

加载一个模块的时候,Python会在指定的路径下搜索对应的.py文件,如果找不到,就会报错。

默认情况下,搜索路径会存放在sys模块的path变量中

如何添加自己的搜索路径,有两种方法:

  1. 直接修改sys.path,在其中添加要搜索的目录

    1
    2
    import sys 
    sys.path.append('') # 在引号中添加要搜索的目录

    这种方法是在运行时修改,运行结束后失效

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

类和实例

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

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

1
2
3
# 给实例bart绑定一个name属性:
bart.name = 'Bart Simpson'
print(bart.name)

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

下面的需要重点理解一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的init方法,可以在创建实例的时候,
# 就把name,score等属性绑上去
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score


class Student1(object):
def __init__(self):
self.name = 'bowenkei'
self.score = 100

no1 = Student("chenqiaochu", 100)
no2 = Student1()
print("--------------------------")
print(str(no1.score) + " " + no1.name )
print(str(no2.score) + " " + no2.name )
# 注意到init方法的第一个参数永远是self,表示创建的实例本身,因此,在init方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身

self不需要传,Python解释器自己会把实例变量传进去:

和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。

数据封装

我们可以直接在Student类的内部定义访问数据的函数,这样,就把“数据”封装起来了。这些封装数据的函数是和Student类本身关联起来的,我们称之为类的方法

访问限制

外部代码还是可以自由地修改一个实例的属性。

如果想让内部属性不被外部访问,可以把属性的名字前加上两个下划线,在Python中,实例的变量名如果以双下划线开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问

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

对于前面只有一个下划线的变量,是可以在外部访问到的。但是,按照约定俗成的规定,当你看到这样的变量时,意思是说,“虽然我可以被访问,但是请把我当作私有变量,不要随意访问我”

双下划线开头的实例变量其实也是可以访问到的,因为Python其实并没有严格的private变量。之所以不能访问以双下划线开头的私有变量,是因为Python解释器把

1
# __name变量变成了_Student__name

总的来说就是: Python本身并没有任何机制阻止你干坏事,全凭自觉

继承和多态

继承

当子类和父类有相同的方法的时候,子类的方法会覆盖父类的方法。当运行子类的实例的时候,总是会调用子类的方法

多态

创建一个class的时候实际上是创建了一种数据类型。

判断某个变量是否是某种类型,可以使用isinstance()判断

在继承关系中,如果一个实例的数据类型是某个子类,那么它的数据类型也可以被看做是父类。但是反过来就不行

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

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

静态语言 VS 动态语言

image-20200731184112361

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

获取对象信息

使用type()

使用isinstance

对于class的继承关系,使用isinstance()函数

isinstance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上

能用type()判断的基本类型也可以用isinstance()判断

还可以用isinstance判断一个变量是否是某些类型中的一种

1
2
3
4
5
# 使用isinstance()判断一个变量是否是某些类型中的一个
# 判断某个变量是否是list或者tuple:
print(isinstance([1, 2, 3, 4, 5, 6], (list, tuple)))
print(isinstance((1, 2, 3, 4, 5, 6), (list, tuple)))
print(isinstance('hello', (list, tuple)))

使用dir()

dir()用于获取一个对象的所有属性和方法。它返回一个包含字符串的list

1
2
# 获得一个str对象的所有属性和方法
print(dir('str'))
1
2
3
# __len__和len()是等价的,使用后者,它会自动去调用该对象的__len__()
print(len('abc'))
print('abc'.__len__())

使用getattr(), setattr(),hasattr()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 使用getattr(),setattr(),hasattr(),我们可以直接操作一个对象的状态
class MyObject(object):
def __init__(self):
self.x = 9
def power(self):
return self.x * self.x


obj = MyObject()
# 测试上面新创建的对象的属性
print(hasattr(obj, 'x'))
print(obj.x)
print(hasattr(obj, 'y'))
setattr(obj, 'y', 19)
print(hasattr(obj, 'y'))
print(getattr(obj, 'y'))
print(obj.y)

如果获取不存在的属性,会抛出AttributeError(属性)的错误:

使用getattr()时,可以传入一个default参数,如果属性不存在,就返回默认值

1
2
# 使用getattr()时,可以传入一个default参数,如果属性不存在,就返回默认值
print(getattr(obj, 'z', '404: No such a attribute'))

小结

如果知道一个对象的属性和方法,就直接使用,不要使用getattr(),setattr()等等

实例属性和类属性

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

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

类属性:类本身需要绑定的属性,可以直接在class中定义,这种属性是类属性,归该class所有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 实例属性和类属性
# 给实例绑定属性
class Student(object):
def __init__(self, name):
self.name = name

s = Student('Bob')
s.score = 19
# 给类添加类属性
class Student1(object):
name = 'Student'
# 上面的name就是类属性。类属性虽然归类所有,但是该类的所有实例都可以访问到
s = Student1()
print(s.name)
print(Student1.name)
s.name = "Michael"
# 因为实例属性的优先级比类属性高,所以他会屏蔽掉类的name属性
print(s.name)
# s有了实例属性之后,Student的name类属性还在且不会消失
# 在删除掉s的实例属性之后,name类属性就会显示出来
del s.name
print(s.name)

注意:

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

面向对象高级编程

使用slots

给实例绑定一个方法:

1
2
3
4
5
6
7
# 定义一个函数作为实例方法
def set_age(self, age):
self.age = age
# 给实例绑定一个方法
s.set_age = types.MethodType(set_age, s)
s.set_age(20)
print(s.age)

但是给一个实例绑定的方法,对于另一个实例是不起作用的

想要给所有实例都绑定方法,可以给class绑定方法:

1
2
3
4
5
6
7
8
9
10
def set_score(self, score):
self.score = score


Student1.set_score = types.MethodType(set_score, Student1)
s.set_score(100)
print(s.score)
s2 = Student1()
s2.set_score(10000)
print(s2.score)

使用slots

如何才能限制实例的属性,比如,只允许对Student实例添加name和age属性。

为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的slots变量,用来限制该class实例能添加的属性

*使用slots时要注意: *

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

多重继承

类似下面这样就实现了多重继承

1
2
3
class Bat(Mammal, Flyable):
pass
# Bat类继承了Mammal和Flyable类

Mixln

为了让一个类实现更多的类,从而让它再多继承一个类,这种设计通常被称为Mixln

Mixln的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个Mixln的功能,而不是设计多层次的复杂的继承关系

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

定制类

Python中有很多特殊的变量或函数都有着特殊的用途,可以帮助我们定制类

str()和repr()

1
2
3
4
5
6
# __str__()和__repr__()用于返回有关类和实例的字符串,其中前者返回用户看到的字符串,后者返回程序开发者看到的字符串,后者是为调试服务的
# 使用下面的方法可以让str()和repr()统一
__str__() = __repr__()
# 直接使用变量时调用的是__repr__(),使用print调用的是__str__
# 如何才能调用__str__方法?使用:
print(Class("Attribute"))

iter

如果一个类想被用于for…in循环,类似list或tuple那样,就必须实现个iter()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的next()方法拿到循环的下一个值,知道遇到StopIteration错误时间退出循环

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

# Test
for n in Fib():
print(n)

getitem

想要FIb实例可以实现按索引取值必须实现getitem方法:

1
2
3
4
5
6
7
8
9
10
# getitem
def __getitem__(self, n):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
Fib.__getitem__ = types.MethodType(__getitem__, Fib)
# 现在可以按下标访问数列的任意一项了
f = Fib()
print(f[0])

让Fib实现切片操作

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
# 让Fib实现切片
# 删除原来的__getitem__方法
del Fib.__getitem__
def __getitem__(self, n):
if isinstance(n, int):
a, b = 1, 1
for x in n:
a, b = b, a + b
return a
if isinstance(n, slice):
start = n.start
stop = n.stop
if start is None:
start = 0
a, b = 1, 1
L = []
for x in range(stop):
if x >= start:
L.append(a)
a, b = b, a + b
return L
# 把新的__getitem__添加到Fib class中去
Fib.__getitem__ = types.MethodType(__getitem__, Fib)
# 现在可以尝试Fib的切片了
f = Fib()
print(f[0:5])

经过上面的操作之后,Fib还没有实现list的所有功能,例如对step参数还没有处理,也没有对含有负数的slice进行处理。要正确实现一个getitem()还是有很多工作要做的

如果想把对象看成一个dict,getitem()的参数也可能是一个可以作key的object,例如str。

与getitem类似的还有setitem以及delitem()方法

getattr

使用getattr()方法可以动态返回一个属性: 即当调用一个不存在的属性时,比如score,Python解释器会试图调用getattr(self, ‘score’)来尝试获得属性

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# __getattr__()
class my_getattr(object):
def __init__(self, name):
self.name = name

def __getattr__(self, item):
if item == 'score':
return 99

one_getattr = my_getattr('Bowenkei')
print(one_getattr.name)
print(one_getattr.score)
# 使用__getattr__()返回函数也是可以的
del my_getattr.__getattr__
def __getattr__(self, attr):
if attr == 'money':
return lambda: '这是我的存款:∞'
my_getattr.__getattr__ = types.MethodType(__getattr__, my_getattr)

two_getattr = my_getattr("bowenkei")
print(two_getattr.money)
print(two_getattr.money())
# 注意,只有在没有找到属性的情况下,才调用__getattr__(),对于已经存在的属性,不会在__getattr__中查找
print(two_getattr.today) # None
# 如上述任意调用都会返回None,因为__getattr__()的默认返回值就是None,但是如果类中没有__getattr__()
# 方法的时候,调用class中没有的熟悉则都会抛出AttributeError。要让class只响应特定的几个属性,我们就要按照约定,
# 抛出AttributeError的错误
del my_getattr.__getattr__
def __getattr__(self, attr):
if attr == 'age':
return lambda :20
raise AttributeError("\'my_getattr\'object has no attribute \'%s\'" % attr)

my_getattr.__getattr__ = types.MethodType(__getattr__, my_getattr)
three_my_getattr = my_getattr("bowenkei")
print(three_my_getattr.age())
# 由此可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。
# 这种完全动态调用的特性有什么实际作用呢?作用就是,可以针对完全动态的情况做调用
# 如果要写SDK,给每个URL对应的API都写一个方法,那太麻烦,而且API一旦改动,SDK也要改
# 利用完全动态的__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__

# Demo
print(Chain('bowenkei').status.user.timeline.list)
print(Chain('bowenkei').status)
print(Chain('bowenkei').status.user)
print(Chain('bowenkei').status.user.timeline)
print(Chain('bowenkei').status.user.timeline.list)
"""output:
bowenkei/status/user/timeline/list
bowenkei/status
bowenkei/status/user
bowenkei/status/user/timeline
bowenkei/status/user/timeline/list
"""

###call

一个对象实例有自己的属性和实例方法,调用实例方法时,用instance.method()来调用。

在Python中,可以直接在实例本身上调用实例方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# call
# 在Python中可以直接在实例本身上调用实例方法
# 任何类,只要实现一个call()方法就可以直接对实例进行调用
class Call_student(object):
def __init__(self, name):
self.name = name

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

call_s = Call_student('bowenkei')
call_s()
"""
call()还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。

如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。
"""
# 如何判断一个变量是对象还是函数?
# 能被调用的对象就是一个Callable对象
# 函数和带有call()的类实例都是Callable对象
# 使用callable()
print(callable(Call_student))

小结

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

Python官方文档地址:https://docs.python.org/3/reference/datamodel.html#special-method-names

使用枚举类

使用枚举类

定义常量时,更好的方法是为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了Enum类来实现这个功能:

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
# 使用枚举类
from enum import Enum, unique
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))

# 输出枚举成员
print(Month.Jan.value)
for name, member in Month.__members__.items():
print(name, '=>', member, ',', member.value)
# value属性是自动赋给成员的int常量,默认从1开始计数
# 从Enum中派生出自定义类
@unique
class Weekday(Enum):
Sun = 0 #Sun的value被设定为0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6

# @unique是一个装饰器,可以帮助我们检查有没有重复值
# 访问枚举类型可以有若干种方法
print(Weekday.Mon)
print(Weekday['Sat'])
print(Weekday(1))
# 即既可以用成员名称引用枚举常量,又可以直接根据value的值获得枚举常量

错误、调试和测试

错误、调试和测试

运行一个程序总会遇到各种各样的错误:

程序编写造成的问题叫做bug,bug是必须修复的

用户输入造成的错误可以通过检查用户的输入来做相应的处理

另一类错误是完全无法在程序运行过程中预测的。这种错误也称为异常,例如:写入文件的时候,磁盘满了,写不进去了。

Python内置有一套异常处理机制。

我们也需要跟踪程序的执行,查看变量的值是否正确,这个过程称为调试。Python的pdb可以让我们以单步方式执行代码。

错误处理

在程序运行的过程中,如果发生了错误,可以事先约定返回一个错误代码,这样,就知道是否有错。

但是用错误码来表示是否出错十分不便,更好的方法是使用try-except-finally的错误处理机制

当我们认为某些代码可能会出错时,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕。

注意:如果有finally语句,则finally语句则一定会被执行,但是可以没有finally语句

还可以在except语句后面加一个else,当没有错误发生时,会自动执行else语句

Python的错误也是class,所有的错误都继承自BaseExceptioon

常见的错误类型和继承关系:

https://docs.python.org/3/library/exceptions.html#exception-hierarchy

Try-except-finally可以跨越多层调用,所以在使用时,只需要在合适的层次去捕获错误就可以了。

###调用堆栈

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

###记录错误

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

1
2
3
4
5
6
7
import logging
try:
do something
except Exception as e:
logging.exception(e)

do someting

抛出错误

错误是class,捕获一个错误就是捕获到该class的一个实例。

错误并不是凭空产生的,而是有意创建并抛出的。Python内置的函数或抛出很多类型的错误,我们自己编写的函数也可以抛出错误。

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

1
2
3
4
5
6
7
8
9
10
11
class FootError(ValueError):
pass

def foo(s):
n = int(s)
if n == 0:
raise FootError("invalid value: %s " %s)

return 10 / n

foo('0')

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

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

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

调试

调试

程序能一次写完并且正常运行的概率很小,基本不超过1%

我们需要知道,出措时,哪些变量的值是正确的,哪些变量的值是错误的。

第一种方法: 使用print()把可能有问题的变量打印出来

第二种方法: 断言

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 断言(assert)
def foo(s):
n = int(s)
assert n != 0, '0 is zero!'
return 10 / n

def main():
foo('0')

main()

# assert的意思是,表达式n!=0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。如果断言失败,
# assert语句本身就会抛出AssertionError
# 启动python解释器时可以用 -0 参数(python3 -0 python_file_name)来关闭assert,关闭后,所有的assert语句可以当成pass

第三种方法: logging

把print()换成logging是第三种方法,和assert相比,logging不会抛出任何错误,而且可以输出到文本

1
2
3
4
5
6
7
8
9
10
11
12
# logging
import logging
logging.basicConfig(level=logging.INFO)
s = '0'
n = int(s)
logging.info("n = %d" % n)
print(10 / n)
'''
# info是通告,信息的意思
# logging的好处就是,它允许你使用不同的参数来区分记录信息的级别,可选的参数有debug, info, waring, error。
# 当我们选定其中info的时候哦,logging.debug就不起作用了,这样就可以输出不同级别的信息,也不用删除。
# 使用logging的另外一个好处是,通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件

*注意: *

1
这就是logging的好处,它允许你指定记录信息的级别,有debug,info,warning,error等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debug和info就不起作用了。(备注: 使用Debug的时候,logging.info以及logging.debug都是会运行的)

第四种方法: pdb

pdb是Python的调试器,它可以让程序以单步方式运行。

使用方法:

  1. 以参数 -m pdb启动后,pdb自动定位到下一步将要执行的代码
  2. 使用命令参数 l来查看代码
  3. 使用命令参数 n 可以单步执行代码
  4. 在任何时候都可以输入命令参数 p 变量名来查看变量
  5. 输入命令参数 q结束调试

第五种方法:pdb.set_trace()

这个方法也是使用pdb,但是不需要单步执行,我们只需要import pdb,然后在可能出错的地方放一个pdb.set_trace(),就可以设置一个断点。

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

1
2
3
4
5
import pdb
for i in range(10):
pdb.set_trace() # 设置断点
print(i)
pdb.set_trace() # 设置断点

最后一种方法: 使用IDE

单元测试

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

https://www.nli.cn/read/liaoxuefeng-python30/7cfd1dfdf10276d3.md

setUp与setDown

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

文档测试

Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。

doctest严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候,可以用表示中间一大段烦人的输出。

什么输出也没有。这说明我们编写的doctest运行都是正确的。如果程序有问题,比如把**getattr**()方法注释掉,再运行就会报错

当模块正常导入时,doctest不会被执行。只有在命令行直接运行时,才执行doctest。所以,不必担心doctest会在非测试环境下执行。

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

IO编程

异步IO和同步IO

在IO编程中,就存在速度严重不匹配的问题。举个例子来说,比如要把100M的数据写入磁盘,CPU输出100M的数据只需要0.01秒,可是磁盘要接收这100M数据可能需要10秒,怎么办呢?有两种办法:

第一种是CPU等着,也就是程序暂停执行后续代码,等100M的数据在10秒后写入磁盘,再接着往下执行,这种模式称为同步IO;

另一种方法是CPU不等待,只是告诉磁盘,“您老慢慢写,不着急,我接着干别的事去了”,于是,后续代码可以立刻接着执行,这种模式称为异步IO。

使用异步IO来编写程序性能会远远高于同步IO,但是异步IO的缺点是编程模型复杂。想想看,你得知道什么时候通知你“汉堡做好了”,而通知你的方法也各不相同。如果是服务员跑过来找到你,这是回调模式,如果服务员发短信通知你,你就得不停地检查手机,这是轮询模式。总之,异步IO的复杂度远远高于同步IO

操作IO的能力都是由操作系统提供的,每一种编程语言都会把操作系统提供的低级C接口封装起来方便使用,Python也不例外。我们后面会详细讨论Python的IO编程接口。

操作IO的能力都是由操作系统提供的,每一种编程语言都会把操作系统提供的低级C接口封装起来方便使用,Python也不例外。

文件读写

文件对象: 通常被称为文件描述

读文件

使用Python内置的open()函数,传入文件名和标识符:

1
>>> f = open('/User/Test/test.txt', 'r')

如果文件不存在,open()函数就会抛出一个IOErro,并且给出错误码和详细的信息告诉你文件不存在。

如果文件打开成功,调用read()方法可以一次读取文件的全部内容,Python把内容读取到内容中,用一个str对象表示

最后一步是调用close()方法关闭文件。文件使用完毕后必须关闭,因为文件对象会占用操作系统的资源,并且操作系统同一时间能打开的文件数量也是有限的

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

1
2
3
4
5
6
7
try: 
file_object = open('/Users/bowenkei/Desktop/Test.txt', 'r')
print(file_object.read())

finally:
if file_object:
file_object.close()

上述操作更好的方法是使用Python的with语句来自动帮助我们调用close()方法:

1
2
with open('/Users/bowenkei/Desktop/Test.txt', 'r') as file_object:
print(file_object.read())

read():会一次性读取文件的所有内容

read(size):每次最多读取size个字节的内容,可以反复调用

readline():每次读取一行内容

readlines():一次读取所有内容并按行返回list。

如果文件很小,read()一次性读取最方便;如果不能确定文件大小,反复调用read(size)比较保险;如果是配置文件,调用readlines()最方便

####file_like object

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

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

二进制文件

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

字符编码

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

1
2
with open('/Users/bowenkei/Desktop/GBK.txt', encoding='gbk') as file_object:
print(file_object.read())

遇到有些编码不规范的文件,你可能会遇到UnicodeDecodeError,因为在文本文件中可能夹杂了一些非法编码的字符。遇到这种情况,open()函数还接收一个errors参数,表示如果遇到编码错误后如何处理。最简单的方式是直接忽略:

1
2
with open('/Users/bowenkei/Desktop/GBK.txt', encoding='gbk', errors='ignore') as file_object:
print(file_object.read())

写入文件时,只需要使用相应的’w’和’wb’即可

‘a’模式可以实现往一个文件中添加内容而不清空原有内容

下面是各种模式:

image-20200809223545474

StringIO和BytesIO

StringIO: 在内存中读写str

要把str写入StringIO,我们需要先创建一个StringIO,然后,像文件一样写入即可:

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

BytesIO: 实现了在内存中读写bytes:

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
# 导入os模块之后,使用os.name可以获得操作系统类型
# 如果是posix,说明系统是Linux、Unix或Mac OS X,如果是nt,就是Windows系统。
# 要获取详细的系统信息,可以调用uname()函数:
# 注意uname()函数在Windows上不提供,也就是说,os模块的某些函数是跟操作系统相关的。
import os
print(os.name)
print(os.uname())

环境变量

在操作系统中定义的环境变量,全部在os.environ这个变量中,可以直接查看:

1
2
3
4
# 查看变量
os.environ
# 获取某个环境变量的值
os.emviron.get('key')

操作文件和目录

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

查看、创建和删除目录 :

  • 把新目录的完整路径表示出来: os.path.join(‘’, ‘’)

  • 创建一个目录: os.mkdir(‘’)

  • 删除一个目录: os.rmdir(‘’)

  • 拆分路径: os.path.split()

  • 拆分路径之分离文件名和后缀: os.path.splitext()

  • 重命名: os.rename(‘’)

  • 删掉文件: os.remove(‘’)

  • 列出当前目录下的所有文件: os.listdir()

  • 判断是否是目录: os.path.isdir()

把两个路径合为一个时,通过os.path.join()可以正确处理不同操作系统的路径分隔符

拆分路径时,不要直接去拆字符串,要通过os.path.split(),这样就可以baggie路径分成两部分,后一部分总是最后级别的目录或文件名

合并、拆分路径的函数并不是要求目录和文件要真实存在,它们只对字符串尽心操作。

复制文件的函数在os中不存在,因为复制文件并非由操作系统提供的系统调用

shutil模块是os的补充,其中提供了包括copy file()在内的多个函数

序列化

我们把变量从内存中变成可存储或可传输的过程称之为序列化,在Python中称为pickling,在Java中称为serialization。

序列化之后,我们就可以把序列化后的内容写入磁盘或者通过网络传输到别的机器上

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

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

pickle.dumps()方法把任意对象序列化成一个bytes,然后,就可以把这个bytes写入文件。或者用另一个方法pickle.dump()直接把对象序列化后写入一个file-like Object

由反序列化得到的变量和原来的变量是完全不相干的对象,它们只是内容相同而已

Pickle的问题和所有其他编程语言特有的序列化问题一样,就是它只能用于Python,并且可能不同版本的Python彼此都不兼容,因此,只能用Pickle保存那些不重要的数据

*小总结: *

pickle.dumps() and pickle.loads()都是将一个byte写入文件

pickle.dump() and pickle.load()都是将一个file-like-Object写入文件

JSON

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

JSON和Python内置的数据类型对应如下:

JSON Python
{} dict
[] list
“string” str
1234.56 int或float
true/false True/False
null None

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

1
2


JSON进阶

将class对象序列化为JSON的{} + 将JSON反序列化为class对象:

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
33
34
35
import pickle
import json
# 将class对象序列为JSON的{} 和 反序列化class对象
class SuperMan(object):
def __init__(self, name='bowenkei', age=20, sex='man'):
self.name = name
self.age = age
self.sex = sex

def get_name(self):
return self.name


s = SuperMan()


def superman2dict(super):
return {
'name': super.name,
'age': super.age,
'sex': super.sex
}


def dict2super(dict):
return SuperMan(dict['name'], dict['age'], dict['sex'])


print(json.dumps(s, default=superman2dict))
print(json.dumps(s, default=lambda obj: obj.__dict__))

json_str = '{"name": "lei bo wen", "age": 20, "sex": "man"}'
print(json.loads(json_str, object_hook=dict2super))
new_superman = json.loads(json_str, object_hook=dict2super)
print(new_superman.name)

进程和线程

多进程

Unix/linux操作系统提供了一个fork()系统调用。普通的函数调用,返回一次,fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后分别在父进程和子进程内返回。子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。

Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程

1
2
3
4
5
6
7
8
9
10
11
12
13
import os
print("Process (%s) start ... " % os.getpid())
pid = os.fork()
if pid == 0:
print("I am child process (%s) and my parent is %s." % (os.getpid(), os.getppid()))
else:
print("I (%s) just created a child process (%s)." % (os.getpid(), pid))


"""
os.getpid(): 获取当前进程id
os.getppid(): 获取父进程id
"""

####multiprocessing

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

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

用进程池的方式批量创建大量子进程:

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
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" % (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,))
p.close()
p.join()
print("All subprocesses done.")

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

请注意输出的结果,task 0,1,2,3是立刻执行的,而task 4要等待前面某个task完成后才执行,这是因为Pool的默认大小在我的电脑上是4,
因此,最多同时执行4个进程。这是Pool有意设计的限制,并不是操作系统的限制。如果改成:p = Pool(5)就可以同时跑5个进程。
Pool的默认大小是CPU的核心数。
"""

####子进程

很多时候,子进程并不是自身,而是一个外部进程。创建子进程后,还需要控制子进程的输入和输出

subprocess模块用于启动一个子进程,然后控制其输入和输出

1
2
3
4
import subprocess
print('$ nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)

子进程需要输入,可以使用communicate()方法输入(下面代码没有看懂,希望有大神指点指点):

1
2
3
4
5
6
import subprocess
print('$ nslookup')
p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = p.communicate(b'set q=mx\npython.org\nexit\n')
print(output.decode('utf-8'))
print('Exit code:', p.returncode)

####进程间通信

Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据,以实现Process之间的通信:

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
import os, time, random
# 以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据
# 写数据进程执行的代码
def write(q):
print("Process to write: %s" % os.getpid())
for value in ['A', 'B', 'C']:
q.put(value)
time.sleep(random.random())


# 读数据进程执行的代码
def read(q):
print("Process to read:%s " % os.getpid())
while True:
value = q.get(True)
print("Get %s from queue" % value)


if __name__ == '__main__':
# 父进程创建Queue,并传给各个子进程
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))

# 启动子进程pw,写入:
pw.start()
# 启动子进程pr,读取:
pr.start()
# 等待pw结束
pw.join()
# pr进程是死循环,只能强行终止:
pr.terminate()

多线程

多任务可以由多进程完成,也可以由一个进程内的多进程完成。

进程是由若干个线程组成的,一个进程至少有一个线程

线程是操作系统直接支持的执行单元。

Python的线程是真正的Posix Thread,而不是模拟出来的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Python中 _thread和threading用于多线程。其中_thread是低级模块,threading是高级模块,它对_thread进行了封装。
# 启动一个线程就是把一个函数传入并创建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)

任何进程默认会启动一个线程,这个线程称为主线程(MainThread),Python的threading模块下面的current_thread()函数用于返回当前线程的实例。子线程的名字在创建时指定。名字仅仅在打印时用来显示,完全没有其他任何意义。默认情况下,Python自动给线程命名为Thread-1, Thread-2……

Lock

线程的调度是由操作系统决定的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 没有使用Lock之前:
import threading, time
#
balance = 0
def change_it(n):
#
global balance
balance = balance + n
balance = balance - n


def run_thread(n):
for i in range(10000):
change_it(n)

t1 = threading.Thread(target=run_thread, args=(5, ))
t2 = threading.Thread(target=run_thread, args=(8, ))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)

两个线程同时一存一取,就可能导致余额不对,你肯定不希望你的银行存款莫名其妙地变成了负数,所以,我们必须确保一个线程在修改balance的时候,别的线程一定不能改。

如果我们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过threading.Lock()来实现:

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
import threading, time
#
balance = 0
lock = threading.Lock()
def change_it(n):
#
global balance
balance = balance + n
balance = balance - n


def run_thread(n):
for i in range(10000):
# 获取锁
lock.acquire()
try:
change_it(n)
finally:
# 释放锁
lock.release()

t1 = threading.Thread(target=run_thread, args=(5, ))
t2 = threading.Thread(target=run_thread, args=(8, ))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)

使用Lock的优缺点:

锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。

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

Threadlocal

用一个全局dict存放所有的Student对象,然后以thread自身作为key获得线程对应的Student对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 实现
from ReviewPython3 import Student
import threading
global_dict = {}

def std_thread(name):
std = Student(name)
# 把std放到全局变量global_dict中:
global_dict[threading.current_thread()] = std
do_task1()
do_task2()

def do_task1():
# 不传入std,而是根据当前的线程查找
std = global_dict[threading.current_thread()]
def do_task2():
std = global_dict[threading.current_thread()]

用Threadlocal替代上面自己创建的dict

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import threading
local_school = threading.local()

def process_student():
# 获取当前线程关联的student
std = local_school.student
print("Hello, %s (in %s)" % (std, threading.current_thread().name))


def process_thread(name):
# 绑定ThreadLocal的student:
local_school.student = name
process_student()


t1 = threading.Thread(target=process_thread, args=('Alice', ), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('Bob', ), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。

可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。

ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。

###进程 VS 线程

Master-Worker模式

Master负责分配任务,Worker负责执行任务,因此,多任务环境下,通常是一个Master,多个Worker。

如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。

如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。

多线程和多进程的优劣

正则表达式

正则表达式是一种用来匹配字符串的有力的武器

在正则表达式中,直接给出字符,就是精确匹配。

  • \d:one number
  • \w: one letter or one number
  • .: everything
  • *:任意个字符(包括0个)
  • +: 至少一个字符
  • ?: 0个或1个字符
  • {n}:n个字符
  • {n, m}: n-m个字符
  • \s: 可以匹配一个空格(也包括Tab等空白符)

进阶

更精确地匹配,使用[]

1
2
3
4
5
6
7
8
9

- [0-9a-zA-Z_]: 匹配一个数字,字母,或者下划线

- [0-9a-zA-Z_]+:匹配至少由一个数字、字母或者下划线组成的字符串

- [a-zA-Z_][0-9a-zA-Z_]{0, 19}: 更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)。
- A|B: 可以匹配A或B
- ^: 表示行的开头(^\d:以数字开头)
_ $: 表示行的结束(\d$:以数字结束)

re模块

1
2
3
4
5
import re
# 使用match方法判断正则表达式是否匹配
is_true = re.match(r'bowen[a-zA-Z]{3}', 'bowenkei')
print(is_true)
# 如果匹配,会返回一个Match对象,否则返回None。

常见的判断方法:

1
2
3
4
5
6
7
import re
yourname = input("Enter your name: ")

if re.match(r'bowen[a-zA-Z]{3}$', yourname):
print("Your name is true.")
else:
print("Sorry, your name is not right.")

切分字符串

用正则表达式切分字符串比用固定的字符更灵活。

1
2
3
4
5
6
7
8
# 普通的切分代码:
print('a b c'.split(' ')) # ['a', '', 'b', '', '', '', '', 'c']

# 使用正则表达式
print('a b c'.split(' ')) # ['a', '', 'b', '', '', '', '', 'c']
print(re.split(r'\s+', 'a b c')) # ['a', 'b', 'c']
print(re.split(r'[\s,]+', 'a,b,c, d')) # ['a', 'b', 'c', 'd']
print(re.split(r'[\s,;]+', 'a,b;c, d')) # ['a', 'b', 'c', 'd']

分组

正则表达式还有提取子串的强大功能。用()表示的就是要提取的分组(Group)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 分组
m = re.match(r'^([0-9]{3})-(\d{3,8})$', '012-456789')
print(m)
print(m.group(0))
print(m.group(1))
print(m.group(2))
'''
012-456789
012
456789
'''
# 注意到group(0)始终是原始字符串。
# 匹配时间: 19:05:30
time = '19:05:30'

m = re.match(r'^(0[0-9]|1[0-9]|2[0-3]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])$', time)

print(m.groups())

贪婪匹配

正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。

编译

使用正则表达式时,re模块内部会做下面两件事:

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

如果一个正则表达式要重复使用上千次,出于效率的考虑,可以预编译该正则表达式并保存下来,接下来重复使用的时候就可以跳过编译这个步骤,直接匹配了:

1
2
3
4
5
6
7
re_name = re.compile(r'bowen[a-zA-Z]{3}')

yourname = 'bowenkei'
yourname1 = 'bowenlei'

print(re_name.match(yourname))
print(re_name.match(yourname1))

小节练习:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
'''
1.1 请尝试写一个验证Email地址的正则表达式。版本一应该可以验证出类似的Email
1.2 版本二可以提取出带名字的Email地址
'''
import re
# Version 1
# someone@gmail.com
# bill.gates@microsoft.com
email_1 = re.compile(r'^[\w\.]+@[\w]+\.com$')
print(email_1.match('someone@gmail.com'))
print(email_1.match('bill.gates@microsoft.com'))
print(email_1.match('nianchucqc@gmail.com'))

# Version 2
# <Tom Paris> tom@voyager.org

email_2 = re.compile(r'^(<)([\w\.\s]+)(>)(\s[\w\.\s]+@[\w\.\s]+)$')
print(email_2.match('<Tom Paris> tom@voyager.org').group(2))

常用内建模块

datetime

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# 获取当前时间
from datetime import datetime
n = datetime.now()
print(n)

print(type(n)) # 获得datetime类型
# 注意,使用的是datetime包下的datetime类

# 获取指定日期和时间
dt = datetime(2008, 8, 8, 20, 8, 8)
print(dt)

# datetime转换为timestamp
'''
在计算机中,时间实际上使用数字表示的。我们把1970年1月1日 00:00:00 UTC+00:00时区的时刻称为epoch time,记为0。
1970年以前的时间timestamp为负数。
当前时间就是相对于epoch time的秒数,称为timestamp
可以认为:
timestamp = 0 = 1970-1-1 00:00:00 UTC+0:00
对应的北京时间就是:
timestamp = 0 = 1970-1-1 08:00:00 UTC+8:00

可见timestamp的值与时区毫无关系,因为timestamp一旦确定,其UTC时间就确定了,转换到任意时区的时间也是完全确定的,
这就是为什么计算机存储的当前时间是以timestamp表示的,因为全球各地的计算机在任意时刻的timestamp都是完全相同的(假定时间已校准)。
'''
tt = dt.timestamp()
print(tt)

# timestamp是一个浮点数,后面的小数表示毫秒
# Java和JavaScript的timestamp使用整数表示毫秒数,只需要将其timestamp除以1000就得到Python的浮点数表示方法

# timestamp转换为datetime
dt_1 = datetime.fromtimestamp(tt)
print(dt_1)

'''
timestamp没有时区,但是datetime有时区。
fromtimestamp默认转换为本地时间,也就是操作系统设置的时间。
要想转换为UTC标准时间可以将其替换为utcformtimestamp
'''
## 获得08年奥运会举办时标准UTC时间
dt_2 = datetime.utcfromtimestamp(tt)
print(dt_2)
# 可见北京时间时间比UTC标准时间多了8个小时
print("-------------------------------")
# str转换为datetime(转换后的时间没有时区信息) —— 使用strptime()
copy = datetime.strptime('200888200808', '%Y%m%d%H%M%S')
print(copy)
"""
注意:
year: Y
month: m
day: d
hour: H
minute: M
second: S
"""
print("-------------------------------")
# datetime转换为str —— 使用strftime()
print(n.strftime('%A, %B, %d %H:%M:%S'))
print("-------------------------------")
# datetime加减
"""
对时间和日期进行加减实际上就是把datetime往后或者往前计算,得到新的datetime。在导入timedelta这个类之后,加减可以直接使用+,-运算符

"""
from datetime import timedelta
# 获取当前时间
now = datetime.now()
print(now)
# 时间加
print(now + timedelta(hours=8))
# 时期减
print(now - timedelta(days=3))
# 时间加减
print(now + timedelta(days=3, hours=10))

print("-------------------------------")
# 本地时间转换为UTC时间
"""
一个datetime类型有一个时区属性tzinfo,但是默认为None,所以无法区分这个datetime到底是哪个时区,除非强行给datetime设置一个时区
"""
from datetime import timezone
now = datetime.now()
print(now)
# 创建时区UTC+8
timezone_utc_8 = timezone(timedelta(hours=8))

datetime = now.replace(tzinfo=timezone_utc_8)
print(datetime)

print("-------------------------------")
# 时区转换
# 拿到utc时间,并设置时区为utc+0.00
utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc)
print('UTC时间:',utc_dt)

# astimezone()将时区转换为北京时间
beijing_date = datetime.astimezone(timezone(timedelta(hours=8)))
print('北京时间:',beijing_date)
# astimezone()转换为东京时间
dongjing_date = datetime.astimezone(timezone(timedelta(hours=9)))
print('东京时间:', dongjing_date)

"""
时区转换的关键在于,拿到一个datetime时,要获知其正确的时区,然后强制设置时区,作为基准时间。

利用带时区的datetime,通过astimezone()方法,可以转换到任意时区。

注:不是必须从UTC+0:00时区转换到其他时区,任何带时区的datetime都可以正确转换,例如上述bj_dt到tokyo_dt的转换。
"""

下面截图是Python官方关于时间、日期转换的介绍:

image-20200814121200406

小节练习

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
"""
假设你获取了用户输入的日期和时间如2015-1-21 9:01:30,以及一个时区信息如UTC+5:00,均是str,请编写一个函数将其转换为timestamp:
"""
# answer
import re
from datetime import timezone, datetime, timedelta
def to_timestamp(dt_str, tz_str):
dt_com = re.compile(r'UTC([+|-][\d]{1,2}):00')
tz = dt_com.match(tz_str).group(1)
int_tz = int(tz)
set_timezone = timezone(timedelta(hours=int_tz))
str2dt = datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S')
str2dt_new = str2dt.replace(tzinfo=set_timezone) # 使用replace之后必须复制给另外一个变量,否则tzinfo无法修改

return str2dt_new.timestamp()




# 测试:
t1 = to_timestamp('2015-6-1 08:10:30', 'UTC+7:00')
assert t1 == 1433121030.0, t1
t2 = to_timestamp('2015-5-31 16:10:30', 'UTC-09:00')
assert t2 == 1433121030.0, t2
print('Pass')

collections

Collections 是Python内建的一个集合模块,提供了很多有用的集合类

namedtuple

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# namedtuple
from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)

print(p.x)
print(p.y)

"""
namedtuple是一个函数,用来创建一个自定义的tuple对象,并且规定了tuple元素的个数,并可以用属性而不是索引来引用tuple的某个元素
下面验证Point的类型:
"""
print(isinstance(p, Point))
print(isinstance(Point, tuple))
print(isinstance(p, tuple))

# 下面使用namedtuple定义一个圆
Circle = namedtuple('Circle', ['x', 'y', 'radius'])
# 创建一个圆形
circle = Circle(0, 0, 5)
print("The center of the circle is (%d, %d), and its radius is %s " % (circle.x, circle.y, circle.radius))

deque

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# namedtuple
from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)

print(p.x)
print(p.y)

"""
namedtuple是一个函数,用来创建一个自定义的tuple对象,并且规定了tuple元素的个数,并可以用属性而不是索引来引用tuple的某个元素
下面验证Point的类型:
"""
print(isinstance(p, Point))
print(isinstance(Point, tuple))
print(isinstance(p, tuple))

# 下面使用namedtuple定义一个圆
Circle = namedtuple('Circle', ['x', 'y', 'radius'])
# 创建一个圆形
circle = Circle(0, 0, 5)
print("The center of the circle is (%d, %d), and its radius is %s " % (circle.x, circle.y, circle.radius))

defaultdict

1
2
3
4
5
6
7
8
9
10
11
12
# defaultdict
'''
使用dict时,如果引用的Key不存在,就会抛出KeyError。如果希望key不存在时,返回一个默认值,就可以用defaultdict
默认值是调用函数返回的,函数在创建defaultdict对象时传入
除了在Key不存在时返回默认值,defaultdict的其他行为跟dict是完全一样的
'''
from collections import defaultdict

dd = defaultdict(lambda: 'The default value')
dd['key1'] = ' The value of key1'
print(dd['key1']) # The value of key1
print(dd['key2']) # The default value

orderedDict

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
# OrderedDict
'''
dict是无序的,如果要保持Key的顺序,可以使用OrderedDict;
OrderedDict的Key会按照插入的顺序排列,不是Key本身排序;
'''
from collections import OrderedDict
d = dict(a=1, c = 3, b = 2) # {'a': 1, 'b': 2, 'c': 3} ——--- Python3.6以后dict也是有序的了。
print(d)
# 实现Orderedict
d1 = OrderedDict(d)
print(d1)
# OrderedDict可以实现一个FIFO(先进先出)的dict,当容量超出限制时,先删除最早添加的Key:
# 下面这个实现没有怎么看懂
class LastUpdatedOrderedDict(OrderedDict):
def __init__(self, capacity):
super(LastUpdatedOrderedDict, self).__init__()
self.__capacity = capacity

def __setitem__(self, key, value):
containsKey = 1 if key in self else 0
if len(self) - containsKey >= self.__capacity:
last = self.popitem(last=False)
print('remove', last)

if containsKey:
del self[key]
print('set', (key, value))

else:
print('add', (key, value))
OrderedDict.__setitem__(self, key, value)

Counter

1
2
3
4
5
6
7
8
9
10
# Counter: 一个简单的计数器
# 使用Counter统一字符串中字符出现的次数
# Counter就是一个dict
from collections import Counter
c = Counter()
s = 'today is a good day, it\'s a good time to learn something'
for ch in s:
c[ch] = c[ch] + 1

print(c)

base64

Base是一种用64个字符来表示任意二进制数据的方法。

要让记事本这样的文本处理软件能处理二进制数据,就需要一个二进制到字符串的转换方法。

Base64 是一种最常见的二进制编码方法

Python内置的base64可以直接进行base64的解编码:

1
2
3
4
5
6
7
8
9
# base64
# 解编码
import base64
# 编码
bs1 = base64.b64encode(b'binary string')
print(bs1)
# 解码
bs2 = base64.b64decode(bs1)
print(bs2)

由于标准的Base64编码后可能出现字符+和/,在URL中就不能直接作为参数。

一种叫做“Url safe”的base64编码,其实就是把字符+和/分别编程-和_:

1
2
3
4
5
6
7
8
# 使用url-safe的base64编码
'''
由于标准的Base64编码后可能出现字符+和/,在URL中就不能直接作为参数,所以又有一种"url safe"的base64编码,其实就是把字符+和/分别变成-和_
'''
bs3 = base64.b64encode(b'i\xb7\x1d\xfb\xef\xff')
print(bs3) # b'abcd++//' ---- 字符+和/没有改变
bs4 = base64.urlsafe_b64encode(b'i\xb7\x1d\xfb\xef\xff')
print(bs4) # 字符+和/分别变成了-和_
1
2
3
4
5
6
7
8
9

base64是一种通过查表的编码方法,不能用于加密,即使使用自定义的编码表也不行
Base适用于小段内容的编码,比如数字证书签名、Cookie的内容等。
由于=字符也可能出现在Base64编码中,但=用在URL、Cookie里面会造成歧义,所以,很多Base64编码后会把=去掉:
# 标准Base64:
'abcd' -> 'YWJjZA=='
# 自动去掉=:
'abcd' -> 'YWJjZA'
去掉=后怎么解码呢?因为Base64是把3个字节变为4个字节,所以,Base64编码的长度永远是4的倍数,因此,需要加上=把Base64字符串的长度变为4的倍数,就可以正常解码了。

常见函数

.join()

用于将序列中的元素以指定的字符连接生成一个新的字符

用法:

Str.join(sequence)

  • Sequence – 要连接的元素序列

capitalize ()

-capitalize()方法返回字符串的一个副本,只有它的第一个字母大写。对于8位的字符串,这个方法与语言环境相关。

用法如下:

1
2
str.capitalize()
# 上述方法并不会改变str中的内容,只是会返回一个首字母大写的副本

index()

index(content):用于判断给定字符的下标

strip()

用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列

*注意: *该方法只能删除开头或是结尾的字符,不能删除中间部分的字符

bytes()

描述:

bytes函数返回一个新的bytes对象,该对象是一个0<=x<256区间内的整数不可变序列。它是bytearray的不可变版本。

语法如下:

1
class bytes([source[, encoding[, errors]]])

参数解释:

  • 如果source为整数,则返回一个长度为source的初始化数组;
  • 如果source为字符串,则按照指定的encoding将字符串转换为字节序列;
  • 如果source为可迭代类型,则元素必须为[0.256]中的整数
  • 如果source为与buffer接口一致的对象,则此对象也可以被用于初始化bytearray;
  • 如果没有输入任何参数,默认就是初始化数组为0个元素

struct

Python提供了一个struct模块来解决bytes和其他二进制数据的转换

struct的pack函数把任意数据类型编程bytes:

1
2
3
4
5
6
7
8
9
10
11
12
# struct
# struct的pack函数把任意数据类型变成bytes
import struct
s = struct.pack('>I', 10240099)
print(s) # b'\x00\x9c@c'
# 上面>表示: 字节顺序是big-endian,也就是网络序;I表示4字节无符号整数
# 后面的参数个数要和处理指令一致
# unpack把bytes变成相应的数据类型
four_bytes_int, two_bytes_int = struct.unpack('>IH', b'\xf0\xf0\xf0\xf0\x80\x80')
print(four_bytes_int) # 4042322160
print(two_bytes_int) # 32896
# H表示2字节无符号整数

小节练习

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
import struct
# 请编写一个bmpinfo.py,可以检查任意文件是否是位图文件,如果是,打印出图片大小和颜色数。
s = b'\x42\x4d\x38\x8c\x0a\x00\x00\x00\x00\x00\x36\x00\x00\x00\x28\x00\x00\x00\x80\x02\x00\x00\x68\x01\x00\x00\x01\x00\x18\x00'

def bmpinfo(s):
if not isinstance(s, bytes):
s = bytes(s, encoding='utf-8')
if s[0:2] == (b'BM' or b'BA'):
result = struct.unpack('<ccIIIIIIHH', s)
photo_size = result[-4:-2]
print("此位图的大小是:%d x %d" % (photo_size[0], photo_size[1]))
print("此位图的颜色数是:%d" % result[len(result) - 1])

else:
print("抱歉, 这不是一个位图。")

bmpinfo(s)
s1 = 'fdsfsdfsdfdsf'
bmpinfo(s1)

"""
output:
b'BM'
此位图的大小是:640 x 360
此位图的颜色数是:24
b'fd'
抱歉, 这不是一个位图。
"""

hashlib

Python的hashlib提供了常见的摘要算法: 如MD5, SHA1等等

摘要算法: 即哈希算法、散列算法。把任意长度的数据转换为一个长度固定的数据串(通常用16进制的字符串表示)。

摘要算法就是通过摘要函数对任意长度的数据data计算出固定长度的digest,目的是为了发现原始数据是否被篡改过。

以MD5为例,计算出一个字符串的MD5值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# hashlib
import hashlib
md5 = hashlib.md5()
md5.update('how to use md5 in python hashlib?'.encode('utf-8'))
print(md5.hexdigest())
'''
MD5是常见的摘要算法,速度很快,生成结果是固定的128bit字节,通常用一个32位的16进制字符串表示。
'''

"""
另一种常见的摘要算法是SHA1,调用SHA1和调用MD5完全类似:
"""
sh1 = hashlib.sha1()
sh1.update('how to use sha1 in '.encode('utf-8'))
sh1.update('python hashlib?'.encode('utf-8'))
print(sh1.hexdigest())
""""
SHA1的结果是160bit字节,通常用一个40位的16进制字符串表示
"""

摘要算法应用

任何允许用户登录的网站都会存储用户登录的用户名和口令。

正确的保存口令的方式是不存储用户的明文口令,而是存储用户口令的摘要

当用户登录时,首先计算用户输入的明文口令的MD5,然后和数据库存储的MD5对比,如果一致,说明口令输入正确,如果不一致,口令肯定错误。

小节练习

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
import hashlib


# 1 - 根据用户输入的口令,计算出存储在数据库中的MD5口令:
def calc_md5(password):
s = str(password)
one_md5 = hashlib.md5()
one_md5.update(s.encode('utf-8'))

return one_md5.hexdigest()


print(calc_md5("todayisaniceday."))

# 2 - 设计一个验证用户登录的函数,根据用户输入的口令是否正确,返回True或False:
db = {
'michael': 'e10adc3949ba59abbe56e057f20f883e',
'bob': '878ef96e86145580c38c87f0410ad153',
'alice': '99b1c2188db85afee403b1536010c2c9'
}

def login(user, password):
s = str(password)
one_md5 = hashlib.md5()
one_md5.update(s.encode('utf-8'))
return one_md5.hexdigest() == db[str(user)]


print(login('michael', 123456))
print(login('bob', '888888'))
print(login('alice', password=123456))

常用口令的MD5很容易被计算出来,所以,要确保存储的用户口令不是那些已经被计算出来的常用口令的MD5,这一方法通过对原始口令加一个复杂字符串来实现,俗称“加盐”

1
2
def calc_mad(password):
return get_md5(password + 'the-Salt')

这样只要salt不被黑客知道,即使用户输入简单口令,也很难通过MD5反推口令。

练习

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
33
import hashlib

# 根据用户输入的登录名和口令模拟用户注册,计算更安全的MD5:
salt_value = 'the-Salt'
db = {}
def register(username, password):
db[username] = get_md5(password + username + salt_value)



def get_md5(password):
s = str(password)
one_md5 = hashlib.md5()
one_md5.update(s.encode('utf-8'))
return one_md5.hexdigest()



# 测试
register('bob', 'hello world')
print(db) # {'bob': '7fa0c1935fed7e7730be403321f3b04c'}
register('bowenkei', 'today is a good day')
print(db)

# 根据修改后的MD5算法实现用户登录的验证:
def login(user, password):
return db[str(user)] == get_md5(str(password) + str(user) + salt_value)


# Test
print(login('bob', 'hello world'))
print(login('bowenkei', 'today is a good day'))
print(login('bowenkei', 123456))

*注意: *

摘要算法不是加密算法,不能用于加密,只能用于防篡改,但是它的单向计算性决定了可以在不存储明文口令的情况下验证用户口令

itertools

Python的内建模块itertools提供了非常有用的用于操作迭代对象的函数

“无限”迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# count()会创建一个无限迭代器。
import itertools
naturals = itertools.count(100) # count()括号里面的数字指定了从哪一个数字开始数
print(naturals.__next__())
print(naturals.__next__())

# cycle() —— 会把传入的一个序列无限重复下去
abc = itertools.cycle(['a', 'b', 'c'])
for i in range(3 * 2): # 这里指定了次数,避免出现死循环
print(abc.__next__())

# repeat()负责将一个元素无限重复下去,不过如果提供第二个参数就可以限定重复次数:
ns = itertools.repeat('A', 3)
for n in ns:
print(n) # 迭代了三次

# 无限序列虽然可以无限迭代下去,但是通常会通过takenwhile()等函数根据条件判断来截取一个有限的序列:
naturals = itertools.count(1)
ns = itertools.takewhile(lambda x:x <= 10, naturals)
print(ns)
print(list(ns))

itertools提供的几个迭代器操作函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
# chain()
# chain()可以把一组迭代对象串联起来,形成一个更大的迭代器
new_iter = itertools.chain('ABC', 'XYZ')
print(list(new_iter)) # ['A', 'B', 'C', 'X', 'Y', 'Z']

# proupby()
# groupby把迭代器中相邻的重复元素挑出来放在一起:
for key, group in itertools.groupby('AAABBBBBBDDDDCCCCAAA'):
print(key, list(group))

# 向groupby中传入一个函数可以实现忽略大小写:
for key, group in itertools.groupby('AAABBBBbcdaabccBBDDDDCCCCAAA', lambda c: c.upper()):
print(key, list(group))

XML(解析)

操作XML有两种方法:DOM和SAX。正常情况下,优先考虑SAX,因为DOM实在太占内存

解析XML时,我们关心三件事情: start_element, end_element和char_data。

1
2


其他知识

Python- 变量前加或者*

➕*

image-20200729113837756

➕ **

image-20200729113908275

Python标准注释

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

关于切片

对于切片,[:]中:前后两个数字必须满足从小到大的顺序,同时,包括前者,但并不包括后者

关于Dict

Python中,可以使用if判断key是否在dict中,但是不能直接使用value判断其是否在dict中

关于JSON

标准JSON中不能有多余的——逗号(,)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import pickle
import json
# python to json
python_str = {"age":20, "score":88, "name":"Bob"}
json_str = json.dumps(python_str)
print(json_str)
print(type(json_str))
# json to python
json_str = '{"age":20, "score":88, "name":"Bob"}'
python_str = json.loads(json_str)
print(python_str)
print(type(python_str))
"""
{"age": 20, "score": 88, "name": "Bob"}
<class 'str'>
{'age': 20, 'score': 88, 'name': 'Bob'}
<class 'dict'>
"""

关于全局变量

参见:

https://blog.csdn.net/songyunli1111/article/details/76095971

关于join()

Join()函数并不会改变原有的序列