0%

Socket介绍

Socket套接字
Python中提供socket.py标准库,非常底层的接口库。
Socket是一种通用的网络编程接口,和网络层次没有一一对应的关系。

协议族
AF表示Address Family,用于socket()第一个参数

名称 含义
AF_INET IPV4
AF_INET6 IPv6
AF_UNIX Unix Domain Socket,windows没有

socket类型
| 名称 | 含义 |
| ——– | ——————————- |
| SOCK_STREAM| 面向连接的流套接字。默认值,TCP协议|
|SOCK_DGRAM |无连接的数据报文套接字。UDP协议 |

TCP编程

Socket编程,需要两端,一般来说需要一个服务端、一个客户端,服务端称为Server,客户端称为Client

TCP服务端编程


服务器端编程步骤

  • 创建Socket对象

  • 绑定IP地址Address和端口Port。bind()方法

    IPv4地址为一个二元组(‘IP地址字符串’, Port)

  • 开始监听,将在指定的IP的端口上监听。listen()方法

  • 获取用于传送数据的Socket对象 socket.accept() -> (socket object, address info)
    accept方法阻塞等待客户端建立连接,返回一个新的Socket对象和客户端地址的二元组
    地址是远程客户端的地址,IPv4中它是一个二元组(clientaddr, port)

    • 接收数据
      recv(bufsize[, flags]) 使用缓冲区接收数据

    • 发送数据
      send(bytes)发送数据

查看监听端口

1
2
3
4
5
6
windows 命令
# netstat -anp tcp | findstr 9999

linux命令
# netstat -tanl | grep 9999
# ss -tanl | grep 9999

练习——写一个一对多通信

需求分析

聊天工具是CS程序,C是每一个客户端,S是服务器端。

服务器应该具有的功能:
启动服务,包括绑定地址和端口,并监听
建立连接,能和多个客户端建立连接
接收不同用户的信息
分发,将接收的某个用户的信息转发到已连接的所有客户端
停止服务
记录连接的客户端

代码实现

服务端应该对应一个类

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
import logging
import socket
import threading
import datetime

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(thread)d %(message)s")


class ChatServer:
def __init__(self, ip='127.0.0.1', port=9999): # 启动服务
self.sock = socket.socket()
self.addr = (ip, port)
self.clients = {} # 客户端
self.event = threading.Event()

def start(self): # 启动监听
self.sock.bind(self.addr) # 绑定
self.sock.listen() # 监听
#accept会阻塞主线程,所以开一个新线程
threading.Thread(target=self.accept).start()

def accept(self): # 多人连接
while not self.event.is_set():
sock, client = self.sock.accept() # 阻塞
self.clients[client] = sock # 添加到客户端字典
#准备接收数据,recv是阻塞的,开启新的线程
threading.Thread(target=self.recv, args=(sock, client)).start()

def recv(self, sock:socket.socket, client): # 接收客户端数据
while not self.event.is_set():
data = sock.recv(1024) # 阻塞到数据到来
msg = data.decode().strip()
# 客户端退出命令
if msg == 'quit' or msg == '':
self.clients.pop(client)
sock.close()
logging.info('{} quits'.format(client))
break

msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(datetime.datetime.now(), *client,data.decode())
logging.info(msg)
msg = msg.encode()
for s in self.clients.values():
s.send(msg)

def stop(self): # 停止服务
for s in self.clients.values():
s.close()
self.sock.close()
self.event.set()

cs = ChatServer()
cs.start()

while True:
cmd = input('>>').strip()
if cmd == 'quit':
cs.stop()
threading.Event().wait(3)
break
logging.info(threading.enumerate()) # 用来观察断开后线程的变化

socket常用的方法


名称 含义
socket.recv(bufsize[, flags]) 获取数据。默认是阻塞的方式
socket.recvfrom(bufsize[, flags]) 获取数据,返回一个二元组(bytes, address)
socket.recv_into(buffer[, nbytes[,flags]]) 获取到nbytes的数据后,存储到buffer中。如果nbytes没有指定或0,将buffer大小的数据存入buffer中。返回接收的字节数。
socket.recvfrom_into(buffer[,nbytes[, flags]]) 获取数据,返回一个二元组(bytes, address)到buffer中
socket.send(bytes[, flags]) TCP发送数据
socket.sendall(bytes[, flags]) TCP发送全部数据,成功返回None
socket.sendto(string[,flag],address) UDP发送数据
socket.sendfile(file, offset=0,count=None) 发送一个文件直到EOF,使用高性能的os.sendfile机制,返回发送的字节数。如果win下不支持sendfile,或者不是普通文件,使用send()发送文件。offset告诉起始位置。3.5版本开始
socket.getpeername() 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)
socket.getsockname() 返回套接字自己的地址。通常是一个元组(ipaddr,port)
socket.setblocking(flag) 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常
socket.settimeout(value) 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
socket.setsockopt(level,optname,value) 设置套接字选项的值。比如缓冲区大小。太多了,去看文档。不同系统,不同版本都不尽相同

MakeFile


socket.makefile(mode='r', buffering=None, *, encoding=None, errors=None, newline=None)
创建一个与该套接字相关连的文件对象,将recv方法看做读方法,将send方法看做写方法。

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
# 使用makefile简单例子
import socket
sockserver = socket.socket()
ip = '127.0.0.1'
port = 9999
addr = (ip, port)
sockserver.bind(addr)
sockserver.listen()
print('-'30)
s, _ = sockserver.accept()
print('-'30)
f = s.makefile(mode='rw')

line = f.read(10) # 阻塞等
print('-'30)
print(line)
f.write('Return your msg: {}'.format(line))
f.flush()


# 改写成循环接收消息
import socket
import threading


sockserver = socket.socket()
ip = '127.0.0.1'
port = 9999
addr = (ip, port)
sockserver.bind(addr)
sockserver.listen()
print('-'30)

event = threading.Event()

def accept(sock:socket.socket, e:threading.Event):
s, _ = sock.accept()
f = s.makefile(mode='rw')

while True:
line = f.readline()
print(line)
if line.strip() == "quit": # 注意要发quit\n
break
f.write('Return your msg: {}'.format(line))
f.flush()
f.close()
sock.close()
e.wait(3)

t = threading.Thread(target=accept, args=(sockserver, event))
t.start()
t.join()

print(sockserver)

makefile练习


使用makefile改写一对多通信

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
import logging
import socket
import threading
import datetime


logging.basicConfig(level=logging.INFO, format="%(asctime)s %(thread)d %(message)s")

class ChatServer:
def init(self, ip='127.0.0.1', port=9999): # 启动服务
self.sock = socket.socket()
self.addr = (ip, port)
self.clients = {} # 客户端
self.event = threading.Event()

def start(self): # 启动监听
self.sock.bind(self.addr) # 绑定
self.sock.listen() # 监听
#accept会阻塞主线程,所以开一个新线程
threading.Thread(target=self.accept).start()

def accept(self): # 多人连接
while not self.event.is_set():
sock, client = self.sock.accept() # 阻塞
#准备接收数据,recv是阻塞的,开启新的线程
f = sock.makefile('rw') # 支持读写
self.clients[client] = f # 添加到客户端字典
threading.Thread(target=self.recv, args=(f, client), name='recv').start()

def recv(self, f, client): # 接收客户端数据
while not self.event.is_set():
try:
data = f.readline() # 阻塞到换行符
except Exception as e:
logging.error(e) # 有任何异常,退出
data = 'quit'
msg = data.strip()
#客户端退出命令
if msg == 'quit':
self.clients.pop(client)
f.close()
logging.info('{} quits'.format(client))
break
msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(datetime.datetime.now(), *client,data)
logging.info(msg)

for s in self.clients.values():
s.write(msg)
s.flush()

def stop(self): # 停止服务
for s in self.clients.values():
s.close()
self.sock.close()
self.event.set()

def main():
cs = ChatServer()
cs.start()

while True:
cmd = input('>>').strip()
if cmd == 'quit':
cs.stop()
threading.Event().wait(3)
break
logging.info(threading.enumerate()) # 用来观察断开后线程的变化
if __name__ == '__main__':
main()

TCP客户端编程


客户端编程步骤

  • 创建Socket对象
  • 连接到远端服务端的IP和PORT、connect()方法
  • 传输数据
    • 使用send、recv方法发送、接收数据
  • 关闭连接、释放资源
1
2
3
4
5
6
7
8
import socket
client = socket.socket()
ipaddr = ('127.0.0.1', 9999)
client.connect(ipaddr) # 直接连接服务器
client.send(b'abcd\n')
data = client.recv(1024) # 阻塞等待
print(data)
client.close()

编写聊天的客户类

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
import socket
import threading
import datetime
import logging

FORMAT = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)


class ChatClient:
def init(self, ip='127.0.0.1', port=9999):
self.sock = socket.socket()
self.addr = (ip, port)
self.event = threading.Event()

def start(self): # 启动对远端服务器的连接
self.sock.connect(self.addr)
self.send("I'm ready.")
#准备接收数据,recv是阻塞的,开启新的线程
threading.Thread(target=self.recv, name="recv").start()

def recv(self): # 接收客户端的数据
while not self.event.is_set():
try:
data = self.sock.recv(1024) # 阻塞
except Exception as e:
logging.error(e)
break
msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(datetime.datetime.now(), *self.addr,data.strip())
logging.info(msg)

def send(self, msg:str):
data = "{}\n".format(msg.strip()).encode() # 服务端需要一个换行符
self.sock.send(data)

def stop(self):
self.sock.close()
self.event.wait(3)
self.event.set()
logging.info('Client stops.')

def main():
cc = ChatClient()
cc.start()
while True:
cmd = input('>>>')
if cmd.strip() == 'quit':
cc.stop()
break
cc.send(cmd) # 发送消息


if name == 'main':
main()

反射

概述

运行时,区别于编译时,指的是程序被加载到内存中执行的时候。
反射,reflection,指的是运行时获取类型定义信息。
简单说,在Python中,能够通过一个对象,找出其type、class、attribute或method的能力,称为反射或者自省。
具有反射能力的函数有:type()、isinstance()、callable()、dir()、getattr()

内建函数 意义
getattr(object, name[, default]) 通过name返回object的属性值。当属性不存在,将使用default返回,如果没有default,则抛出AttributeError。name必须为字符串
setattr(object, name, value) object的属性存在,则覆盖,不存在,新增
hasattr(object, name) 判断对象是否有这个名字的属性,name必须为字符串

给一个实例用setattr添加一个方法,不做绑定,不会绑定到类中,此时不能用类调用,然而类使用setattr方法,会把那个方法绑定到类的字典上。

思考
这种动态增加属性的方式和装饰器修饰一个类、Mixin方式的差异?

这种动态增删属性的方式是运行时改变类或者实例的方式,但是装饰器或Mixin都是定义时就决定了,因此反射能力具有更大的灵活性。

反射相关的魔术方法

__getattr__()__setattr__()__delattr__()这三个魔术方法。

魔术方法 意义
__getattr__() 当通过搜索实例、实例的类及祖先类查不到属性,就会调用此方法
__setattr__() 通过.访问实例属性,进行增加、修改都要调用它
__delattr__() 当通过实例来删除属性时调用此方法
__getattribute__() 实例所有的属性调用都从这个方法开始

一个类的属性会按照继承关系找,如果找不到,就会执行 __getattr__() 方法,如果没有这个方法,就会抛出AttributeError异常表示找不到属性。
查找属性顺序为:
实例调用__getattribute__() –> instance.__dict__ –> instance.__class__.__dict__ –> 继承的祖先类(直到object)的__dict__—找不到–> 调用__getattr__()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Point(Base):
z = 6
def __init__(self, x, y):
self.x = x
self.y = y

def show(self):
print(self.x, self.y)

def __getattr__(self, item):
return "missing {}".format(item)

def __setattr__(self, key, value):
print("setattr {}={}".format(key,value))
self.__dict__[key] = value

__setattr__()方法,可以拦截对实例属性的增加、修改操作,如果要设置生效,需要自己操作实例的
__dict__

__getattribute__方法

实例的所有的属性访问,第一个都会调用__getattribute__方法,它阻止了属性的查找,该方法应该返回(计算后的)值或者抛出一个AttributeError异常。

  • 它的return值将作为属性查找的结果。
  • 如果抛出AttributeError异常,则会直接调用__getattr__方法,因为表示属性没有找到。

__getattribute__方法中为了避免在该方法中无限的递归,它的实现应该永远调用基类的同名方法以访问需要的任何属性,例如object.__getattribute__(self, name)
注意,除非你明确地知道__getattribute__方法用来做什么,否则不要使用它。

描述器

描述器的表现
用到3个魔术方法:__get__()、__set__()、__delete__()
方法签名如下
object.__get__(self, instance, owner)
object.__set__(self, instance, value)
object.__delete__(self, instance)
self 指代当前实例,调用者
instance 是owner的实例
owner 是属性的所属的类

描述器定义

Python中,一个类实现了__get____set____delete__三个方法中的任何一个方法,就是描述器。
如果仅实现了__get__,就是非数据描述符 non-data descriptor
同时实现了__get____set__就是数据描述符 data descriptor

如果一个类的类属性设置为描述器实例,那么它被称为owner属主。

属性查找顺序

实例的__dict__ 优先于非数据描述器
数据描述器优先于实例的__dict__
__delete__方法有同样的效果,有了这个方法,也是数据描述器。

Python中的描述器


Python的方法(包括staticmethod()和classmethod())都实现为非数据描述器。因此,实例可以重新定义和覆盖方法。这允许单个实例获取与同一类的其他实例不同的行为。

property()函数实现为一个数据描述器。因此,实例不能覆盖属性的行为。

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
class A:
@classmethod # 非数据描述器
def foo(cls):
pass

@staticmethod # 非数据描述器
def bar():
pass

@property # 数据描述器
def z(self):
return 5

def getfoo(self): # 非数据描述器
return self.foo

def __init__(self): # 非数据描述器
self.foo = 100
self.bar = 200
#self.z = 300

a = A()
print(a.__dict__)
print(A.__dict__)


#foo、bar都可以在实例中覆盖,但是z不可以。

特殊属性

属性 含义
__name_ 类、函数、方法等的名字
__module_ 类定义所在的模块名
__class_ 对象或者类所属的类
__bases__ 类的基类的元组,顺序为它们在基类列表中出现的顺序
__doc__ 类、函数的文档字符串,如果没有定义则为None
__mro__ 类的mro,class.mro()返回的结果保存在__mro__
__dict__ 类的实例的属性,可写的字典

查看属性

方法 意义
__dir__ 返回类或者对象的所有成员名称列表。dir()函数就是调用__dir__()。
使用实例调用时,如果提供__dir__(),则返回其返回值,要求是可迭代对象。 如果没有提供__dir__(),则会从实例和类及祖先类中收集信息(尽可能多的)

如果dir([obj])参数obj包含方法__dir__(),该方法将被调用。如果参数obj不包含__dir__(),该方法将最大限
度地收集属性信息。

dir(obj)对于不同类型的对象obj具有不同的行为:

  1. 如果对象是模块对象,返回的列表包含模块的属性名和变量名。

  2. 如果对象是类或者类对象,返回的列表包含类的属性名,及它的基类的属性名。

  3. 如果obj不写,返回列表包含内容不同

    • 在模块中,返回模块的属性和变量名
    • 在函数中,返回本地作用域的变量名
    • 在方法中,返回本地作用域的变量名

魔术方法


分类:

  • 创建、初始化和销毁
    • __new____init____del__
  • hash
  • bool
  • 可视化
  • 运算符重载
  • 容器和大小
  • 可调用对象
  • 上下文管理
  • 反射
  • 描述器
  • 其他

实例化


方法 意义
__new__ 实例化一个对象
该方法需要返回一个值(本类型的实例),如果该值不是cls的实例,则不会调用__init__
该方法永远都是静态

__new__方法很少使用,即使创建了该方法,也会使用return super().__new__(cls)得到实例化对象,或者基类object的__new__方法来创建实例并返回。

hash


方法 意义
__hash__ 内建函数hash()调用的返回值,返回一个整数。如果定义这个方法该类的实例就可hash。
__eq__ 对应==操作符,判断2个对象是否相等,返回bool值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A:
def __init__(self, name, age=18):
self.name = name

def __hash__(self):
return 1

def __eq__(self, other):
return self.name == other.name

def __repr__(self):
return self.name

print(hash(A('tom')))
print((A('tom'), A('tom')))
print([A('tom'), A('tom')])
print({('tom',), ('tom',)})

hash(x) ,x都一样,求得的hash应该是不变的,这是幂等性的缘故,一般来说,x不一样,hash应该不一样。

不同的hash算法,不同的x求得同样的hash值,这就是hash冲突。


__hash__方法只是返回一个hash值作为set的key,但是去重,还需要__eq__(等效==,就是内容相等)来判断2个对象是否相等,is判断的是内存地址,is相等的话就肯定是同一个元素。
hash值相等,只是hash冲突,不能说明两个对象是相等的。
因此,一般来说提供__hash__方法是为了作为set或者dict的key,所以去重要同时提供__eq__方法。

不可hash对象isinstance(p1, collections.Hashable)一定为False。
去重需要提供__eq__方法。


list类实例为什么不可hash

源码中有一句__hash__ = None,也就是如果调用__hash__()相当于None(),一定报错。
所有类都继承object,而这个类是具有__hash__()方法的,如果一个类不能被hash,就把__hash__设置为None。

bool


方法 意义
__bool__ 内建函数bool(),或者对象放在逻辑表达式的位置,调用这个函数返回布尔值。 定义__bool__(),这个函数的返回值必须要是bool类型。没有定义__bool__(),就找__len__()返回长度,非0为真。 如果__len__()也没有定义,那么所有实例都返回真。

两个对象(类和实例)可以当作True理解。

等效Fals的本质是:对于四大皆空:空串,空元组。空列表,空字典,先找bool,如果没有bool就看长度,当长度为0就恒为假。

可视化


方法 意义
__repr__ 内建函数repr()对一个对象获取字符串表达。
调用__repr__方法返回字符串表达,如果__repr__也没有定义,就直接返回object的定义,就是显示内存地址信息。
__str__ str()函数、format()函数、print()函数调用,需要返回对象的字符串表达。如果没有定义,就去调用__repr__方法返回字符串表达,如果__repr__没有定义,就直接返回对象的内存地址信息。
__bytes__ bytes()函数调用,返回一个对象的bytes表达,即返回bytes对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A:
def __init__(self, name, age=18):
self.name = name
self.age = age

def __repr__(self):
return 'repr: {},{}'.format(self.name, self.age)

def __str__(self):
return 'str: {},{}'.format(self.name, self.age)

def __bytes__(self):
#return "{} is {}".format(self.name, self.age).encode()
import json
return json.dumps(self.__dict__).encode()


print(A('tom')) # print函数使用__str__
print([A('tom')]) # []使用__str__,但其内部使用__repr__
print([str(A('tom'))]) # []使用__str__,其中的元素使用str()函数也调用__str__
print(bytes(A('tom')))

总结:一般首先找的是repr,当repr没有就找str,都没有就去object中去寻找。bytes方法转成二进制只是一种序列化的表达,和序列化还是有差别的,序列化是一种通用的二进制格式或者通用的中间格式,序列化是一种交互,转化成文本传输的,而这里的转换只是一种表达,给人展示用的。

注意不能通过判断是否带引号来判断输出值的类型,类型判断要使用type或isinstance

运算符重载


operator模块提供了以下特殊方法,可以将类的实例使用下面的操作符来操作

运算符 特殊方法 含义
<, <=, ==, >, >=, != __lt__, __le__, __eq__, __gt__, __ge__, __ne__ 比较运算符
+, -, *, /, %, //,**, divmod __add__, __sub__, __mul__, __truediv__, __mod__, __floordiv__, __pow__, __divmod__ 算数运算符
+=, -=, *=, /=, %=, //=, **= __iadd__, __isub__, __imul__, __itruediv__, __imod__, __ifloordiv__, __ipow__

@functools.total_ordering 装饰器

__lt__, __le__, __eq__, __gt__, __ge__是比较大小必须实现的方法,但是全部写完太麻烦,使用
@functools.total_ordering 装饰器就可以大大简化代码。

但是要求__eq__必须实现,其它方法__lt__, __le__, __gt__, __ge__ 实现其一。

但是:

__eq__等于可以推断不等于
__gt__大于可以推断小于
__ge__大于等于可以推断小于等于
也就是用3个方法,就可以把所有比较解决了,所以total_ordering可以不使用

容器相关方法


方法 意义
__len__ 内建函数len(),返回对象的长度(>=0的整数),如果把对象当做容器类型看,就如同list或者dict。bool()函数调用的时候,如果没有__bool__()方法,则会看__len__()方法是否存在,存在返回非0为真。
__iter__ 迭代容器时,调用,返回一个新的迭代器对象
__contains__ in 成员运算符,没有实现,就调用__iter__方法遍历
__getitem__ 实现self[key]访问。序列对象,key接受整数为索引,或者切片。对于set和dict,key为hashable。key不存在引发KeyError异常
__setitem__ __getitem__的访问类似,是设置值的方法
__missing__ 字典或其子类使用__getitem__()调用时,key不存在执行该方法
1
2
3
4
5
6
7
class A(dict):
def __missing__(self, key):
print('Missing key : ', key)
return 0

a = A()
print(a['k'])

可调用对象


1
2
3
4
5
6
def foo():
print(foo.__module__, foo.__name__)

foo()
# 等价于
foo.__call__()

函数即对象,对象foo加上(),就是调用此函数对象的__call__()方法。

可调用对象

方法 意义
__call__ 类中定义一个方法,实例就可以像函数一样调用
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
class Point:
def __init__(self, x, y):
self.x = x
self.y = y

def __call__(self, *args, **kwargs):
return "<Point {}:{}>".format(self.x, self.y)

p = Point(4, 5)
print(p)
print(p())

class Adder:
def __call__(self, *args):
ret = 0
for x in args:
ret += x
self.ret = ret
return ret

adder = Adder()
print(adder(4, 5, 6))
print(adder.ret)


#<__main__.Point object at 0x000002D5F04C2198>
#<Point 4:5>
#15
#15

练习:

定义一个斐波那契数列的类,方便调用,计算第n项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Fib:
def __init__(self):
self.items = [0, 1, 1]

def __call__(self, index):
if index < 0:
raise IndexError('Wrong Index')
if index < len(self.items):
return self.items[index]

for i in range(3, index+1):
self.items.append(self.items[i-1] + self.items[i-2])
return self.items[index]

print(Fib()(100))

上例中,增加迭代的方法、返回容器长度、支持索引的方法

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
class Fib:
def __init__(self):
self.items = [0, 1, 1]

def __call__(self, index):
return self[index]

def __iter__(self):
return iter(self.items)

def __len__(self):
return len(self.items)

def __getitem__(self, index):
if index < 0:
raise IndexError("Wrong Index")
if index < len(self.items):
return self.items[index]

for i in range(len(self), index + 1):
self.items.append(self.items[i - 1] + self.items[i - 2])
return self.items[index]

def __str__(self):
return str(self.items)

__repr__ = __str__


fib = Fib()
print(fib(5), len(fib)) # 全部计算
print(fib(10), len(fib)) # 部分计算
print("------")
for x in fib:
print(x, end=" ")

print()
print(fib[5], fib[9]) # 索引访问,不计算

上下文管理对象


当一个对象同时实现了__enter__()和__exit__()方法,它就属于上下文管理的对象。

方法 意义
__enter__ 进入与此对象相关的上下文。如果存在该方法,with语法会把该方法的返回值作为绑定到as子句中指定的变量上
__exit__ 退出与此对象相关的上下文。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Point:
def __init__(self):
print("init")

def __enter__(self):
print("enter")

def __exit__(self, exc_type, exc_val, exc_tb):
print("exit")


with Point() as f:
print("-" * 30)
raise Exception('error')

print("=====end=======")

实例化对象的时候,并不会调用enter,进入with语句块调用__enter__方法,然后执行语句体,最后离开with语句块的时候,调用__exit__方法。

with可以开启一个上下文运行环境,在执行前做一些准备工作,执行后做一些收尾工作。
注意,with并不开启一个新的作用域。

上下文管理很安全,不管是碰到异常都还是会正常执行

__enter__方法返回值就是上下文中使用的对象,with语法会把它的返回值赋给as子句的变量。

方法的参数


__enter__方法 没有其他参数。
__exit__方法有3个参数:
__exit__(self, exc_type, exc_value, traceback)
这三个参数都与异常有关。
如果该上下文退出时没有异常,这3个参数都为None。
如果有异常,参数意义如下
exc_type,异常类型
exc_value,异常的值
traceback,异常的追踪信息
__exit__方法返回一个等效True的值,则压制异常;否则,继续抛出异常

上下文应用场景


  1. 增强功能
    在代码执行的前后增加代码,以增强其功能。类似装饰器的功能。
  2. 资源管理
    打开了资源需要关闭,例如文件对象、网络连接、数据库连接等
  3. 权限验证
    在执行代码之前,做权限的验证,在__enter__中处理

contextlib.contextmanager


contextlib.contextmanager
它是一个装饰器实现上下文管理,装饰一个函数,而不用像类一样实现__enter__和__exit__方法。
对下面的函数有要求,必须有yield,也就是这个函数必须返回一个生成器,且只有yield一个值。
也就是这个装饰器接收一个生成器对象作为参数。

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

@contextlib.contextmanager
def sub(x, y): # 为生成器函数增加了上下文管理
print("enter")
start = datetime.datetime.now()
try:
yield x - y # yield的值只能有一个,作为__enter__方法的返回值
finally:
detla = (datetime.datetime.now() - start).total_seconds()
print(detla)
print("exit")

with sub(6, 2) as f:
time.sleep(2)
print("------------")
print(f)
print("~~~~~~~~~~")

#输出
enter
------------
4
~~~~~~~~~~
2.000654
exit

当yield发生处为生成器函数增加了上下文管理。这是为函数增加上下文机制的方式。

  • 把yield之前的当做__enter__方法执行
  • 把yield之后的当做__exit__方法执行
  • 把yield的值作为__enter__的返回值

总结 :
如果业务逻辑简单可以使用函数加contextlib.contextmanager装饰器方式,如果业务复杂,用类的方式加__enter__和__exit__方法方便。

面向对象


一种认识世界,分析世界的方法论。将万事万物抽象为类

类class

类是抽象的概念,是万事万物的抽象,是一类事物的共同特征的集合。用计算机语言来描述就是属性方法的集合(封装)。

对象instance、object,对象是类的具象,是一个实体,类也是对象。

属性,它是对象状态的抽象,用数据结构来描述。

操作,是对对象行为的抽象,用操作名和实现该操作的方法来描述。

哲学

一切皆对象

对象是数据和操作的封装

对象是独立的,但是对象之间可以相互作用

目前OOP是最接近人类认知的编程范式

面向对象三要素

封装:将属性和方法组装到一起,隐藏数据,对外只暴露一些接口用于连接。

继承:目的:多复用,继承来的就不用自己写了;多继承少修改(开闭原则),OCP(open_closed principle),使用继承来改变。作用:就是为了修改不一样的属性。

多态:面向对象编程最灵活的地方,多种表现,动态绑定。

Python的类

定义:

1
2
class ClassName
语句块

要求:

  1. 必须使用class关键字

  2. 类名必须使用大驼峰命名

  3. 类定义完成后会产生一个类对象,会绑定到ClassName这个标识符上面。

类对象和类属性

类对象,类的定义就会生成一个类对象

类的属性,类定义中的变量和类中定义的方法都是类的属性

类变量,定义在类中的变量

实例化

在类对象名称后面加上一个括号,就调用类的实例化方法,完成实例化,实例化就真正创建一个该类的对象(实例)。

每一次实例化,只能生成该类的一个具体实例,生成的是不同的实例。即使通过相同的参数实例化,得到的对象也不相同。

python类实例化后,会自动调用__init__方法,可以不定义,如果没有定义会在实例化后隐式调用,初始化函数可以有多个参数,第一参数必须留给self,init方法不能有返回值,也就是只能有return None

作用:对实例进行初始化

类里面的函数叫做方法对象method,不是普通的函数对象function,一般至少要有一个参数,第一个参数可以是self(一般习惯上用self),这个参数位置指代的就是当前这个实例本身。

实例对象instance

类实例化后一定会获得一个对象,就是实例对象。init方法的第一个参数self就是指代某一个实例。

实例变量和类变量

实例变量是每个实例自己的变量,是自己独有的;列变量是类的变量,是类的所有实例共享的属性和方法。

特殊属性 含义
_name_ 对象名
_class_ 对象的类型
_dict_ 对象的属性的字典
_qualname_ 类的限定名

类属性保存在类的dict中,实例属性保存在实例的dict中,如果从实例访问类的属性,就要借助class找到所属的类。

类有类名字,实例没有实体名。

python中每一种对象都拥有不同的属性。函数、类都是对象,类的实例也是对象。

类不可以访问实例的属性,实例可以访问类的属性。(是类的,也是这个类所有实例的,其实例都可以访问到;是实例的,就是这个实例自己的,通过类访问不到。)

对象(实例或类)可以动态的给自己增加一个属性,实例._dict_[变量名]可以访问到,实例的同名变量会隐藏掉类变量,或者说是覆盖了这个类的变量。

一般来说,属性使用字典保存是为了提升查找效率,必须用空间换时间,但是也有个问题,如果数百万个对象,那么字典占的比较大,所以可以使用__slots__方法。

slots告诉解释器,实例的属性都叫什么,一般来说,既然要节省内存,那就最好还是使用元组。

一旦类提供了slots,就阻止实例产生dict来保存实例的属性。子类不会继承slots

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A:
X = 1

__slots__ = ('z','y')

def __init__(self):
self.y = 5

def show(self):
print(self.X,self.y)

a = A()
a.show()

print("A",A.__dict__.keys())
# print("obj",a.__dict__.keys())

**实例属性的查找顺序 **
指的是实例使用.点号来访问属性,会先找实例自己的__dict__,如果没有,然后通过属性__class__找到自己的类,再去类的__dict__中找类属性中没找到就往类继承的父类中去查找,最终找到根基类object,没找到则抛出异常 。
注意,如果实例使用__dict__[变量名]访问变量,将不会按照上面的查找顺序找变量了,这是指明使用字典的key查找,不是属性查找。
一般来说,类变量可使用全大写来命名。

装饰一个类

为一个类通过装饰,添加一些类属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#增加类变量
def adda_name(name,cls):
cls.NAME = name #动态增加类属性
#改进成装饰器
def add_name(name):
def wrapper(cls):
cls.NAME = name
return cls
return wrapper
@add_name("jerry")
class Person(): #Person = add_name("jerry")(Person) = wrapper(person) = Person
AGE = 3

# adda_name(Person ,"tom")

print(Person.__dict__)
print(Person.NAME)

之所以能够装饰,本质上是为类对象动态的添加了一个属性,而Person这个标识符指向这个类对象。


类方法和静态方法

类方法

1
2
3
4
5
6
7
8
class Person:
@classmethod
def class_method(cls): # cls是什么
print('class = {0.__name__} ({0})'.format(cls))
cls.HEIGHT = 170

Person.class_method()
print(Person.__dict__)

类方法

  1. 在类定义中,使用@classmethod装饰器修饰的方法,不管使用实例还是类来调用,调用的都是类的方法,传入当前类自身。

  2. 必须至少有一个参数,且第一个参数留给了cls,cls指代调用者即类对象自身

  3. cls这个标识符可以是任意合法名称,但是为了易读,请不要修改

  4. 通过cls可以直接操作类的属性

静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person:
def method(self,name): #普通方法
print(self)

@classmethod #类方法
def class_method(cls , age): # cls是什么 是当前类
print('class = {0.__name__} ({0})'.format(cls))
cls.HEIGHT = 170

@staticmethod #静态方法
def static_methd(a):
print(Person.HEIGHT , a)


Person.method(Person(),"ken") #第一参数不会自动传入,自己传Person()
Person().method("tom") #第一参数自动传入Person()

Person.class_method(20) #第一参数cls自动传入
Person().method(18) #第一参数cls自动传入

Person.static_methd() #第一参数不用传
Person().static_method() #第一参数不用传

print(Person.__dict__)

静态方法

  1. 在类定义中,使用@staticmethod装饰器修饰的方法
  2. 调用时,不管是实例还是类调用,不会隐式的传入参数 。
    静态方法,只是表明这个方法属于这个名词空间。函数归在一起,方便组织管理。

总结:
类除了普通方法都可以调用,普通方法需要对象的实例作为第一参数。
实例可以调用所有类中定义的方法(包括类方法、静态方法),普通方法传入实例自身,静态方法和类方法需要找到实例的类。

实例方法

如果用实例,第一参数则传入self,则叫做绑定。如果使用类调用,则没有绑定(未绑定行为)。

访问控制

私有(Private)属性

使用双下划线开头的属性名,就是私有属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person:
def __init__(self, name, age=18):
self.name = name
self.__age = age

def growup(self, i=1):
if i > 0 and i < 150: # 控制逻辑
self.__age += i

def getage(self):
return self.__age

print(Person('tom').getage())

私有变量的本质:
类定义的时候,如果声明一个实例变量的时候,使用双下划线,Python解释器会将其改名,转换名称为_类名__变量名的名称,所以用原来的名字访问不到了。

保护变量

在变量名前使用一个下划线,称为保护变量。

1
2
3
4
5
6
7
8
class Person:
def __init__(self, name, age=18):
self.name = name
self._age = age

tom = Person('Tom')
print(tom._age)
print(tom.__dict__)

可以看出,这个_age属性根本就没有改变名称,和普通的属性一样,解释器不做任何特殊处理。 这只是开发者共同的约定,看见这种变量,就如同私有变量,不要直接使用。

私有方法

参照保护变量、私有变量,使用单下划线、双下划线命名方法。

私有方法的本质

单下划线的方法只是开发者之间的约定,解释器不做任何改变。

双下划线的方法,是私有方法,解释器会改名,改名策略和私有变量相同,**_类名__方法名
方法变量都在类的
__dict__**中可以找到。

私有成员的总结 :

在Python中使用_单下划线或者 __ 双下划线来标识一个成员被保护或者被私有化隐藏起来。
但是,不管使用什么样的访问控制,都不能真正的阻止用户修改类的成员。Python中没有绝对的安全的保护成员或者私有成员。
因此,前导的下划线只是一种警告或者提醒,请遵守这个约定。除非真有必要,不要修改或者使用保护成员或者私有成员,更不要修改它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person:
def __init__(self, name, age=18):
self.name = name
self._age = age

def _getname(self):
return self.name

def __getage(self):
return self._age

tom = Person('Tom')
print(tom._getname()) # 没改名
print(tom.__getage()) # 无此属性
print(tom.__dict__)
print(tom.__class__.__dict__)
print(tom._Person__getage()) # 改名了

补丁

可以通过修改或者替换类的成员。使用者调用的方式没有改变,但是,类提供的功能可能已经改变了。

猴子补丁(Monkey Patch):
在运行时,对属性、方法、函数等进行动态替换。
其目的往往是为了通过替换、修改来增强、扩展原有代码的能力。
黑魔法,慎用。

1
2
3
4
5
6
7
8
9
10
11
# test1.py
from test2 import Person
from test3 import get_score

def monkeypatch4Person():
Person.get_score = get_score

monkeypatch4Person() # 打补丁

if __name__ == "__main__":
print(Person().get_score())
1
2
3
4
5
6
# test2.py
class Person:
def get_score(self):
# connect to mysql
ret = {'English':78, 'Chinese':86, 'History':82}
return ret
1
2
3
# test3.py
def get_score(self):
return dict(name=self.__class__.__name__,English=88, Chinese=90, History=85)

上例中,假设Person类get_score方法是从数据库拿数据,但是测试的时候,不方便。
为了测试时方便,使用猴子补丁,替换了get_score方法,返回模拟的数据。

属性装饰器

一般好的设计是:把实例的属性保护起来,不让外部直接访问,外部使用getter读取属性和setter方法设置属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person:
def __init__(self, name, age=18):
self.name = name
self.__age = age

@property
def age(self):
return self.__age

@age.setter
def age(self, age):
self.__age = age

@age.deleter
def age(self):
# del self.__age
print('del')

tom = Person('Tom')
print(tom.age)
tom.age = 20
print(tom.age)
del tom.age

特别注意:使用property装饰器的时候这三个方法同名
property装饰器
后面跟的函数名就是以后的属性名。它就是getter。这个必须有,有了它至少是只读属性

setter装饰器
与属性名同名,且接收2个参数,第一个是self,第二个是将要赋值的值。有了它,属性可写

deleter装饰器
可以控制是否删除属性。很少用

property装饰器必须在前,setter、deleter装饰器在后。
property装饰器能通过简单的方式,把对方法的操作变成对属性的访问,并起到了一定隐藏效果.

对象的销毁

类中可以定义 __del__ 方法,称为析构函数(方法)。

作用:销毁类的实例的时候调用,以释放占用的资源。其中就放些清理资源的代码,比如释放连接。

注意这个方法不能引起对象的真正销毁,只是对象销毁的时候会自动调用它。

使用del语句删除实例,引用计数减1。当引用计数为0时,会自动调用__del__ 方法。
由于Python实现了垃圾回收机制,不能确定对象何时执行垃圾回收。

由于垃圾回收对象销毁时,才会真正清理对象,还会在回收对象之前自动调用__del__ 方法,除非你明确知道自己的目的,建议不要手动调用这个方法。

方法重载(overload)

其他面向对象的高级语言中,会有重载的概念。
所谓重载,就是同一个方法名,但是参数数量、类型不一样,就是同一个方法的重载。

Python没有重载!
Python不需要重载!
Python中,方法(函数)定义中,形参非常灵活,不需要指定类型(就算指定了也只是一个说明而非约束),参数个数也不固定(可变参数)。一个函数的定义可以实现很多种不同形式实参的调用。所以Python不需要方法的重载。

或者说Python本身就实现了其它语言的重载。

封装

面向对象的三要素之一,封装Encapsulation

将数据和操作组织到类中,即属性和方法

将数据隐藏起来,给使用者提供操作(方法)。使用者通过操作就可以获取或者修改数据。getter和setter。

通过访问控制,暴露适当的数据和操作给用户,该隐藏的隐藏起来,例如保护成员或私有成员。

python中,对象赋值实际上是对象的引用。当创建一个对象,将其赋值给另一个变量,python并没有拷贝这个对象,而是拷贝了这个对象的引用。 所以如果从单纯的赋值语句来实现clone对象的话, 那可能bug出现的也会莫名其妙. Python中可以使用copy模块来复制对象.

copy.copy 为浅拷贝, 只copy父对象, 不会拷贝对象内部的子对象 copy.deepcopy 深拷贝, 拷贝对象及其子对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> import copy
>>> a = [1,2,3,4,['a','b']]
>>> b = a # 赋值拷贝
>>> c = copy.copy(a) # 浅拷贝, 只拷贝了a的引用, 内部元素没有拷贝.
>>> d = copy.deepcopy(a) # 深拷贝, 完全拷贝
>>>
>>> a.append(5)
>>> a[4].append('c')
>>>
>>> print 'a=',a
a= [1, 2, 3, 4, ['a', 'b', 'c'], 5]
>>> print 'b=',b
b= [1, 2, 3, 4, ['a', 'b', 'c'], 5] #赋值拷贝, 内存地址指向一样的。 相当于一个人的两个名字而已.
>>> print 'c=',c
c= [1, 2, 3, 4, ['a', 'b', 'c']] #浅拷贝, 子元素c[4]引用的地址和a[4]是一样的. 所以改变a[4]相当于改了c[4].
>>> print 'd=',d
d= [1, 2, 3, 4, ['a', 'b']] #深拷贝完全拷贝. a和d完全改变了.

异常处理

异常Exception


错误 Error :错误是可以避免的
逻辑错误:算法写错了,加法写成了减法
笔误:变量名写错了,语法错误
函数或类使用错误,其实这也属于逻辑错误

异常 Exception :异常不可能避免
本意就是意外情况
这有个前提,没有出现上面说的错误,也就是说程序写的没有问题,但是在某些情况下,会出现一些意外,导致程序无法正常的执行下去。
例如open函数操作一个文件,文件不存在,或者创建一个文件时已经存在了,或者访问一个网络文件,突然断网了,这就是异常,是个意外的情况。

错误和异常
在高级编程语言中,一般都有错误和异常的概念,异常是可以捕获,并被处理的,但是错误是不能被捕获的。

产生异常


产生:

  • raise 语句显式的抛出异常
  • Python解释器自己检测到异常并引发它

程序会在异常抛出的地方中断执行,如果不捕获,就会提前结束程序(其实是终止当前线程的执行)

raise语句
raise后什么都没有,表示抛出最近一个被激活的异常,如果没有,则抛类型异常。这种方式很少用 。

raise后要求应该是BaseException类的子类或实例,如果是类,将被无参实例化。

异常类及继承层次


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
# Python异常的继承

BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- RuntimeError
| +-- RecursionError
+-- MemoryError
+-- NameError
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- SyntaxError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError

BaseException及子类


BaseException
所有内建异常类的基类是BaseException

SystemExit
sys.exit(n)函数引发的异常,异常不捕获处理,就直接交给Python解释器,解释器退出。n=0,正常退出,n=1异常退出。

如果except语句捕获了该异常,则继续向后面执行,如果没有捕获住该异常SystemExit,解释器直接退出程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import sys

print('before')
sys.exit(1)
print('SysExit')
print('outer') # 不执行

# 捕获这个异常
import sys
try:
sys.exit(1)
except SystemExit: # 换成Exception
print('SysExit')
print('outer') # 执行

KeyboardInterrupt
对应的捕获用户中断行为Ctrl + C

1
2
3
4
5
6
7
8
try:
import time
while True:
time.sleep(0.5)
pass
except KeyboardInterrupt:
print('ctl + c')
print('outer')

Exception及子类

Exception是所有内建的、非系统退出的异常的基类,自定义异常应该继承自它

SyntaxError 语法错误
Python将这种错误也归到异常类下面的Exception下的子类,但是这种错误是不可捕获的

ArithmeticError 所有算术计算引发的异常,其子类有除零异常等

LookupError
使用映射的键或序列的索引无效时引发的异常的基类:IndexError, KeyError

自定义异常
从Exception继承的类

1
2
3
4
5
6
7
class MyException(Exception):
pass

try:
raise MyException()
except MyException: # 捕获自定义异常
print('catch the exception')

未实现和未实现异常

1
2
3
4
5
print(type(NotImplemented))
print(type(NotImplementedError))

#<class 'NotImplementedType'>
#<class 'type'>

NotImplemented是个值,单值,是NotImplementedType的实例

NotImplementedError是类型,是异常,返回type

异常的捕获


1
2
3
4
try:
待捕获异常的代码块
except [异常类型]:
异常的处理代码块

使用了try…except语句块捕捉到了这个异常,异常生成位置之后语句将不再执行,转而执行对应的except部分的语句,最后执行try…except语句块之外的语句。

except 后接异常类型,用来捕获指定类型的异常,except可以捕获多个异常。

捕获规则
捕获是从上到下依次比较,如果匹配,则执行匹配的except语句块
如果被一个except语句捕获,其他except语句就不会再次捕获了
如果没有任何一个except语句捕获到这个异常,则该异常向外抛出

捕获的原则
从小到大,从具体到宽泛

被抛出的异常,应该是异常的实例,使用as子句接收这个抛出的异常。

finally子句


finally
最终,即最后一定要执行的,try…finally语句块中,不管是否发生了异常,都要执行finally的部分

finally中一般放置资源的清理、释放工作的语句,也可以在finally中再次捕捉异常。

异常的传递


1
2
3
4
5
6
7
8
9
def foo1():
return 1/0

def foo2():
print('foo2 start')
​ foo1()
print('foo2 stop')

foo2()

foo2调用了foo1,foo1产生的异常,传递到了foo2中。
异常总是向外层抛出,如果外层没有处理这个异常,就会继续向外抛出
如果内层捕获并处理了异常,外部就不能捕获到了
如果到了最外层还是没有被处理,就会中断异常所在的线程的执行。注意整个程序结束的状态返回值。

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
# 线程中测试异常
import threading
import time


def foo1():
return 1/0

def foo2():
time.sleep(3) # 3秒后抛出异常
print('foo2 start')
foo1()
print('foo2 stop')

t = threading.Thread(target=foo2)
t.start()

while True:
time.sleep(1)
print('Everything OK')
if t.is_alive():
print('alive')
else:
print('dead')

try嵌套


内部捕获不到异常,会向外层传递异常
但是如果内层有finally且其中有return、break语句,则异常就不会继续向外抛出:异常被压制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try:
try:
ret = 1 / 0
except KeyError as e:
print(e)
finally:
print('inner fin')
except:
print('outer catch')
finally:
print('outer fin')

#输出
#inner fin
#outer catch
#outer fin

异常的捕获的时机

1.立即捕获

需要立即返回一个明确的结果

1
2
3
4
5
6
7
def parse_int(s):
try:
return int(s)
except:
return 0

print(parse_int('s'))

2.边界捕获

封装产生了边界

例如,写了一个模块,用户调用这个模块的时候捕获异常,模块内部不需要捕获、处理异常,一旦内部处理了,外
部调用者就无法感知了。
例如,open函数,出现的异常交给调用者处理,文件存在了,就不用再创建了,看是否修改还是删除
例如,自己写了一个类,使用了open函数,但是出现了异常不知道如何处理,就继续向外层抛出,一般来说最外
层也是边界,必须处理这个异常了,否则线程退出

else子句

1
2
3
4
5
6
7
8
try:
ret = 1 * 0
except ArithmeticError as e:
print(e)
else:
print('OK')
finally:
print('fin')

else子句
没有任何异常发生,则执行

总结


1
2
3
4
5
6
7
8
9
10
11
try:
<语句> #运行别的代码
except <异常类>:
<语句> # 捕获某种类型的异常
except <异常类> as <变量名>:
<语句> # 捕获某种类型的异常并获得对象
else:
<语句> #如果没有异常发生
finally:
<语句> #退出try时总会执行

try的工作原理

1、如果try中语句执行时发生异常,搜索except子句,并执行第一个匹配该异常的except子句
2、如果try中语句执行时发生异常,却没有匹配的except子句,异常将被递交到外层的try,如果外层不处理这个异常,异常将继续向外层传递。如果都不处理该异常,则会传递到最外层,如果还没有处理,就终止异常所在的线程
3、如果在try执行时没有发生异常,将执行else子句中的语句
4、无论try中是否发生异常,finally子句最终都会执行。

模块化

Python中只有一种模块对象类型,但是为了模块化组织模块的便利,提供了“包”的概念。模块module,指的是Python的源代码文件。

包package,指的是模块组织在一起的和包名同名的目录及其相关文件。

导入语句

语句 含义
import 模块1,[模块2….] 完全导入
import….as….. 模块别名

import语句

1、找到指定的模块,加载和初始化它,生成模块对象。找不到,抛出ImportError

2、在import所在的作用域的局部命名空间中,增加名称和上一步创建的对象关联。

总结

导入顶级模块,其名称会加入到本地名词空间中,并绑定到其模块对象。

导入非顶层模块,只将其顶级模块名称加入到本地名称空间中。导入的模块必须使用完全限定名称来访问。

如果使用了as,as后的名称直接绑定到导入的模块对象,并将该名称加入到本地名词空间中。

文件IO常用操作

一般说IO操作,指的是文件IO。

把文件存储到磁盘上的这个过程,叫做落地。

column column
open 打开
read 读取
write 写入
close 关闭
readline 行读取
readlines 多行读取
seek 文件指针操作
tell 指针位置
open打开操作
1
2
3
4
5
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True,opener=None)

f = open("file名字") #文件对象
print(f.read()) #读取文件
f.close() #关闭文件

打开一个文件,返回一个文件对象(流对象)和文件描述符。打开文件失败,则返回异常。
基本使用: 创建一个文件test,然后打开它,用完关闭。

文件操作中最常用的就是读和写。文件访问的模式有两种:文本模式和二进制模式。

注:windows中使用codepage代码页。可以认为每一个代码页就是一张编码表 cp936和gbk等价。

mode模式
描述字符
r 缺省的,表示只读打开
w 只写打开,有的话就清除重新写
x 创建并写入一个新文件
a 写入打开,如果文件存在,则追加
b 二进制模式
t 缺省的,文本模式
+ 读写打开一个文件,给原来只读、只写的增加缺失的功能

open默认是只读模式r打开已经存在的文件。

  • r 只读打开文件,如果使用write方法,会抛异常。 如果文件不存在,抛出FileNotFoundError异常。

  • w 表示只写方式打开,如果读取则抛出异常 如果文件不存在,则直接创建文件 如果文件存在,则清空文件内容。

  • x 文件不存在,创建文件,并只写方式打开,文件存在,抛出FileExistsError异常。

  • a 文件存在,只写打开,追加内容 文件不存在,则创建后,只写打开,追加内容

  • r是只读,wxa都是只写。 wxa都可以产生新文件,w不管文件存在与否,都会生成全新内容的文件;a不管文件是否存在,都能在打开的文件尾部追加;x必须要求文件事先不存在,自己造一个新文件。

  • +为r、w、a、x提供缺失的读或写功能,但是,获取文件对象依旧按照r、w、a、x自己的特征。 +不能单独使用,可以认为它是为前面的模式字符做增强功能的。

t和b:

  • 文本模式t 字符流,将文件的字节按照某种字符编码理解,按照字符操作。open的默认mode就是rt。

  • 二进制模式b 字节流,将文件就按照字节理解,与字符编码无关。二进制模式操作时,字节操作使用bytes类型。

t/b不能单独存在,要和a/w/x/r配合使用。

seek文件指针

文件指针,指向当前字节位置。

mode = r,指针起始在0 ,mode = a 指针起始在EOF。

tell():显示指针当前位置。

seek(offset[, whence]) 移动文件指针位置。offest偏移多少字节,whence从哪里开始。

  • 文本模式下 whence 0 缺省值,表示从头开始,offset只能正整数 whence 1 表示从当前位置,offset只接受0,whence 2 表示从EOF开始,offest只接受0

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 文本模式
    f = open('test4','r+')
    f.tell() # 起始
    f.read()
    f.tell() # EOF
    f.seek(0) # 起始
    f.read()
    f.seek(2,0)
    f.read()
    f.seek(2,0)
    f.seek(2,1) # offset必须为0
    f.seek(2,2) # offset必须为0
    f.close()
  • 二进制模式下 whence 0 缺省值,表示从头开始,offest只能正整数 whence 1表示从当前位置,offest可正可负,whence 2 表示从EOF开始,offest可正可负。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 二进制模式
    f = open('test4','rb+')
    f.tell() # 起始
    f.read()
    f.tell() # EOF
    f.write(b'abc')
    f.seek(0) # 起始
    f.seek(2,1) # 从当前指针开始,向后2
    f.read()

    f.seek(-2,2) # 从EOF开始,向前2
    f.read()
    f.seek(-20,2) # OSError
    f.close()
  • 二进制模式支持任意起点的偏移,从头、从尾、从中间位置开始。 向后seek可以超界,但是向前seek的时候,不能超界,否则抛异常。

buffering缓冲区

-1 表示使用缺省大小的buffer。如果是二进制模式,使io.DEFAULT_BUFFER_SIZE值,默认是4096或者8192。如果是文本模式,如果是终端设备,是行缓存方式,如果不是,则使用二进制模式的策略。

  • 0 只在二进制模式使用,表示关buffer
  • 1 只在文本模式使用,表示使用行缓冲。意思就是见到换行符就flush
  • 大于1 用于指定buffer的大小

buffer 缓冲区

缓冲区一个内存空间,一般来说是一个FIFO队列,到缓冲区满了或者达到阈值,数据才会flush到磁盘。

flush() 将缓冲区数据写入磁盘 close() 关闭前会调用flush()。

io.DEFAULT_BUFFER_SIZE 缺省缓冲区大小,字节。

buffering 说明
buffering = -1 t和b,都是io.DEFAULT_BUFFER_SIZE
buffering = 0 b 关闭缓冲区
t 不支持
buffering = 1 b 就一个字节
t 行缓冲,遇到换行符才flush
buffering > 1 b模式表示行缓冲大小。缓冲区的值可以超过io.DEFAULT_BUFFER_SIZE,直到设定的值超出后才把缓冲区flush 。
t模式,是io.DEFAULT_BUFFER_SIZE字节,flush完后把当前字符串也写入磁盘

一般来说:

  1. 文本模式,一般都用默认缓冲区大小
  2. 二进制模式,是一个个字节的操作,可以指定buffer的大小
  3. 一般来说,默认缓冲区大小是个比较好的选择,除非明确知道,否则不调整它
  4. 一般编程中,明确知道需要写磁盘了,都会手动调用一次flush,而不是等到自动flush或者close的时候
其他参数

编码:windows下缺省GBK(0xB0A1),Linux下缺省UTF-8(0xE5 95 8A)

errors :编码错误将被捕获 None和strict表示有编码错误将抛出ValueError异常;ignore表示忽略

newline:文本模式中,换行的转换。可以为None、’’ 空串、’\r’、’\n’、’\r\n’ 。

  • None表示’\r’、’\n’、’\r\n’都被转换为’\n’;
  • ‘’ 表示不会自动转换通用换行符;其它合法字符表示换行符就是指定字符,就会按照指定字符分行写。
  • ‘\n’或’’表示’\n’不替换;其它合法字符表示’\n’会被替换为指定的字符

closefd:关闭文件描述符,True表示关闭它。False会在文件关闭后保持这个描述符。fileobj.fileno()查看。

文件描述符:Linux一切皆文件,文件打开后都会有一个位于的文件描述符,在计算机系统中是一个有限的资源。0,1,2,标准输入,标准输出,标准错误输出。

对于类似于文件对象的IO对象,一般来说都需要在不使用的时候关闭、注销,以释放资源。
IO被打开的时候,会获得一个文件描述符。计算机资源是有限的,所以操作系统都会做限制。就是为了保护计算机的资源不要被完全耗尽,计算资源是共享的,不是独占的。

一般情况下,除非特别明确的知道资源情况,否则不要提高资源的限制值来解决问题。

read()

read(size=-1)
size表示读取的多少个字符或字节;负数或者None表示读取到EOF

readline(size=-1)
一行行读取文件内容。size设置一次能读取行内几个字符或字节。

readlines(hint=-1)
读取所有行的列表。指定hint则返回指定的行数。

write()

write(s),把字符串s写入到文件中并返回字符的个数 writelines(lines),将字符串列表写入文件。

close()

flush并关闭文件对象。
文件已经关闭,再次关闭没有任何效果。

其他
名称 说明
seekable() 是否可seek
readable() 是否可读
writeable() 是否可写
closed() 是否已经关闭
上下文管理

1、异常处理
当出现异常的时候,拦截异常。但是,因为很多代码都可能出现OSError异常,还不好判断异常就是应为资源限制产生的。

1
2
3
4
5
f = open('test')
try:
f.write("abc") # 文件只读,写入失败
finally:
f.close() # 这样才行

使用finally可以保证打开的文件可以被关闭。

上下文管理

  1. 使用with … as 关键字

  2. 上下文管理的语句块并不会开启新的作用域

  3. with语句块执行完的时候,会自动关闭文件对象

StringIO操作

io模块中的类

from io import StringIO

内存中,开辟的一个文本模式的buffer,可以像文件对象一样操作它

当close方法被调用的时候,这个buffer会被释放

1
2
3
4
5
6
7
8
9
from io import StringIO
# 内存中构建
sio = StringIO() # 像文件对象一样操作
print(sio.readable(), sio.writable(), sio.seekable())# True True True
sio.write("luo\nPython")
sio.seek(0)
print(sio.readline()) #magedu
print(sio.getvalue()) # 无视指针,输出全部内容 magedu Python
sio.close()

好处

一般来说,磁盘的操作比内存的操作要慢得多,内存足够的情况下,一般的思路是少落地,减少磁盘IO的过程,可以大大的提高程序的运行效率。

BytesIO操作

io模块中的类

from io import BytesIO

内存中,开辟的一个二进制模式的buffer,可以像文件对象一样操作它

当close方法被调用的时候,这个buffer会被释放

1
2
3
4
5
6
7
8
from io import BytesIO # 内存中构建
bio = BytesIO()
print(bio.readable(), bio.writable(), bio.seekable()) #True True True
bio.write(b"luo\nPython")
bio.seek(0)
print(bio.readline()) # b'magedu\n'
print(bio.getvalue()) # 无视指针,输出全部内容 b'magedu\nPython'
bio.close()

file-like对象

类文件对象,可以像文件对象一样操作。

socket对象,输入输出对象(stdin、stdout)都是类文件对象

1
2
3
4
from sys import stdout, stderr
f = stdout
print(type(f)) #<class 'ipykernel.iostream.OutStream'>
f.write('magedu.com') #magedu.com

路径操作

os.path模块

3.4版本之前

1
2
3
4
5
6
7
8
9
10
11
from os import path
p = path.join('d:/','tmp')
print(type(p), p) #<class 'str'> d:/tmp
print(path.exists(p)) #判断是否存在该路径 True
print(path.split(p)) # (head,tail) ('d:/', 'tmp')
print(path.abspath('.')) # 打印当前的绝对路径 C:\Users\vampire\python
p = path.join('D:/', p, 'test.txt') # 'd:/tmp\\test.txt'
print(path.dirname(p)) # 目录名
print(path.basename(p)) #基名,就是文件名
print(path.splitdrive(p)) #二元组 ('d:', '/tmp\\test.txt')

1
2
3
4
5
6
7
8
9
10
11
12
p1 = path.abspath(".")  #“文件路径”
print(p1, path.basename(p1))
while p1 != path.dirname(p1):
p1 = path.dirname(p1)
print(p1, path.basename(p1))

​```
C:\Users\vampire\python python
C:\Users\vampire vampire
C:\Users Users
C:\
​```
pathlib模块

提供Path对象来操作。包括目录和文件。

导入模块:from pathlib import Path

目录操作初始化

1
2
3
4
p = Path() # 当前目录    WindowsPath('.')
p.absolute()# WindowsPath('C:/Users/vampire/python')
p = Path('a','b','c/d') # 当前目录下的 WindowsPath('C:/Users/vampire/python/a/b/c/d')
p = Path('/etc') # 根下的etc目录

路径拼接和分解

操作符/

Path对象 / Path对象
Path对象 / 字符串 或者 字符串 / Path对象

分解

parts属性,可以返回路径中的每一个部分

1
2
p3.absolute()   #WindowsPath('C:/Users/vampire/python/c/a')
p3.absolute().parts #('C:\\', 'Users', 'vampire', 'python', 'c', 'a')
joinpath

joinpath(*other) 连接多个字符串到Path对象中

1
2
3
4
5
6
7
8
9
10
11
p = Path()   #   WindowsPath('.')
p = p / 'a' # WindowsPath('a')
p.absolute() # WindowsPath('C:/Users/vampire/python/a')
p1 = 'b' / p # WindowsPath('C:/Users/vampire/python/b/a')
p2 = Path('c') # WindowsPath('C:/Users/vampire/python/c')
p2.absolute() # WindowsPath('C:/Users/vampire/python/c')
p3 = p2 / p1 # WindowsPath('c/b/a')
p3.absolute() # WindowsPath('C:/Users/vampire/python/c/b/a')
print(p3.parts) #
p3.absolute().parts # ('C:\\', 'Users', 'vampire', 'python', 'c', 'b', 'a')
p3.joinpath('etc','init.d',Path('httpd'))
获取路径

str 获取路径字符串

bytes 获取路径字符串的bytes

1
2
3
4
p = Path('/etc')
print(str(p), bytes(p))

# \etc b'\\etc'
父目录

parent 目录的逻辑父目录

parents 父目录序列,索引0是直接的父

1
2
3
4
5
6
7
8
9
10
p = Path('/a/b/c/d')
print(p.absolute()) #C:\a\b\c\d
print(p.parent.parent) #\a\b
for x in p.parents:
print(x)

#\a\b\c
#\a\b
#\a
#\
目录的组合部分

name、stem、suffix、suffixes、with_suffix(suffix)、with_name(name)

name 目录的最后一个部分

suffix 目录中最后一个部分的扩展名

stem 目录最后一个部分,没有后缀

suffixes 返回多个扩展名列表

with_suffix(suffix) 有扩展名则替换,无则补充扩展名

with_name(name) 替换目录最后一个部分并返回一个新的路径

1
2
3
4
5
6
7
8
9
p = Path('mysqlinstall/mysql.tar.gz')
print(p.name) #mysql.tar.gz
print(p.suffix) #.gz
print(p.suffixes) # ['.tar', '.gz']
print(p.stem) # mysql.tar
print(p.with_name('mysql-5.tgz')) #\mysqlinstall\mysql-5.tgz
print(p.with_suffix('.png')) #\mysqlinstall\mysql.tar.png
p = Path('README') # README
print(p.with_suffix('.txt')) # README.txt
判断方法

is_dir() 是否是目录,目录存在返回True

is_file() 是否是普通文件,文件存在返回True

is_symlink() 是否是软链接

is_socket() 是否是socket文件

is_block_device() 是否是块设备

is_char_device() 是否是字符设备

is_absolute() 是否是绝对路径

resolve() 返回一个新的路径,这个新路径就是当前Path对象的绝对路径,如果是软链接则直接被解析

absolute() 获取绝对路径

exists() 目录或文件是否存在

rmdir() 删除空目录。没有提供判断目录为空的方法

touch(mode=0o666, exist_ok=True) 创建一个文件

as_uri() 将路径返回成URI,例如’file:///etc/passwd’

mkdir(mode=0o777, parents=False, exist_ok=False)

  • parents,是否创建父目录,True等同于mkdir -p;False时,父目录不存在,则抛出FileNotFoundError

  • exist_ok参数,在3.5版本加入。False时,路径存在,抛出FileExistsError;True时,FileExistsError被忽略

iterdir() 迭代当前目录

匹配

match(pattern)

模式匹配,成功返回True。

1
2
3
4
5
6
Path('a/b.py').match('*.py') # True
Path('/a/b/c.py').match('b/*.py') # True
Path('/a/b/c.py').match('a/*.py') # False
Path('/a/b/c.py').match('a/*/*.py') # True
Path('/a/b/c.py').match('a/**/*.py') # True
Path('/a/b/c.py').match('**/*.py') # True

stat() 相当于stat命令 ,lstat() 同stat(),但如果是符号链接,则显示符号链接本身的文件信息

pathlib模块下的文件操作

Path.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None)

使用的方法类似内建函数open,返回一个文件对象。

3.5增加的新函数

Path.read_bytes()

以’rb’读取路径对应文件,并返回二进制流。看源码

Path.read_text(encoding=None, errors=None)

以’rt’方式读取路径对应文件,返回文本。

Path.write_bytes(data)

以’wb’方式写入数据到路径对应文件。

Path.write_text(data, encoding=None, errors=None)

以’wt’方式写入字符串到路径对应文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
p = Path('my_binary_file')
p.write_bytes(b'Binary file contents')
p.read_bytes() # b'Binary file contents'

p = Path('my_text_file')
p.write_text('Text file contents')
p.read_text() # 'Text file contents'


from pathlib import Path
p = Path('o:/test.py')
p.write_text('hello python')
print(p.read_text())
with p.open() as f:
print(f.read(5))

csv文件

逗号分隔值Comma-Separated Values。

CSV 是一个被行分隔符、列分隔符划分成行和列的文本文件。

CSV 不指定字符编码。

行分隔符为\r\n,最后一行可以没有换行符

列分隔符常为逗号或者制表符。

每一行称为一条记录record

字段可以使用双引号括起来,也可以不使用。如果字段中出现了双引号、逗号、换行符必须使用双引号括起来。如果字段的值是双引号,使用两个双引号表示一个转义。

表头可选,和字段列对齐就行了。

手动生成csv文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pathlib import Path

p = Path('D:/tmp/test.csv')
parent = p.parent
if not parent.exists():
parent.mkdir(parents=True,exist_ok =True) #exist_ok 用在python3.5之后,如果文件目录存在,True则压制异常。
csv_body = '''\
id,name,age,comment
1,zs,18,"I'm 18"
2,ls,20,"this is a ""test"" string."
3,ww,23,"你好

计算机
"
'''
p.write_text(csv_body)

csv模块
1
def reader(iterable, dialect='excel', *args, **kwargs)

返回一个reader对象,是一个行迭代器

默认使用excel方言,如下:

  • delimiter 列分隔符,逗号

  • lineterminator 行分隔符\r\n

  • quotechar 字段的引用符号,缺省为”双引号

  • 双引号的处理

    • doublequote 双引号的处理,默认为True。如果碰到数据中有双引号,而quotechar也是双引号,True则使用2个双引号表示,False表示使用转义字符将作为双引号的前缀。
    • escapechar 一个转义字符,默认为None
    • writer = csv.writer(f, doublequote=False, escapechar=‘@’) 遇到双引号,则必须提供转义字符
  • quoting 指定双引号的规则

    • QUOTE_ALL 所有字段

    • QUOTE_MINIMAL特殊字符字段,Excel方言使用该规则

    • QUOTE_NONNUMERIC非数字字段

    • QUOTE_NONE都不使用引号。

1
def writer(fileobj, dialect='excel', *args, **kwargs)

返回DictWriter实例,主要的方法有writerow,writerows。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import csv

p = Path('d://tmp/tesr.csv')
with open(str(p)) as f:
reader = csv.reader(f) #返回一个迭代对象
print(next(reader)) #不回头
print(next(reader))
for line in reader:
print(line)

rows = [
[4,'tom',22,'tom'],
(5,'jerry',24,'jerry'),
(6,'justin',22,'just\t"in'),
"abcdefghi",
((1,),(2,))
]
row = rows[0]

with open(str(p), 'a',newline="") as f: #newline为了不换行
writer = csv.writer(f)
writer.writerow(row) #一次写一条
writer.writerows(rows) #将所有的一次写入

ini文件

一般作为配置文件。

ini文件:

1
2
3
4
5
6
7
8
9
10
11
[DEFAULT]
a = test

[mysql]
default-character-set=utf8

[mysqld]
datadir =/dbserver/data
port = 33060
character-set-server=utf8
sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES

中括号里面的部分称为section,译作节、区、段。

每一个section内,都是key=value形成的键值对,key称为option选项。

这里的DEFAULT是缺省section的名字,必须大写。

configparser模块

configparser模块的ConfigParser类就是用来操作。

可以将section当做key,section存储着键值对组成的字典,可以把ini配置文件当做一个嵌套的字典。默认使用的是有序字典。

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
read(filenames, encoding=None) 
#读取ini文件,可以是单个文件,也可以是文件列表。可以指定文件编码。
sections() #返回section列表。缺省section不包括在内。
add_section(section_name) #增加一个section。

has_section(section_name) #判断section是否存在
options(section) #返回section的所有option,会追加缺省section的option

has_option(section, option) #判断section是否存在这个option
get(section, option, *, raw=False, vars=None[, fallback])

#从指定的段的选项上取值,如果找到返回,如果没有找到就去找DEFAULT段有没有。

getint(section, option, *, raw=False, vars=None[, fallback])
getfloat(section, option, *, raw=False, vars=None[, fallback])
getboolean(section, option, *, raw=False, vars=None[, fallback])
#上面3个方法和get一样,返回指定类型数据。

items(raw=False, vars=None)
items(section, raw=False, vars=None)
#没有section,则返回所有section名字及其对象;如果指定section,则返回这个指定的section的键值对组成二元组。

set(section, option, value)
#section存在的情况下,写入option=value,要求option、value必须是字符串。

remove_section(section)
#移除section及其所有option

remove_option(section, option)
#移除section下的option。

write(fileobject, space_around_delimiters=True)
#将当前config的所有内容写入fileobject中,一般open函数使用w模式。

代码示例:

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
from configparser import ConfigParser
from pathlib import Path

filename = Path("d://tmp/mysql.ini")
newfilename = Path("d://tmp/mysql111.ini")

cfg = ConfigParser()
read_ok = cfg.read(str(filename))
print(read_ok)
print(cfg.sections())
print(cfg.has_section("mysql"))
print("-"*30)

for k,v in cfg.items(): #未指定section
print(k,type(k))
print(v,type(v))
print(cfg.items(k))
print("~~~~~~~~~~~~~~~~~~")
print("-"*30)


for k,v in cfg.items("mysqld"): #指定section
print(k,type(k))
print(v,type(v))
print("~~~~~~~~~~")

tmp = cfg.get("mysqld","port")
print(tmp, type(tmp))
print(cfg.get("mysqld", "a"))
print(cfg.get("mysqld", "python" , fallback= "linux")) #按照类型,fallbac:给与缺省值

tmp = cfg.getint("mysqld", "port")
print(type(tmp), tmp)

cfg.add_section("test")
cfg.set("test","test1","1")
cfg.set("test","test2","2")
with open(newfilename,"w+",newline="") as f:
cfg.write(f)

print(cfg.getint("test" , "test1"))

cfg.remove_option("test", "test1")
# cfg.remove_section("test")
# print("x" in cfg["test2"])
#字典操作
cfg["test3"] = {"c":"1000"} #没有落地,在内存中修改

print("x" in cfg["test"])
print("c" in cfg["test3"])

# 其他内部方式
print(cfg._dict) # 返回默认的字典类型,默认使用有序字典

for k, v in cfg._sections.items():
print(k, v)

for k,v in cfg._sections['mysqld'].items():
print(k,v)
#重新写入文件
with open(newfilename, 'w') as f:
cfg.write(f)

序列化和反序列化

要设计一套协议,按照某种规则,把内存中数据保存到文件中,文件是一个字节序列,所以必须把数据转换成<font color=red >字节</font>序列,输出到文件。这就是序列化。反之,从文件的字节序列恢复到内存。就是反序列化。
  1. serialization:序列化
    将内存中对象存储下来,变成一个个字节 –> 二进制

  2. deseiralization:反序列化
    将文件中的一个个字节恢复成内存中对象 <–二进制

序列化保存到文件就是持久化,可以将数据序列化后持久化,或者网络传输;也可以将文件中或者网络中接收到的字节序列反序列化。

pickle库

python中的序列化,反序列化模块。

dumps 对象序列化为bytes对象 dump 对象序列化到文件对象,就是存入文件

loads 从bytes对象反序列化 load 对象反序列化,从文件读取数据

序列化的应用

一般来说,本地序列化的情况,应用较少。大多数场景都应用在网络传输中。

将数据序列化后通过网络传输到远程节点,远程服务器上的服务将接收到的数据反序列化后,就可以使用了。

但是,要注意一点,远程接收端,反序列化时必须有对应的数据类型,否则就会报错。尤其是自定义类,必须远程的有一致的定义。

现在,大多数项目,都不是单机的,也不是单服务的。需要通过网络将数据传送到其他节点上去,这就需要大量的序列化、反序列化过程。

但是,问题是,Python程序之间还可以都是用pickle解决序列化、反序列化,如果是跨平台、跨语言、跨协议pickle就不太适合了,就需要公共的协议。例如XML、Json、Protocol Buffer等。

不同的协议,效率不同、学习曲线不同,适用不同场景,要根据不同的情况分析选型。

Json
Json(JavaScript Object Notation,JS对象标记)是一种轻量级的数据交换格式。它基于 ECMAScript (w3c组织制定的JS规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。网址: http://json.org/

Json的数据类型

值:双引号引起来的字符串,数值,true和false,null,对象,数组,这些都是值

字符串:有正负,有整数,浮点数。

对象:无序的键值对的集合,格式{key:value…},key必须是一个字符串,需要双引号包围这个字符串,value可以是任意合法的值。

数组:有序的值的集合 格式[val1,,,,valn]

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"person": [
{
"name": "tom",
"age": 18
},
{
"name": "jerry",
"age": 16
}
],
"total": 2
}

Json模块

Python支持少量内建数据类型到Json类型的转换

Python类型 Json类型
True true
False false
None null
str string
int integer
float float
list array
dict object

常用方法

Python类型 Json类型
dumps Json编码
dump Json编码并存入文件
loads Json解码
load Json解码,从文件读取数据

一般Json编码的数据很少落地,数据都是通过网络传输,传输的时候,要考虑压缩它,节省流量。本质来说它就是个文本,就是个字符串。

MessagePack

MessagePack是一个基于二进制高效的对象序列化类库,可用于跨语言通信。

它可以像JSON那样,在许多种语言之间交换结构对象。

兼容 json和pickle。

MessagePack简单易用,高效压缩,支持语言丰富。

所以,用它序列化也是一种很好的选择。

安装:$pip install msgpack-python

常用方法:

packb 序列化对象。提供了dumps来兼容pickle和json。

unpackb 反序列化对象。提供了loads来兼容。

pack 序列化对象保存到文件对象。提供了dump来兼容。

unpack 反序列化对象保存到文件对象。提供了load来兼容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pickle
import json
import msgpack

d = {"person":[{"name":"tom","age":18},{"name":"jerry","age":16}],"total":2}
j = json.dumps(d)
print(j, type(j), len(j)) # 请注意引号的变化
print(len(j.replace(' ',''))) # 72 bytes 注意这样替换的压缩是不对的
print("-"*30)
p = pickle.dumps(d)
print(p)
print(len(p)) # 101 bytes
print("-"*30)
m = msgpack.dumps(d)
print(m)
print(len(m)) # 48 bytes
print("-"*30)
u = msgpack.unpackb(m)
print(type(u), u)
u = msgpack.loads(m, encoding='utf-8')
print(type(u), u)
1
2
3
4
5
6
7
8
9
10
11
{"person": [{"name": "tom", "age": 18}, {"name": "jerry", "age": 16}], "total": 2} <class 'str'> 82
72
------------------------------
b'\x80\x03}q\x00(X\x06\x00\x00\x00personq\x01]q\x02(}q\x03(X\x04\x00\x00\x00nameq\x04X\x03\x00\x00\x00tomq\x05X\x03\x00\x00\x00ageq\x06K\x12u}q\x07(h\x04X\x05\x00\x00\x00jerryq\x08h\x06K\x10ueX\x05\x00\x00\x00totalq\tK\x02u.'
101
------------------------------
b'\x82\xa6person\x92\x82\xa4name\xa3tom\xa3age\x12\x82\xa4name\xa5jerry\xa3age\x10\xa5total\x02'
48
------------------------------
<class 'dict'> {b'person': [{b'name': b'tom', b'age': 18}, {b'name': b'jerry', b'age': 16}], b'total': 2}
<class 'dict'> {'person': [{'name': 'tom', 'age': 18}, {'name': 'jerry', 'age': 16}], 'total': 2}

文件IO常用操作

一般说IO操作,指的是文件IO。

把文件存储到磁盘上的这个过程,叫做落地。

column column
open 打开
read 读取
write 写入
close 关闭
readline 行读取
readlines 多行读取
seek 文件指针操作
tell 指针位置
open打开操作
1
2
3
4
5
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True,opener=None)

f = open("file名字") #文件对象
print(f.read()) #读取文件
f.close() #关闭文件

打开一个文件,返回一个文件对象(流对象)和文件描述符。打开文件失败,则返回异常。
基本使用: 创建一个文件test,然后打开它,用完关闭。

文件操作中最常用的就是读和写。文件访问的模式有两种:文本模式和二进制模式。

注:windows中使用codepage代码页。可以认为每一个代码页就是一张编码表 cp936和gbk等价。

mode模式
描述字符
r 缺省的,表示只读打开
w 只写打开,有的话就清除重新写
x 创建并写入一个新文件
a 写入打开,如果文件存在,则追加
b 二进制模式
t 缺省的,文本模式
+ 读写打开一个文件,给原来只读、只写的增加缺失的功能

open默认是只读模式r打开已经存在的文件。

  • r 只读打开文件,如果使用write方法,会抛异常。 如果文件不存在,抛出FileNotFoundError异常。

  • w 表示只写方式打开,如果读取则抛出异常 如果文件不存在,则直接创建文件 如果文件存在,则清空文件内容。

  • x 文件不存在,创建文件,并只写方式打开,文件存在,抛出FileExistsError异常。

  • a 文件存在,只写打开,追加内容 文件不存在,则创建后,只写打开,追加内容

  • r是只读,wxa都是只写。 wxa都可以产生新文件,w不管文件存在与否,都会生成全新内容的文件;a不管文件是否存在,都能在打开的文件尾部追加;x必须要求文件事先不存在,自己造一个新文件。

  • +为r、w、a、x提供缺失的读或写功能,但是,获取文件对象依旧按照r、w、a、x自己的特征。 +不能单独使用,可以认为它是为前面的模式字符做增强功能的。

t和b:

  • 文本模式t 字符流,将文件的字节按照某种字符编码理解,按照字符操作。open的默认mode就是rt。

  • 二进制模式b 字节流,将文件就按照字节理解,与字符编码无关。二进制模式操作时,字节操作使用bytes类型。

t/b不能单独存在,要和a/w/x/r配合使用。

seek文件指针

文件指针,指向当前字节位置。

mode = r,指针起始在0 ,mode = a 指针起始在EOF。

tell():显示指针当前位置。

seek(offset[, whence]) 移动文件指针位置。offest偏移多少字节,whence从哪里开始。

  • 文本模式下 whence 0 缺省值,表示从头开始,offset只能正整数 whence 1 表示从当前位置,offset只接受0,whence 2 表示从EOF开始,offest只接受0

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 文本模式
    f = open('test4','r+')
    f.tell() # 起始
    f.read()
    f.tell() # EOF
    f.seek(0) # 起始
    f.read()
    f.seek(2,0)
    f.read()
    f.seek(2,0)
    f.seek(2,1) # offset必须为0
    f.seek(2,2) # offset必须为0
    f.close()
  • 二进制模式下 whence 0 缺省值,表示从头开始,offest只能正整数 whence 1表示从当前位置,offest可正可负,whence 2 表示从EOF开始,offest可正可负。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 二进制模式
    f = open('test4','rb+')
    f.tell() # 起始
    f.read()
    f.tell() # EOF
    f.write(b'abc')
    f.seek(0) # 起始
    f.seek(2,1) # 从当前指针开始,向后2
    f.read()

    f.seek(-2,2) # 从EOF开始,向前2
    f.read()
    f.seek(-20,2) # OSError
    f.close()
  • 二进制模式支持任意起点的偏移,从头、从尾、从中间位置开始。 向后seek可以超界,但是向前seek的时候,不能超界,否则抛异常。

buffering缓冲区

-1 表示使用缺省大小的buffer。如果是二进制模式,使io.DEFAULT_BUFFER_SIZE值,默认是4096或者8192。如果是文本模式,如果是终端设备,是行缓存方式,如果不是,则使用二进制模式的策略。

  • 0 只在二进制模式使用,表示关buffer
  • 1 只在文本模式使用,表示使用行缓冲。意思就是见到换行符就flush
  • 大于1 用于指定buffer的大小

buffer 缓冲区

缓冲区一个内存空间,一般来说是一个FIFO队列,到缓冲区满了或者达到阈值,数据才会flush到磁盘。

flush() 将缓冲区数据写入磁盘 close() 关闭前会调用flush()。

io.DEFAULT_BUFFER_SIZE 缺省缓冲区大小,字节。

buffering 说明
buffering = -1 t和b,都是io.DEFAULT_BUFFER_SIZE
buffering = 0 b 关闭缓冲区
t 不支持
buffering = 1 b 就一个字节
t 行缓冲,遇到换行符才flush
buffering > 1 b模式表示行缓冲大小。缓冲区的值可以超过io.DEFAULT_BUFFER_SIZE,直到设定的值超出后才把缓冲区flush 。
t模式,是io.DEFAULT_BUFFER_SIZE字节,flush完后把当前字符串也写入磁盘

一般来说:

  1. 文本模式,一般都用默认缓冲区大小
  2. 二进制模式,是一个个字节的操作,可以指定buffer的大小
  3. 一般来说,默认缓冲区大小是个比较好的选择,除非明确知道,否则不调整它
  4. 一般编程中,明确知道需要写磁盘了,都会手动调用一次flush,而不是等到自动flush或者close的时候
其他参数

编码:windows下缺省GBK(0xB0A1),Linux下缺省UTF-8(0xE5 95 8A)

errors :编码错误将被捕获 None和strict表示有编码错误将抛出ValueError异常;ignore表示忽略

newline:文本模式中,换行的转换。可以为None、’’ 空串、’\r’、’\n’、’\r\n’ 。

  • None表示’\r’、’\n’、’\r\n’都被转换为’\n’;
  • ‘’ 表示不会自动转换通用换行符;其它合法字符表示换行符就是指定字符,就会按照指定字符分行写。
  • ‘\n’或’’表示’\n’不替换;其它合法字符表示’\n’会被替换为指定的字符

closefd:关闭文件描述符,True表示关闭它。False会在文件关闭后保持这个描述符。fileobj.fileno()查看。

文件描述符:Linux一切皆文件,文件打开后都会有一个位于的文件描述符,在计算机系统中是一个有限的资源。0,1,2,标准输入,标准输出,标准错误输出。

对于类似于文件对象的IO对象,一般来说都需要在不使用的时候关闭、注销,以释放资源。
IO被打开的时候,会获得一个文件描述符。计算机资源是有限的,所以操作系统都会做限制。就是为了保护计算机的资源不要被完全耗尽,计算资源是共享的,不是独占的。

一般情况下,除非特别明确的知道资源情况,否则不要提高资源的限制值来解决问题。

read()

read(size=-1)
size表示读取的多少个字符或字节;负数或者None表示读取到EOF

readline(size=-1)
一行行读取文件内容。size设置一次能读取行内几个字符或字节。

readlines(hint=-1)
读取所有行的列表。指定hint则返回指定的行数。

write()

write(s),把字符串s写入到文件中并返回字符的个数 writelines(lines),将字符串列表写入文件。

close()

flush并关闭文件对象。
文件已经关闭,再次关闭没有任何效果。

其他
名称 说明
seekable() 是否可seek
readable() 是否可读
writeable() 是否可写
closed() 是否已经关闭
上下文管理

1、异常处理
当出现异常的时候,拦截异常。但是,因为很多代码都可能出现OSError异常,还不好判断异常就是应为资源限制产生的。

1
2
3
4
5
f = open('test')
try:
f.write("abc") # 文件只读,写入失败
finally:
f.close() # 这样才行

使用finally可以保证打开的文件可以被关闭。

上下文管理

  1. 使用with … as 关键字

  2. 上下文管理的语句块并不会开启新的作用域

  3. with语句块执行完的时候,会自动关闭文件对象

StringIO操作

io模块中的类

from io import StringIO

内存中,开辟的一个文本模式的buffer,可以像文件对象一样操作它

当close方法被调用的时候,这个buffer会被释放

1
2
3
4
5
6
7
8
9
from io import StringIO
# 内存中构建
sio = StringIO() # 像文件对象一样操作
print(sio.readable(), sio.writable(), sio.seekable())# True True True
sio.write("luo\nPython")
sio.seek(0)
print(sio.readline()) #magedu
print(sio.getvalue()) # 无视指针,输出全部内容 magedu Python
sio.close()

好处

一般来说,磁盘的操作比内存的操作要慢得多,内存足够的情况下,一般的思路是少落地,减少磁盘IO的过程,可以大大的提高程序的运行效率。

BytesIO操作

io模块中的类

from io import BytesIO

内存中,开辟的一个二进制模式的buffer,可以像文件对象一样操作它

当close方法被调用的时候,这个buffer会被释放

1
2
3
4
5
6
7
8
from io import BytesIO # 内存中构建
bio = BytesIO()
print(bio.readable(), bio.writable(), bio.seekable()) #True True True
bio.write(b"luo\nPython")
bio.seek(0)
print(bio.readline()) # b'magedu\n'
print(bio.getvalue()) # 无视指针,输出全部内容 b'magedu\nPython'
bio.close()

file-like对象

类文件对象,可以像文件对象一样操作。

socket对象,输入输出对象(stdin、stdout)都是类文件对象

1
2
3
4
from sys import stdout, stderr
f = stdout
print(type(f)) #<class 'ipykernel.iostream.OutStream'>
f.write('magedu.com') #magedu.com

路径操作

os.path模块

3.4版本之前

1
2
3
4
5
6
7
8
9
10
11
from os import path
p = path.join('d:/','tmp')
print(type(p), p) #<class 'str'> d:/tmp
print(path.exists(p)) #判断是否存在该路径 True
print(path.split(p)) # (head,tail) ('d:/', 'tmp')
print(path.abspath('.')) # 打印当前的绝对路径 C:\Users\vampire\python
p = path.join('D:/', p, 'test.txt') # 'd:/tmp\\test.txt'
print(path.dirname(p)) # 目录名
print(path.basename(p)) #基名,就是文件名
print(path.splitdrive(p)) #二元组 ('d:', '/tmp\\test.txt')

1
2
3
4
5
6
7
8
9
10
11
12
p1 = path.abspath(".")  #“文件路径”
print(p1, path.basename(p1))
while p1 != path.dirname(p1):
p1 = path.dirname(p1)
print(p1, path.basename(p1))

​```
C:\Users\vampire\python python
C:\Users\vampire vampire
C:\Users Users
C:\
​```
pathlib模块

提供Path对象来操作。包括目录和文件。

导入模块:from pathlib import Path

目录操作初始化

1
2
3
4
p = Path() # 当前目录    WindowsPath('.')
p.absolute()# WindowsPath('C:/Users/vampire/python')
p = Path('a','b','c/d') # 当前目录下的 WindowsPath('C:/Users/vampire/python/a/b/c/d')
p = Path('/etc') # 根下的etc目录

路径拼接和分解

操作符/

Path对象 / Path对象
Path对象 / 字符串 或者 字符串 / Path对象

分解

parts属性,可以返回路径中的每一个部分

1
2
p3.absolute()   #WindowsPath('C:/Users/vampire/python/c/a')
p3.absolute().parts #('C:\\', 'Users', 'vampire', 'python', 'c', 'a')
joinpath

joinpath(*other) 连接多个字符串到Path对象中

1
2
3
4
5
6
7
8
9
10
11
p = Path()   #   WindowsPath('.')
p = p / 'a' # WindowsPath('a')
p.absolute() # WindowsPath('C:/Users/vampire/python/a')
p1 = 'b' / p # WindowsPath('C:/Users/vampire/python/b/a')
p2 = Path('c') # WindowsPath('C:/Users/vampire/python/c')
p2.absolute() # WindowsPath('C:/Users/vampire/python/c')
p3 = p2 / p1 # WindowsPath('c/b/a')
p3.absolute() # WindowsPath('C:/Users/vampire/python/c/b/a')
print(p3.parts) #
p3.absolute().parts # ('C:\\', 'Users', 'vampire', 'python', 'c', 'b', 'a')
p3.joinpath('etc','init.d',Path('httpd'))
获取路径

str 获取路径字符串

bytes 获取路径字符串的bytes

1
2
3
4
p = Path('/etc')
print(str(p), bytes(p))

# \etc b'\\etc'
父目录

parent 目录的逻辑父目录

parents 父目录序列,索引0是直接的父

1
2
3
4
5
6
7
8
9
10
p = Path('/a/b/c/d')
print(p.absolute()) #C:\a\b\c\d
print(p.parent.parent) #\a\b
for x in p.parents:
print(x)

#\a\b\c
#\a\b
#\a
#\
目录的组合部分

name、stem、suffix、suffixes、with_suffix(suffix)、with_name(name)

name 目录的最后一个部分

suffix 目录中最后一个部分的扩展名

stem 目录最后一个部分,没有后缀

suffixes 返回多个扩展名列表

with_suffix(suffix) 有扩展名则替换,无则补充扩展名

with_name(name) 替换目录最后一个部分并返回一个新的路径

1
2
3
4
5
6
7
8
9
p = Path('mysqlinstall/mysql.tar.gz')
print(p.name) #mysql.tar.gz
print(p.suffix) #.gz
print(p.suffixes) # ['.tar', '.gz']
print(p.stem) # mysql.tar
print(p.with_name('mysql-5.tgz')) #\mysqlinstall\mysql-5.tgz
print(p.with_suffix('.png')) #\mysqlinstall\mysql.tar.png
p = Path('README') # README
print(p.with_suffix('.txt')) # README.txt
判断方法

is_dir() 是否是目录,目录存在返回True

is_file() 是否是普通文件,文件存在返回True

is_symlink() 是否是软链接

is_socket() 是否是socket文件

is_block_device() 是否是块设备

is_char_device() 是否是字符设备

is_absolute() 是否是绝对路径

resolve() 返回一个新的路径,这个新路径就是当前Path对象的绝对路径,如果是软链接则直接被解析

absolute() 获取绝对路径

exists() 目录或文件是否存在

rmdir() 删除空目录。没有提供判断目录为空的方法

touch(mode=0o666, exist_ok=True) 创建一个文件

as_uri() 将路径返回成URI,例如’file:///etc/passwd’

mkdir(mode=0o777, parents=False, exist_ok=False)

  • parents,是否创建父目录,True等同于mkdir -p;False时,父目录不存在,则抛出FileNotFoundError

  • exist_ok参数,在3.5版本加入。False时,路径存在,抛出FileExistsError;True时,FileExistsError被忽略

iterdir() 迭代当前目录

匹配

match(pattern)

模式匹配,成功返回True。

1
2
3
4
5
6
Path('a/b.py').match('*.py') # True
Path('/a/b/c.py').match('b/*.py') # True
Path('/a/b/c.py').match('a/*.py') # False
Path('/a/b/c.py').match('a/*/*.py') # True
Path('/a/b/c.py').match('a/**/*.py') # True
Path('/a/b/c.py').match('**/*.py') # True

stat() 相当于stat命令 ,lstat() 同stat(),但如果是符号链接,则显示符号链接本身的文件信息

pathlib模块下的文件操作

Path.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None)

使用的方法类似内建函数open,返回一个文件对象。

3.5增加的新函数

Path.read_bytes()

以’rb’读取路径对应文件,并返回二进制流。看源码

Path.read_text(encoding=None, errors=None)

以’rt’方式读取路径对应文件,返回文本。

Path.write_bytes(data)

以’wb’方式写入数据到路径对应文件。

Path.write_text(data, encoding=None, errors=None)

以’wt’方式写入字符串到路径对应文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
p = Path('my_binary_file')
p.write_bytes(b'Binary file contents')
p.read_bytes() # b'Binary file contents'

p = Path('my_text_file')
p.write_text('Text file contents')
p.read_text() # 'Text file contents'


from pathlib import Path
p = Path('o:/test.py')
p.write_text('hello python')
print(p.read_text())
with p.open() as f:
print(f.read(5))

csv文件

逗号分隔值Comma-Separated Values。

CSV 是一个被行分隔符、列分隔符划分成行和列的文本文件。

CSV 不指定字符编码。

行分隔符为\r\n,最后一行可以没有换行符

列分隔符常为逗号或者制表符。

每一行称为一条记录record

字段可以使用双引号括起来,也可以不使用。如果字段中出现了双引号、逗号、换行符必须使用双引号括起来。如果字段的值是双引号,使用两个双引号表示一个转义。

表头可选,和字段列对齐就行了。

手动生成csv文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pathlib import Path

p = Path('D:/tmp/test.csv')
parent = p.parent
if not parent.exists():
parent.mkdir(parents=True,exist_ok =True) #exist_ok 用在python3.5之后,如果文件目录存在,True则压制异常。
csv_body = '''\
id,name,age,comment
1,zs,18,"I'm 18"
2,ls,20,"this is a ""test"" string."
3,ww,23,"你好

计算机
"
'''
p.write_text(csv_body)

csv模块
1
def reader(iterable, dialect='excel', *args, **kwargs)

返回一个reader对象,是一个行迭代器

默认使用excel方言,如下:

  • delimiter 列分隔符,逗号

  • lineterminator 行分隔符\r\n

  • quotechar 字段的引用符号,缺省为”双引号

  • 双引号的处理

    • doublequote 双引号的处理,默认为True。如果碰到数据中有双引号,而quotechar也是双引号,True则使用2个双引号表示,False表示使用转义字符将作为双引号的前缀。
    • escapechar 一个转义字符,默认为None
    • writer = csv.writer(f, doublequote=False, escapechar=‘@’) 遇到双引号,则必须提供转义字符
  • quoting 指定双引号的规则

    • QUOTE_ALL 所有字段

    • QUOTE_MINIMAL特殊字符字段,Excel方言使用该规则

    • QUOTE_NONNUMERIC非数字字段

    • QUOTE_NONE都不使用引号。

1
def writer(fileobj, dialect='excel', *args, **kwargs)

返回DictWriter实例,主要的方法有writerow,writerows。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import csv

p = Path('d://tmp/tesr.csv')
with open(str(p)) as f:
reader = csv.reader(f) #返回一个迭代对象
print(next(reader)) #不回头
print(next(reader))
for line in reader:
print(line)

rows = [
[4,'tom',22,'tom'],
(5,'jerry',24,'jerry'),
(6,'justin',22,'just\t"in'),
"abcdefghi",
((1,),(2,))
]
row = rows[0]

with open(str(p), 'a',newline="") as f: #newline为了不换行
writer = csv.writer(f)
writer.writerow(row) #一次写一条
writer.writerows(rows) #将所有的一次写入

ini文件

一般作为配置文件。

ini文件:

1
2
3
4
5
6
7
8
9
10
11
[DEFAULT]
a = test

[mysql]
default-character-set=utf8

[mysqld]
datadir =/dbserver/data
port = 33060
character-set-server=utf8
sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES

中括号里面的部分称为section,译作节、区、段。

每一个section内,都是key=value形成的键值对,key称为option选项。

这里的DEFAULT是缺省section的名字,必须大写。

configparser模块

configparser模块的ConfigParser类就是用来操作。

可以将section当做key,section存储着键值对组成的字典,可以把ini配置文件当做一个嵌套的字典。默认使用的是有序字典。

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
read(filenames, encoding=None) 
#读取ini文件,可以是单个文件,也可以是文件列表。可以指定文件编码。
sections() #返回section列表。缺省section不包括在内。
add_section(section_name) #增加一个section。

has_section(section_name) #判断section是否存在
options(section) #返回section的所有option,会追加缺省section的option

has_option(section, option) #判断section是否存在这个option
get(section, option, *, raw=False, vars=None[, fallback])

#从指定的段的选项上取值,如果找到返回,如果没有找到就去找DEFAULT段有没有。

getint(section, option, *, raw=False, vars=None[, fallback])
getfloat(section, option, *, raw=False, vars=None[, fallback])
getboolean(section, option, *, raw=False, vars=None[, fallback])
#上面3个方法和get一样,返回指定类型数据。

items(raw=False, vars=None)
items(section, raw=False, vars=None)
#没有section,则返回所有section名字及其对象;如果指定section,则返回这个指定的section的键值对组成二元组。

set(section, option, value)
#section存在的情况下,写入option=value,要求option、value必须是字符串。

remove_section(section)
#移除section及其所有option

remove_option(section, option)
#移除section下的option。

write(fileobject, space_around_delimiters=True)
#将当前config的所有内容写入fileobject中,一般open函数使用w模式。

代码示例:

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
from configparser import ConfigParser
from pathlib import Path

filename = Path("d://tmp/mysql.ini")
newfilename = Path("d://tmp/mysql111.ini")

cfg = ConfigParser()
read_ok = cfg.read(str(filename))
print(read_ok)
print(cfg.sections())
print(cfg.has_section("mysql"))
print("-"*30)

for k,v in cfg.items(): #未指定section
print(k,type(k))
print(v,type(v))
print(cfg.items(k))
print("~~~~~~~~~~~~~~~~~~")
print("-"*30)


for k,v in cfg.items("mysqld"): #指定section
print(k,type(k))
print(v,type(v))
print("~~~~~~~~~~")

tmp = cfg.get("mysqld","port")
print(tmp, type(tmp))
print(cfg.get("mysqld", "a"))
print(cfg.get("mysqld", "python" , fallback= "linux")) #按照类型,fallbac:给与缺省值

tmp = cfg.getint("mysqld", "port")
print(type(tmp), tmp)

cfg.add_section("test")
cfg.set("test","test1","1")
cfg.set("test","test2","2")
with open(newfilename,"w+",newline="") as f:
cfg.write(f)

print(cfg.getint("test" , "test1"))

cfg.remove_option("test", "test1")
# cfg.remove_section("test")
# print("x" in cfg["test2"])
#字典操作
cfg["test3"] = {"c":"1000"} #没有落地,在内存中修改

print("x" in cfg["test"])
print("c" in cfg["test3"])

# 其他内部方式
print(cfg._dict) # 返回默认的字典类型,默认使用有序字典

for k, v in cfg._sections.items():
print(k, v)

for k,v in cfg._sections['mysqld'].items():
print(k,v)
#重新写入文件
with open(newfilename, 'w') as f:
cfg.write(f)

序列化和反序列化

要设计一套协议,按照某种规则,把内存中数据保存到文件中,文件是一个字节序列,所以必须把数据转换成<font color=red >字节</font>序列,输出到文件。这就是序列化。反之,从文件的字节序列恢复到内存。就是反序列化。
  1. serialization:序列化
    将内存中对象存储下来,变成一个个字节 –> 二进制

  2. deseiralization:反序列化
    将文件中的一个个字节恢复成内存中对象 <–二进制

序列化保存到文件就是持久化,可以将数据序列化后持久化,或者网络传输;也可以将文件中或者网络中接收到的字节序列反序列化。

pickle库

python中的序列化,反序列化模块。

dumps 对象序列化为bytes对象 dump 对象序列化到文件对象,就是存入文件

loads 从bytes对象反序列化 load 对象反序列化,从文件读取数据

序列化的应用

一般来说,本地序列化的情况,应用较少。大多数场景都应用在网络传输中。

将数据序列化后通过网络传输到远程节点,远程服务器上的服务将接收到的数据反序列化后,就可以使用了。

但是,要注意一点,远程接收端,反序列化时必须有对应的数据类型,否则就会报错。尤其是自定义类,必须远程的有一致的定义。

现在,大多数项目,都不是单机的,也不是单服务的。需要通过网络将数据传送到其他节点上去,这就需要大量的序列化、反序列化过程。

但是,问题是,Python程序之间还可以都是用pickle解决序列化、反序列化,如果是跨平台、跨语言、跨协议pickle就不太适合了,就需要公共的协议。例如XML、Json、Protocol Buffer等。

不同的协议,效率不同、学习曲线不同,适用不同场景,要根据不同的情况分析选型。

Json
Json(JavaScript Object Notation,JS对象标记)是一种轻量级的数据交换格式。它基于 ECMAScript (w3c组织制定的JS规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。网址: http://json.org/

Json的数据类型

值:双引号引起来的字符串,数值,true和false,null,对象,数组,这些都是值

字符串:有正负,有整数,浮点数。

对象:无序的键值对的集合,格式{key:value…},key必须是一个字符串,需要双引号包围这个字符串,value可以是任意合法的值。

数组:有序的值的集合 格式[val1,,,,valn]

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"person": [
{
"name": "tom",
"age": 18
},
{
"name": "jerry",
"age": 16
}
],
"total": 2
}

Json模块

Python支持少量内建数据类型到Json类型的转换

Python类型 Json类型
True true
False false
None null
str string
int integer
float float
list array
dict object

常用方法

Python类型 Json类型
dumps Json编码
dump Json编码并存入文件
loads Json解码
load Json解码,从文件读取数据

一般Json编码的数据很少落地,数据都是通过网络传输,传输的时候,要考虑压缩它,节省流量。本质来说它就是个文本,就是个字符串。

MessagePack

MessagePack是一个基于二进制高效的对象序列化类库,可用于跨语言通信。

它可以像JSON那样,在许多种语言之间交换结构对象。

兼容 json和pickle。

MessagePack简单易用,高效压缩,支持语言丰富。

所以,用它序列化也是一种很好的选择。

安装:$pip install msgpack-python

常用方法:

packb 序列化对象。提供了dumps来兼容pickle和json。

unpackb 反序列化对象。提供了loads来兼容。

pack 序列化对象保存到文件对象。提供了dump来兼容。

unpack 反序列化对象保存到文件对象。提供了load来兼容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pickle
import json
import msgpack

d = {"person":[{"name":"tom","age":18},{"name":"jerry","age":16}],"total":2}
j = json.dumps(d)
print(j, type(j), len(j)) # 请注意引号的变化
print(len(j.replace(' ',''))) # 72 bytes 注意这样替换的压缩是不对的
print("-"*30)
p = pickle.dumps(d)
print(p)
print(len(p)) # 101 bytes
print("-"*30)
m = msgpack.dumps(d)
print(m)
print(len(m)) # 48 bytes
print("-"*30)
u = msgpack.unpackb(m)
print(type(u), u)
u = msgpack.loads(m, encoding='utf-8')
print(type(u), u)
1
2
3
4
5
6
7
8
9
10
11
{"person": [{"name": "tom", "age": 18}, {"name": "jerry", "age": 16}], "total": 2} <class 'str'> 82
72
------------------------------
b'\x80\x03}q\x00(X\x06\x00\x00\x00personq\x01]q\x02(}q\x03(X\x04\x00\x00\x00nameq\x04X\x03\x00\x00\x00tomq\x05X\x03\x00\x00\x00ageq\x06K\x12u}q\x07(h\x04X\x05\x00\x00\x00jerryq\x08h\x06K\x10ueX\x05\x00\x00\x00totalq\tK\x02u.'
101
------------------------------
b'\x82\xa6person\x92\x82\xa4name\xa3tom\xa3age\x12\x82\xa4name\xa5jerry\xa3age\x10\xa5total\x02'
48
------------------------------
<class 'dict'> {b'person': [{b'name': b'tom', b'age': 18}, {b'name': b'jerry', b'age': 16}], b'total': 2}
<class 'dict'> {'person': [{'name': 'tom', 'age': 18}, {'name': 'jerry', 'age': 16}], 'total': 2}

函数


1. 函数的定义


由若干语句组成的语句块、函数名称、参数列表构成,它是组织代码的最小单元。

完成一定的功能。

函数也是对象,python把函数的默认值放在了属性中,这个属性就伴随着这个函数对象的整个生命周期。

2. 函数的作用


  • 结构化编程是对代码的最基本的封装,一般按照功能组织一段代码。
  • 封装的目的是为了复用,减少冗余代码。
  • 代码更加简洁美观,可读易懂。

3. 函数的分类:


内建函数;库函数;自建函数

4. 函数的定义、调用

定义

def语句定义函数

def 函数名(参数列表):

函数体(代码块)

[return 返回值]

定义中的参数列表成为形式参数,只是一种符号表达,简称形参

定义需要在调用前,否则会抛出NameError异常。

调用

函数定义,只是声明了一个函数,它不会被指执行,需要调用。

调用的方式,就是函数名加上小括号,括号内写上参数。

调用时写的参数时实际参数,是实实在在传入的值,简称实参

传参时位置参数要放在关键字参数前面。

参数传递:不可变类型,传递副本给函数,函数内操作不影响原始值

 可变类型,传递的是地址引用,函数内操作可能影响原始值
定义形参和传递实参时候的注意事项

  1. 参数调用时传入的参数要和定义的个数相匹配,可变参数例外
  2. 定义时,缺省参数要放在非缺省参数前。
  3. 定义时加* :可变位置参数:可以收集位置参数传入的所有参数,收集多个实参为一个tuple。可变位置参数不能用关键字传参。
  4. 形参加**:可变关键字参数,只能用关键字传参。可变关键字参数,收集的实参名称和值组成一个字典,所以可修改。
  5. 函数名也是标识符,返回值也是值,函数是可调用的对象,callable(函数名) -> True。
  6. 混合使用参数的时候,可变参数要放到参数列表的最后,普通参数要放到参数列表的最前面,可变位置参数发要放在可变关键字参数的前面。
  7. keyword-only参数:如果在一个可变位置参数后面,出现了普通参数,此时这个普通参数已经变成了一个keyword-only参数
  8. 参数列表参数一般顺序是,普通参数、缺省参数、可变位置参数、keyword-only参数(可带缺省值)、可变关键字参数。
  9. 参数解构:
    • 给函数提供实参的时候,可以在集合类型前使用*或者**,把集合类型的结构解开,提取出所有元素作为函数的实参。
    • 非字典类型使用*解构成位置参数
    • 字典类型使用**解构成关键字参数
    • 提取出来的元素数目要和参数的要求匹配,也要和参数的类型匹配。

5.函数的返回值

python函数使用return语句返回“返回值”。

所有函数都有返回值。如果没有return语句,隐式调用return None。

return语句并不一定是函数的语句块的最后一条语句

return语句只能执行一次,执行完,函数结束,当前return后面的语句就不会再运行了。所以函数一次只能返回一个值,不能返回多个值,但是可以返回容器,容器里面包含多个值。(return [1,3,5]是指明返回一个列表,是一个列表对象;return 1,3,5看似返回多个值,隐式的被python封装成一个元组)

作用:结束函数调用、返回值。

函数的嵌套

函数有可见范围。这就是作用域的概念

外层变量作用域在内层作用域可见

内部函数不能在外部直接使用,会抛NameError异常,因为它不可见。

6. 作用域


一个标识符的可见范围,这就是标识符的作用域。一般常说的是变量的作用域。

全局作用域:在整个函数运行环境中都可见。

局部作用域:在函数、类内部可见;局部变量的使用范围不能超过其所在的局部作用域。

例子:

1
2
3
4
5
6
7
8
a = 5
def foo():
a += 1

foo()
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
UnboundLocalError: local variable 'a' referenced before assignment

报错原因:

a += 1其实就是a = a + 1,a = 5是全局的变量,虽然能在内部函数foo中可见,但是在foo函数内部出现了 a = ,出现等号就是即赋值即重新定义,那么=的右边作为赋值的内容 :a+1,但在函数中,此时的a已经算是重新定义了一个局部变量,而不是用外面的全局变量,但是此时a还没有完成赋值就被拿来进行加1操作,所以才会报错。

解决办法:

在这条语句前增加x=0之类的赋值语句,或者使用global 告诉内部作用域,去全局作用域查找变量定义
默认值的作用域

函数名.__defaults__属性:使用元组来保存所有位置参数默认值,它不会因为在函数体中使用了它而发生了变化。

函数名.__kwdefaults__属性:使用字典保存所有keyword-only参数的默认值。

使用可变类型(引用参数)作为默认值,就有可能修改这个默认值。

使用按需修改,例子。

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
def foo(xyz=[], u='abc', z=123):
xyz = xyz[:] # 影子拷贝
xyz.append(1)
print(xyz)

foo()
print(foo.__defaults__)
foo([10])
print(foo.__defaults__)
foo([10,5])
print(foo.__defaults__)

# 函数体内,不改变默认值
# 使用影子拷贝创建一个新的对象,永远不能改变传入的参数
# xyz都是传入参数或者默认参数的副本,如果就想修改原参数,无能为力

def foo(xyz=None, u='abc', z=123):
if xyz is None:
xyz = []
xyz.append(1)
print(xyz)

# 使用不可变类型默认值
# 如果使用缺省值None就创建一个列表
# 如果传入一个列表,就修改这个列表

全局变量global

使用global关键字的变量,将函数内的定义的局部变量声明成全局变量。

如果函数需要使用外部全局变量,请使用函数的形参传参解决。

尽量不使用

nonlocal关键字

nonlocal将变量标记为不再本地作用域定义,而在上一级的某一级局部作用域中定义,但不能是全局作用域中定义。

7.闭包

自由变量:未在本地作用域中定义的变量,例如定义在内层函数外的外层函数的作用域中的变量

闭包:是一概念,是嵌套函数中,指的是在内层函数中引用到外层函数的自由变量,就形成了闭包。

8.变量名解析原则LEGB

  • Local,本地作用域、局部作用域的local命名空间。函数调用时创建,调用结束消亡。

  • Enclosing,Python2.2时引入了嵌套函数,实现了闭包,这个就是嵌套函数的外部函数的命名空间。

  • Global,全局作用域,即一个模块的命名空间。模块被import时创建,解释器退出时消亡。

  • Build-in,内置模块的命名空间,生命周期从python解释器启动时创建到解释器退出时消亡。例如 print(open),print和open都是内置的变量。

函数变量作用域:级别:Built_in(内建) > Global(全局) > Enclosing(封装)> local(本地)

9.函数的销毁

全局函数销毁
  1. 重新定义同名函数
  2. del 语句删除函数对象名称,函数对象的引用计数减1
  3. 程序结束时
1
2
3
4
5
6
7
8
9
10
11
def foo(xyz=[], u='abc', z=123):
xyz.append(1)
return xyz
print(foo(), id(foo), foo.__defaults__)
def foo(xyz=[], u='abc', z=123):
xyz.append(1)
return xyz
print(foo(), id(foo), foo.__defaults__)
del foo
print(foo(), id(foo), foo.__defaults__)

局部函数销毁
  1. 重新在上级作用域定义同名函数
  2. del 语句删除函数名称,函数对象的引用计数减1
  3. 上级作用域销毁时
1
2
3
4
5
6
7
8
9
10
11
12
13
def foo(xyz=[], u='abc', z=123):
xyz.append(1)
def inner(a=10):
pass
print(inner)
def inner(a=100):
print(xyz)
print(inner)
return inner
bar = foo()
print(id(foo),id(bar), foo.__defaults__, bar.__defaults__)
del bar
print(id(foo),id(bar), foo.__defaults__, bar.__defaults__)

10.递归函数

函数是需要压栈的,栈和线程相关。

11.匿名函数

没有名字的函数,python借助lamdba表达式构建匿名函数。

参数列表不需要小括号。

冒号是用来区分参数列表和表达式的。

不需要return,表达式的值,就是匿名函数返回值。

lambda表达式(匿名函数)只能写在一行上,被成为单行函数。

用途:在高阶函数传参时,使用lambda表达式,往往能简化代码

格式:lambda 参数列表:表达式

1
2
3
lambda x :  x**2

(lambda x : x**2) () #调用

12.高阶函数

FIrst Class Object

函数也是对象,可调用对象

函数可以作为普通变量、参数、返回值等等。

高阶函数,至少满足下面的一个条件的函数。

接收一个或者多个函数作为参数,或者函数的输出是一个函数。

13.装饰器

装饰器本质上是一个 Python 函数或类。

它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

多装饰器的函数执行顺序,由底向上

14.参数注解

文档注解:函数内的最前面,使用三个双引号

函数注解:

  • python3.5引入
  • 对函数的参数进行类型注解
  • 对函数的返回值进行类型注解
  • 只对函数参数做一个辅助的说明,并不对函数参数进行类型检查
  • 提供给第三方工具,做代码分析,发现隐藏的bug
  • 函数注解的信息,保存在__annotations__属性中

变量注解:python3.6引入

函数参数类型检查

思路:

  • 函数参数的检查,一定是在函数外
  • 函数应该作为参数,传入到检查函数中
  • 检查函数拿到函数传入的实际参数,与形参声明对比
  • __annotations__属性是一个字典,其中包括返回值类型的声明,加入要位置参数的判断,无法和字典中的声明对应,使用inspect模块

inspect模块:提取获取对象信息的函数,可以检查函数和类、类型检查

  • inspect.isfunction(add) , 是否是函数
  • inspect.ismethod(add) , 是否是类的方法
  • inspect.isgenerator(add) , 是否是生成器对象
  • inspect.isgeneratorfunction(add) , 是否是生成器函数
  • inspect.isclass(add) , 是否是类
  • inspect.ismodule(inspect) , 是否是模块
  • inspect.isbuiltin(print) , 是否是内建对象

signature(callable),获取签名(函数签名包含了一个函数的信息,包括函数名,它的参数类型,它的所在的类和名称空间及其他信息)

Parameter对象

保存在元组中

输入属性:inspect.signature.parameters.annotation/name/kind/default

返回属性:inspect.signature.return_annotation

当不知道该方法下面有多少属性的时候,可以先用type查看该它的类型,然后通过导入模块,使用参数注解的方式来查看。

标准库

datetime模块

  • 对日期、时间、时间戳的处理

  • datetime类

    • 类方法:

      • today():返回本地时区当前时间的datetime对象

      • now(tz=None):返回当前时间的datetime对象,时间到微秒,如果tz为None,返回和today()一样

      • utcnow():没有时区的当前时间(国际化的时候建议用这个)

      • fromtimestamp(timestamp , tz = None) 从一个时间戳返回一个datetime对象

    • datetime对象

      • timestamp():返回一个到微秒的时间戳
        • 时间戳:格林威治时间1970年1月1日0点到现在的秒数。
      • 构造方法 datetime.datetime(2016, 12, 6, 16, 29, 43, 79043)
      • year、month、day、hour、minute、second、microsecond,取datetime对象的年月日时分秒及微秒
      • weekday() 返回星期的天,周一0,周日6
      • isoweekday() 返回星期的天,周一1,周日7
      • date() 返回日期date对象
      • time() 返回时间time对象
      • replace() 修改并返回新的时间
      • isocalendar() 返回一个三元组(年,周数,周的天)
    • 日期格式化

      • 类方法 strptime(date_string, format) ,返回datetime对象

      • 对象方法 strftime(format) ,返回字符串

      • 字符串format函数格式化

        1
        2
        3
        4
        import datetime
        dt = datetime.datetime.strptime("21/11/06 16:30", "%d/%m/%y %H:%M")
        print(dt.strftime("%Y-%m-%d %H:%M:%S"))
        print("{0:%Y}/{0:%m}/{0:%d} {0:%H}::{0:%M}::{0:%S}".format(dt))
  • timedelta对象

    • datetime2 = datetime1 + timedelta

    • datetime2 = datetime1 - timedelta

    • timedelta = datetime1 - datetime2

    • 构造方法

      1
      2
      3
      - datetime.timedelta(days=0, seconds=0, microseconds=0, milliseconds=0, 
      minutes=0, hours=0, weeks=0)
      - year = datetime.timedelta(days=365)
    • total_seconds() 返回时间差的总秒数

time模块

  • time.sleep(secs) 将调用线程挂起指定的秒数(发起一个系统调用,让程序暂停)

解析式

列表解析式

  1. 语法:
  • [返回值 for 元素 in 可迭代对象 if 条件]

  • 使用中括号[],内部是for循环,if条件语句可选

  • 返回一个新的列表

  1. 列表解析式是一种语法糖:
  • 编译器会优化,不会因为简写而影响效率,反而会因优化而提高了效率。
  • 减少了程序员工作量,减少出错。
  • 简化了代码,但可读性增强,不便于可读。
  1. 举例:
  • 获取10以内的偶数
    1
    2
    3
    4
    5
    6
    7
    8
    #普通
    even = []
    for x in range(10):
    if x % 2 == 0:
    even.append(x)

    #解析式
    even = [x for x in range(10) if x%2 == 0]
  1. 思考:
  • 有这样的赋值语句 newlist = [print(i) for i in range(10)]打印出来的是什么?
    print()把所有的i全打印出来,newlist里面则是10个None

  • 获取20以内的偶数,如果同时是3的倍数,把它打印出来

    1
    2
    [i for i in range(20) if not i % 2 and i % 3 == 0]
    [i for i in range(20) if not i % 2 if i % 3 == 0]
  • 获取20以内2的倍数或者3的倍数,把它打印出来
    [i for i in range(20) if not i % 2 or i % 3 == 0]

  • “0001.abadicddws” 是ID格式,要求ID格式是以点号分割,左边是4位从1开始的整数,右边是
    10位随机小写英文字母。请依次生成前100个ID的列表

1
2
3
4
5
6
7
8
9
10
11
12
import string
import random
x= string.ascii_lowercase
["{:>04}.{}".format(i,"".join(random.choices(x,k = 10))) for i in range(1,8)]

['0001.rvverptnre',
'0002.beqkpbxhfl',
'0003.woxvvayzjo',
'0004.wrbnnkelcc',
'0005.kpsjowcfvz',
'0006.pnqwqzlttc',
'0007.wehobydqnf']

字典解析式

  1. 语法:
  • {返回值 for 元素 in 可迭代对象 if 条件}
  • 列表解析式的中括号换成大括号{}就行了
  • 使用key:value形式
  • 立即返回一个字典
    用法:
    1
    2
    3
    4
    5
    6
    7
    {x:(x,x+1) for x in range(10)}
    {x:[x,x+1] for x in range(10)}
    {(x,):[x,x+1] for x in range(10)}
    {[x]:[x,x+1] for x in range(10)} #[x]不可哈希,字典的k要求可哈希
    {chr(0x41+x):x**2 for x in range(10)}
    {str(x):y for x in range(3) for y in range(4)} # 输出多少个元素?
    #会覆盖,返回{0:3,1:3,2:3}

集合解析式

  1. 语法
  • {返回值 for 元素 in 可迭代对象 if 条件}
  • 列表解析式的中括号换成大括号{}就行了
  • 立即返回一个集合
    用法
    1
    2
    {(x,x+1) for x in range(10)}
    {[x] for x in range(10)} #[x]是个集合,不能哈希,所以放在集合解析式里面会报错

生成器

生成器Generator

生成器指的是生成器对象,可以由生成器表达式得到,也可以使用yield关键字得到一个生成器函数,调用这个函数得到一个生成器对象。

生成器函数:

  1. 函数体内包含yield语句的函数,返回一个生成器对象,生成器函数的函数体不会立即执行。

    生成器对象,是一个可迭代对象,是一个迭代器。

  2. 生成器对象,是延迟计算,惰性求值。

  3. next(generator)会从函数的当前位置向后执行到之后碰到的第一个yield语句,会弹出值,并暂停函数执行。

  4. 再次执行会执行到下一个yield语句,没有多余的yield语句能执行,如果函数没有显式的return语句,继续调用next函数就会抛出StopIteration异常。

  5. return会导致无法继续获取下一个值,抛出StopIteration异常。

  6. 生成器函数,它是函数,不过这个函数不像普通的函数调用时能返回一个合法的值,它返回的是一个还没有求过任何值的生成器对象,用next拨一下才会往后执行一下。

  7. 生成器提供了一个send方法,该方法可以和生成器方向沟通。调用send方法,就可以把send的实参传递给yield语句做结果,这个结果可以在等式右边被赋值给其他变量。yield和next一样可以推动生成器启动并执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #举例
    def gen():
    print('line 1')
    yield 1
    print('line 2')
    yield 2
    print('line 3')
    return 3
    next(gen()) # line 1
    next(gen()) # line 1
    g = gen()
    print(next(g)) # line 1
    print(next(g)) # line 2
    print(next(g, 'End')) # 没有元素给个缺省值
    print(next(g, 'End')) # 没有元素给个缺省值

生成器的应用

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
#计数器
def inc():
def counter():
i = 0
while True:
i += 1
yield i
c = counter()
return lambda : next(c)

foo = inc()
print(foo()) #调用的时候是因为inc()函数返回的是一个匿名函数,而匿名函数的调用方式是(lambda 参数列表:函数体)(),所以调用的时候是foo后要加上(),即也就是inc()()。


#等价于
def inc():
def counter():
i = 0
while True:
i += 1
yield i
c = counter()

def _inc(): #用到外面的自由变量(c):闭包
return next(c)
return _inc
foo = inc() #可调用对象,函数
print(foo())
yield from

yield from是python3.3出现的新语法

yield from iterable 是for item in iterable: yield item形式的语法糖。

1
2
3
4
5
6
for x in range(1000):
yield x

#等价于

yield from range(1000)

从可迭代对象中一个个拿数据

1
2
3
4
5
6
7
8
9
10
def counter(n):   #生成器,迭代器
for x in range(n):
yield x

def inc(n):
yield from counter(n)

foo = inc(10)
print(next(foo))
print(next(foo))
协程coroutine

生成器的高级用法
比进程、线程轻量级
是在用户空间调度函数的一种实现
Python3 asyncio就是协程实现,已经加入到标准库
Python3.5 使用async、await关键字直接原生支持协程
协程调度器实现思路

  • 有2个生成器A、B
  • next(A)后,A执行到了yield语句暂停,然后去执行next(B),B执行到yield语句也暂停,然后再次调用next(A),再调用next(B)在,周而复始,就实现了调度的效果
  • 可以引入调度的策略来实现切换的方式

协程是一种非抢占式调度

生成器表达式Generator expression
  1. 语法
  • (返回值 for 元素 in 可迭代对象 if 条件)

  • 列表解析式的中括号换成小括号就行了

  • 返回一个生成器

  1. 和列表解析式的区别
  • 生成器表达式是按需计算(或称惰性求值、延迟计算),需要的时候才计算值,返回迭代器,可以迭代,从前到后走完一遍后,不能回头。

  • 列表解析式是立即返回值,返回的不是迭代器,返回可迭代对象列表, 从前到后走完一遍后,可以重新回头迭代。

  1. 和列表解析式的对比
  • 计算方式
    • 生成器表达式延迟计算,列表解析式立即计算
  • 内存占用
    • 单从返回值本身来说,生成器表达式省内存,列表解析式返回新的列表
    • 生成器没有数据,内存占用极少,但是使用的时候,虽然一个个返回数据,但是合起来占用的内存也差不多
    • 列表解析式构造新的列表需要占用内存
  • 计算速度
    • 单看计算时间看,生成器表达式耗时非常短,列表解析式耗时长
    • 但是生成器本身并没有返回任何值,只返回了一个生成器对象
    • 列表解析式构造并返回了一个新的列表
  1. 生成器
  • 可迭代对象(able结尾)
  • 迭代器(or结尾),是不是迭代器用next()方法可以检测,iter()方法可以把一个可迭代对象封装成迭代器。
  • 生成器和迭代器是不同的对象,但都是可迭代对象,生成器对象,就是迭代器对象,迭代器不一定是生成器。

内建函数

  1. 标识 id
  • 返回对象的唯一标识,CPython返回内存地址
  1. 哈希 hash()
  • 返回一个对象的哈希值
  1. 类型 type()
  • 返回对象的类型
  1. 类型转换
  • float() int() bin() hex() oct() bool() list() tuple() dict() set() complex() bytes() bytearray()
  1. 输入 input([prompt])
  • 接收用户输入,返回一个字符串
  1. 打印 print(*objects, sep=’ ‘, end=’\n’, file=sys.stdout, flush=False)
  • 打印输出,默认使用空格分割、换行结尾,输出到控制台
  1. 对象长度 len(s)
  • 返回一个集合类型的元素个数
  1. isinstance(obj, class_or_tuple)
  • 判断对象obj是否属于某种类型或者元组中列出的某个类型
  • isinstance(True, int)
  1. issubclass(cls, class_or_tuple)
  • 判断类型cls是否是某种类型的子类或元组中列出的某个类型的子类
  • issubclass(bool, int)
  1. 绝对值abs(x) x为数值

  2. 最大值max() 最小值min()

  • 返回可迭代对象中最大或最小值
  • 返回多个参数中最大或最小值
  1. round(x) 四舍六入五取偶,round(-0.5)

  2. pow(x , y) 等价于 x**y

  3. range(stop) 从0开始到stop-1的可迭代对象;range(start, stop[, step])从start开始到stop-1结束步长为step的可迭代对象

  4. divmod(x, y) 等价于 tuple (x//y, x%y)

  5. sum(iterable[, start]) 对可迭代对象的所有数值元素求和,start:初始值

  • sum(range(1,100,2))
  1. chr(i) 给一个一定范围的整数返回对应的字符
  • chr(97) chr(20013)
  1. ord(c) 返回字符对应的整数
  • ord(‘a’) ord(‘中’)
  1. sorted
    sorted(iterable[, key][, reverse] )排序
  • 返回一个新的列表,对一个可迭代对象的所有元素进行排序,默认升序
  • reverse是反转
    1
    2
    3
    sorted([1, 3, 5])
    sorted([1, 3, 5], reverse=True) #[5, 3 , 1]
    sorted({'c':1, 'b':2, 'a':1})
  1. 翻转 reversed(seq)
    返回一个翻转元素的迭代器(惰性求值)

    1
    2
    3
    4
    5
    list(reversed("13579"))
    { reversed((2, 4)) } # 有几个元素?
    for x in reversed(['c','b','a']):
    print(x)
    reversed(sorted({1, 5, 9}))
  2. 枚举 enumerate(seq, start=0)

迭代一个序列,返回索引数字和元素构成的二元组
start表示索引开始的数字,默认是0

1
2
3
4
for x in enumerate([2,4,6,8]):
print(x)
for x in enumerate("abcde"):
print(x,end=" ")
  1. 迭代器和取元素 iter(iterable)、next(iterator[, default])

iter将一个可迭代对象封装成一个迭代器
next对一个迭代器取下一个元素。如果全部元素都取过了,再次next会抛StopIteration异常

1
2
3
4
5
it = iter(range(5))
next(it)

it = reversed([1,3,5])
next(it)
  1. 拉链函数zip(*iterables)
    像拉链一样,把多个可迭代对象合并在一起,返回一个迭代器
    将每次从不同对象中取到的元素合并成一个元组

    1
    2
    3
    4
    5
    list(zip(range(10),range(10)))
    list(zip(range(10),range(10),range(5),range(10)))

    dict(zip(range(10),range(10)))
    {str(x):y for x,y in zip(range(10),range(10))}
  2. 过滤数据filter(function, iterable) — >filter object

    过滤可迭代对象的元素,返回一个迭代器

    function是一个具有一个参数的函数。返回bool

    1
    2
    3
    #例子:过滤出数列中能被3整除的数字
    In [4]: list(filter(lambda x: x%3 == 0,[1,9,32,3,21,0,-1,123]))
    Out[4]: [9, 3, 21, 0, 123]
  3. reduce:连续计算,连续从一个可迭代对象中获取值,来进行计算,需要从functools模块中导入

  4. 映射map(func, *iterables) —-> map object

    对多个可迭代对象的元素按照指定的函数进行映射,返回一个迭代器