Socket网络编程学习笔记

简介

网络中进程之间如何通信?本地的进程间通信可以有很多种方式,但可以总结为下面4类:

  1. 消息传递(管道、FIFO、消息队列)
  2. 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
  3. 共享内存(匿名的和具名的)
  4. 远程过程调用(Solaris门和Sun RPC)

在本地可以通过PID来标识一个进程,但是在网络中这是行不通的,网络中进程通信首先要解决的问题是如何唯一标识一个进程,否则通信无从谈起。
其实TCP/IP协议族已经帮我们解决了这个问题,网络层中的IP地址可以唯一标识网络中的计算机,而传输层的“协议”+“端口”可以唯一标识主机中的应用程序。
这样利用三元组(IP地址、协议、端口)就可以标识网络中的进程了,网络中的进程通信就可以以利用这个标志与其他进程交互。

socket中的地址簇

  • socket.AF_UNIX:用于本机间的进程通信,如果不用这个也可以用pickle序列化实现,不过效率低
  • socket.AF_INET:IPV4通信
  • socket.AF_INET6:IPV6通信

socket的类型

  • socket.SOCK_STRAEM:用于TCPIP通信
  • socket.SOCK_DGRAM:用于UDP通信
  • socket.SOCK_RAW:原始套接字,可以用来构造IP头

基本通讯思路

首先是先有接受端计算机,选择socket地址簇和socket的类型(相当于告诉计算机使用何种方式发送和解析数据),需要监听某个端口,
等待远程计算机发送数据,接受此数据,然后发送新的数据给远程计算机,继续保持监听状态。然后有发送端计算机,
选择socket地址簇和socket的类型(相当于告诉计算机使用何种方式发送和解析数据),接受数据,关闭远程连接。

TCP编程

客户端

大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。举个例子,当我们在浏览器中访问新浪时,
我们自己的计算机就是客户端,浏览器会主动向新浪的服务器发起连接。如果一切顺利,新浪的服务器接受了我们的连接,一个TCP连接就建立起来的,
后面的通信就是发送网页内容了。所以,我们要创建一个基于TCP连接的Socket,可以这样做:

1
2
3
4
5
6
7
# 导入socket库:
import socket

# 创建一个socket:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接:
s.connect(('www.sina.com.cn', 80))

创建Socket时,AF_INET指定使用IPv4协议,如果要用更先进的IPv6,就指定为AF_INET6。SOCK_STREAM指定使用面向流的TCP协议,
这样,一个Socket对象就创建成功,但是还没有建立连接。

客户端要主动发起TCP连接,必须知道服务器的IP地址和端口号。新浪网站的IP地址可以用域名www.sina.com.cn自动转换到IP地址,
但是怎么知道新浪服务器的端口号呢?

答案是作为服务器,提供什么样的服务,端口号就必须固定下来。由于我们想要访问网页,因此新浪提供网页服务的服务器必须把端口号固定在80端口,
因为80端口是Web服务的标准端口。其他服务都有对应的标准端口号,例如SMTP服务是25端口,FTP服务是21端口,等等。
端口号小于1024的是Internet标准服务的端口,端口号大于1024的,可以任意使用。因此,我们连接新浪服务器的代码如下:

1
s.connect(('www.sina.com.cn', 80))

注意参数是一个tuple,包含地址和端口号。建立TCP连接后,我们就可以向新浪服务器发送请求,要求返回首页的内容:

1
2
# 发送数据:
s.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')

TCP连接创建的是双向通道,双方都可以同时给对方发数据。但是谁先发谁后发,怎么协调,要根据具体的协议来决定。
例如,HTTP协议规定客户端必须先发请求给服务器,服务器收到后才发数据给客户端。
发送的文本格式必须符合HTTP标准,如果格式没问题,接下来就可以接收新浪服务器返回的数据了:

1
2
3
4
5
6
7
8
9
10
# 接收数据:
buffer = []
while True:
# 每次最多接收1k字节:
d = s.recv(1024)
if d:
buffer.append(d)
else:
break
data = b''.join(buffer)

接收数据时,调用recv(max)方法,一次最多接收指定的字节数,因此,在一个while循环中反复接收,直到recv()返回空数据,表示接收完毕,退出循环。
当我们接收完数据后,调用close()方法关闭Socket,这样,一次完整的网络通信就结束了:

1
2
# 关闭连接:
s.close()

接收到的数据包括HTTP头和网页本身,我们只需要把HTTP头和网页分离一下,把HTTP头打印出来,网页内容保存到文件:

1
2
3
4
5
header, html = data.split(b'\r\n\r\n', 1)
print(header.decode('utf-8'))
# 把接收的数据写入文件:
with open('sina.html', 'wb') as f:
f.write(html)

现在,只需要在浏览器中打开这个sina.html文件,就可以看到新浪的首页了。

服务端

服务器进程首先要绑定一个端口并监听来自其他客户端的连接。如果某个客户端连接过来了,服务器就与该客户端建立Socket连接,
随后的通信就靠这个Socket连接了。所以,服务器会打开固定端口(比如80)监听,每来一个客户端连接,就创建该Socket连接。
由于服务器会有大量来自客户端的连接,所以,服务器要能够区分一个Socket连接是和哪个客户端绑定的。
一个Socket依赖4项:服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个Socket。但是服务器还需要同时响应多个客户端的请求,
所以,每个连接都需要一个新的进程或者新的线程来处理,否则,服务器一次就只能服务一个客户端了。
我们来编写一个简单的服务器程序,它接收客户端连接,把客户端发过来的字符串加上Hello再发回去。首先,创建一个基于IPv4和TCP协议的Socket:

1
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

然后,我们要绑定监听的地址和端口。服务器可能有多块网卡,可以绑定到某一块网卡的IP地址上,也可以用0.0.0.0绑定到所有的网络地址,
还可以用127.0.0.1绑定到本机地址。127.0.0.1是一个特殊的IP地址,表示本机地址,如果绑定到这个地址,客户端必须同时在本机运行才能连接,
也就是说,外部的计算机无法连接进来。
端口号需要预先指定。因为我们写的这个服务不是标准服务,所以用9999这个端口号。请注意,小于1024的端口号必须要有管理员权限才能绑定:

1
2
# 监听端口:
s.bind(('127.0.0.1', 9999))

紧接着,调用listen()方法开始监听端口,传入的参数指定等待连接的最大数量:

1
2
s.listen(5)
print('Waiting for connection...')

接下来,服务器程序通过一个永久循环来接受来自客户端的连接,accept()会等待并返回一个客户端的连接:

1
2
3
4
5
6
while True:
# 接受一个新连接:
sock, addr = s.accept()
# 创建新线程来处理TCP连接:
t = threading.Thread(target=tcplink, args=(sock, addr))
t.start()

每个连接都必须创建新线程(或进程)来处理,否则,单线程在处理连接的过程中,无法接受其他客户端的连接:

1
2
3
4
5
6
7
8
9
10
11
def tcplink(sock, addr):
print('Accept new connection from %s:%s...' % addr)
sock.send(b'Welcome!')
while True:
data = sock.recv(1024)
time.sleep(1)
if not data or data.decode('utf-8') == 'exit':
break
sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))
sock.close()
print('Connection from %s:%s closed.' % addr)

连接建立后,服务器首先发一条欢迎消息,然后等待客户端数据,并加上Hello再发送给客户端。如果客户端发送了exit字符串,就直接关闭连接。
要测试这个服务器程序,我们还需要编写一个客户端程序:

1
2
3
4
5
6
7
8
9
10
11
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接:
s.connect(('127.0.0.1', 9999))
# 接收欢迎消息:
print(s.recv(1024).decode('utf-8'))
for data in [b'Michael', b'Tracy', b'Sarah']:
# 发送数据:
s.send(data)
print(s.recv(1024).decode('utf-8'))
s.send(b'exit')
s.close()

我们需要打开两个命令行窗口,一个运行服务器程序,另一个运行客户端程序,就可以看到效果了。需要注意的是,客户端程序运行完毕就退出了,
而服务器程序会永远运行下去,必须按Ctrl+C退出程序。

用TCP协议进行Socket编程在Python中十分简单,对于客户端,要主动连接服务器的IP和指定端口,对于服务器,要首先监听指定端口,
然后,对每一个新的连接,创建一个线程或进程来处理。通常,服务器程序会无限运行下去。同一个端口,被一个Socket绑定了以后,
就不能被别的Socket绑定了。

UDP编程

客户端(UDP编程)

TCP是建立可靠连接,并且通信双方都可以以流的形式发送数据。相对TCP,UDP则是面向无连接的协议。使用UDP协议时,不需要建立连接,
只需要知道对方的IP地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。虽然用UDP传输数据不可靠,但它的优点是和TCP比,
速度快,对于不要求可靠到达的数据,就可以使用UDP协议。我们来看看如何通过UDP协议传输数据。和TCP类似,使用UDP的通信双方也分为客户端和服务器。
服务器首先需要绑定端口:

1
2
3
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定端口:
s.bind(('127.0.0.1', 9999))

recvfrom()方法返回数据和客户端的地址与端口,这样,服务器收到数据后,直接调用sendto()就可以把数据用UDP发给客户端。
注意这里省掉了多线程,因为这个例子很简单。客户端使用UDP时,首先仍然创建基于UDP的Socket,然后,不需要调用connect(),
直接通过sendto()给服务器发数据:

1
2
3
4
5
6
7
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for data in [b'Michael', b'Tracy', b'Sarah']:
# 发送数据:
s.sendto(data, ('127.0.0.1', 9999))
# 接收数据:
print(s.recv(1024).decode('utf-8'))
s.close()

服务端(UDP编程)

创建Socket时,SOCK_DGRAM指定了这个Socket的类型是UDP。绑定端口和TCP一样,但是不需要调用listen()方法,而是直接接收来自任何客户端的数据:

1
2
3
4
5
6
print('Bind UDP on 9999...')
while True:
# 接收数据:
data, addr = s.recvfrom(1024)
print('Received from %s:%s.' % addr)
s.sendto(b'Hello, %s!' % data, addr)

从服务器接收数据仍然调用recv()方法。仍然用两个命令行分别启动服务器和客户端测试。
UDP的使用与TCP类似,但是不需要建立连接。此外,服务器绑定UDP端口和TCP端口互不冲突,也就是说,UDP的9999端口与TCP的9999端口可以各自绑定。

常见问题

粘包

粘包问题是指当发送的数据包过大时,接收方可能会将数据包粘在一起,导致数据包的边界不清晰。为了解决粘包问题,可以采用以下方法:

  1. 使用包边界标识符 在发送数据时,每个数据包都带有边界标识符,接收方根据边界标识符来判断数据包的边界,从而将数据包分开。例如,可以在每个数据包的头部或尾部添加一个包长度字段,接收方根据包长度字段来解析数据包。

  2. 使用包分隔符 在发送数据时,每个数据包之间都添加一个包分隔符,接收方根据包分隔符来判断数据包的边界。例如,在发送文本数据时,可以在每个数据包之间添加一个换行符,接收方根据换行符来解析数据包。

  3. 限制发送数据的大小 限制每个数据包的最大大小,确保数据包不会过大,从而避免粘包问题的发生。

  4. 增加包的冗余信息 在发送数据时,每个数据包都带有冗余信息,例如时间戳或序列号等,接收方根据冗余信息来判断数据包的边界。例如,在发送网络数据时,可以在每个数据包的头部添加一个时间戳字段,接收方根据时间戳字段来解析数据包。

半包

半包问题是指在socket编程中,接收到的数据不是一个完整的数据包,而是一半的数据包或者是一小部分的数据包。这种问题通常出现在网络传输过程中出现丢包或者传输延迟的情况下。下面是解决半包问题的一些方法:

  1. 增加重发机制:当接收到的数据不是一个完整的数据包时,可以发送一个请求给发送方,要求发送方重新发送缺失的数据包。发送方接收到请求后,会重新发送缺失的数据包,这样可以确保接收方能够接收到完整的数据包。

  2. 使用校验和校验数据的完整性:在发送数据时,可以在数据的末尾添加一个校验值,用于校验数据的完整性。当接收方接收到数据后,可以计算接收到的数据的校验值,并与发送方发送的校验值进行比较,如果一致,则说明接收到的数据是完整的;如果不一致,则说明接收到的数据是半包数据。

  3. 增加包的冗余信息:在发送数据时,可以在数据的前面或者后面添加一些冗余信息,用于确保接收方能够正确地接收到数据。例如,在数据的前面添加包的长度信息,这样接收方可以根据包的长度来判断接收到的数据是否完整。

  4. 使用滑动窗口协议:滑动窗口协议是一种流控制协议,用于确保接收方能够正确地接收到数据,并且防止接收方的缓冲区溢出。在滑动窗口协议中,接收方维护一个窗口,用于存储已经接收到的数据,发送方根据接收方返回的确认消息来判断哪些数据已经被接收方接收到,从而决定是否继续发送数据。

HTTPS学习笔记

前言

了解https之前需要了解对称加密和非对称加密、http的弊端。

http的弊端

http_transfer
由上图可见,http在传输数据过程中,所有数据都是明文传输,自然没有安全性可言。
https使用了混合加密算法(对称加密和非对称加密),可以用密钥加密或还原数据,只要确保密钥不被第三方获取,就能确保数据传输的安全。

对称加密 vs 非对称加密

  1. 对称加密
    • 优点:算法公开、计算量小、加密速度快、加密效率高、适合加密比较大的数据。
    • 缺点:交易双方需要使用相同的密钥,也就无法避免密钥偶的传输,二密钥在传输过程中无法保证不被截获,因此对称加密的安全性得不到保证。
      交易双方需要使用相同的密钥,也就无法避免密钥偶的传输,二密钥在传输过程中无法保证不被截获,因此对称加密的安全性得不到保证。
      每次用户使用对称加密算法时,都需要使用其他人不知道的唯一密钥,这会使得发收信双方所拥有的密钥密钥数量急剧增长,密钥管理成为双方负担。
      对称加密算法在分布式系统上使用较为困难,主要因为密钥管理困难,使用成本高。
      symmetric_encryption
      由上图可见,被加密的数据在传输过程中是无规则乱码,即便被第三方截获,在没有密钥的情况下也无法解密数据,这就保证了数据安全。
      有一个致命问题是,既然双方要使用相同的密钥,那就必须要在传输数据之前由一方把密码传给另一方,这个过程很有可能被截获,加密数据也会被轻松解密。
  2. 非对称加密
    • 优点:算法公开,加密和解密使用不同的密钥,私钥不需要通过网络进行传输,安全性很高。
    • 缺点:计算量很大,加密和解密速度对比对称加密慢很多。
      asymmetric_encryption
      由上图可见,客户端在拿到服务器的公钥后,会生成一个随机码(用KEY表示,这个KEY就是后续双方用于对称加密的密钥),
      然后客户端使用公钥包KEY加密后再发送给服务器,服务器使用私钥将其解密,这样双方就有了同一个密钥KEY,然后双方再使用KEY进行对称加密交互数据。
      在非对称加密传输KEY的过程中,即便第三方获取了公钥和加密后的KEY,在没有私钥的情况下也无法破解KEY(私钥存在服务器,泄露风险极小),
      这就保证了接下来对称加密的数据安全。上图流程就是https的雏形,
      https正好综合了两种加密算法的的优点,不仅保障了数据安全,还保证了数据的传输效率。

https原理

HTTPS(Hypertext Transfer Protocol Secure)是基于HTTP的扩展,用于计算机网络的安全通信,已经在互联网得到广泛的应用,
在HTTPS中,原有的HTTP协议会得到TLS(安全传输层协议)或其前辈SSL(安全套接层的加密)。因此,HTTPS也常指HTTP over TLS或HTTP over SSL,也就是说HTTPS = HTTP + SSL/TLS。
https_theory

https加密、解密、验证及数据传输过程

https的整个通讯过程可以分为两大阶段:证书验证和数据传输阶段,数据传输阶段又可以分为非对称加密和对称加密两个阶段。
https_transfer

  1. 客户端请求HTTPS网址,然后连接到server的443端口(HTTPS默认端口,类似于HTTP的80端口)。
  2. 采用HTTPS协议的服务器必须要有一套数字CA(Certification Authority)证书,证书时需要申请的,
    并由专门的数字证书认证机构(CA)通过非常严格的审核之后颁发的电子证书。颁发证书的同时会产生一个公钥和私钥。
    私钥由服务器自己保存,不可泄漏。公钥则是附带在证书信息中,可以以公开的。证书本身也附带一个证书电子签名,
    这个签名用来验证证书的完整性和真实性,可以防止证书被篡改。
  3. 服务器响应客户端请求,将证书传递给客户端,证书包含公钥和其他信息,比如证书的颁发机构信息,公司信息和证书有效期等。
  4. 客户端解析证书并对其进行验证。如果证书不是可信机构颁布,或者整数中的域名与实际域名不一致,或者证书已经过期,
    就会向访问者显示一个警告,由其选择是否还要继续通信。
  5. 客户端把加密后的随机码KEY发送给服务器,作为后面对称加密的密钥。
  6. 服务器收到随机码KEY之后会使用私钥B将其解密。经过以上这些步骤,客户端和服务器终于建立了安全连接,完美解决了对称加密的密钥泄露问题,
    接下来就可以使用对称加密愉快的进行通信了。
  7. 服务器使用密钥(随机码KEY)对数据进行对称加密并发送给客户端,客户端使用相同的密钥(随机码KEY)解密数据。
  8. 双方使用对称加密愉快的传输所有数据。

总结

  • http和https的区别:
    1. 最重要的就是安全性,HTTP明文传输,不对数据进行加密安全性较差。HTTPS的数据传输过程是加密的,安全性较好。
    2. 使用HTTPS协议需要申请CA证书,一般免费证书较少,因此需要一定费用。
    3. HTTP页面响应速度比HTTPS快,HTTPS由于加了一层安全层,建立连接的过程更复杂,也要交换更多的数据,难免影响速度。
    4. 由于HTTPS是建立在SSL/TLS之上的HTTP协议,所以要比HTTP更耗费服务器资源。
    5. HTTPS和HTTP使用的是完全不同的连接方式,用的端口也不一样,前者是443,后者是80。
  • HTTPS的缺点:
    1. 在相同的网络环境中,HTTPS相比HTTP无论是响应时间还是耗电量都有大幅度上升。
    2. HTTPS的安全是有范围的,在黑客攻击、服务器劫持等情况下几乎起不到作用。
    3. 在现有的证书机制下,中间人攻击依然有可能发生。
    4. HTTPS需要更多的服务器资源,也会导致成本的升高。

HTTP协议学习笔记

超文本传输协议(HTTP)是一个用于传输超媒体文档(例如 HTML)的应用层协议。它是为 Web 浏览器与 Web 服务器之间的通信而设计的,
但也可以用于其他目的。HTTP 遵循经典的客户端—服务端模型,客户端打开一个连接以发出请求,然后等待直到收到服务器端响应。
HTTP 是无状态协议,这意味着服务器不会在两个请求之间保留任何数据(状态)。

HTTP概述

HTTP 是一种用作获取诸如 HTML 文档这类资源的协议。它是 Web 上进行任何数据交换的基础,同时,
也是一种客户端—服务器(client-server)协议,也就是说,请求是由接受方——通常是浏览器——发起的。
一个完整网页文档是由获取到的不同文档组件——像是文本、布局描述、图片、视频、脚本等——重新构建出来的。
http_request
客户端与服务端之间通过交换一个个独立的消息(而非数据流)进行通信。由客户端——通常是个浏览器——发出的消息被称作请求(request),
由服务端发出的应答消息被称作响应(response)。

20 世纪 90 年代,HTTP 作为一套可扩展的协议被设计出来,并随时间不断演进。HTTP 是一种应用层的协议,通过 TCP,
或者是 TLS——一种加密过的 TCP 连接——来发送,当然,理论上来说可以借助任何可靠的传输协议。受益于 HTTP 的可扩展性,
时至今日,它不仅可以用来获取超文本文档,还可用来获取图片、视频或者向服务端发送信息,比如填写好的 HTML 表单。
HTTP 还可以用来获取文档的部分内容,以便按需更新 Web 页面。

HTTP缓存

HTTP 缓存会存储与请求关联的响应,并将存储的响应复用于后续请求。可复用性有几个优点。首先,由于不需要将请求传递到源服务器,因此客户端和缓存越近,
响应速度就越快。最典型的例子是浏览器本身为浏览器请求存储缓存。此外,当响应可复用时,源服务器不需要处理请求——因为它不需要解析和路由请求、
根据 cookie 恢复会话、查询数据库以获取结果或渲染模板引擎。这减少了服务器上的负载。缓存的正确操作对系统的稳定运行至关重要。

私有缓存

私有缓存是绑定到特定客户端的缓存——通常是浏览器缓存。由于存储的响应不与其他客户端共享,因此私有缓存可以存储该用户的个性化响应。
另一方面,如果个性化内容存储在私有缓存以外的缓存中,那么其他用户可能能够检索到这些内容——这可能会导致无意的信息泄露。
如果响应包含个性化内容并且你只想将响应存储在私有缓存中,则必须指定 private 指令。

1
Cache-Control: private

个性化内容通常由 cookie 控制,但 cookie 的存在并不能表明它是私有的,因此单独的 cookie 不会使响应成为私有的。
请注意,如果响应具有 Authorization 标头,则不能将其存储在私有缓存(或共享缓存,除非 Cache-Control 指定的是 public)中。

共享缓存

共享缓存位于客户端和服务器之间,可以存储能在用户之间共享的响应。共享缓存可以进一步细分为代理缓存和托管缓存。

代理缓存

除了访问控制的功能外,一些代理还实现了缓存以减少网络流量。这通常不由服务开发人员管理,因此必须由恰当的 HTTP 标头等控制。然而,在过去,
过时的代理缓存实现——例如没有正确理解 HTTP 缓存标准的实现——经常给开发人员带来问题。
Kitchen-sink 标头如下所示,用于尝试解决不理解当前 HTTP 缓存规范指令(如 no-store)的“旧且未更新的代理缓存”的实现。

1
Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate

然而,近年来,随着 HTTPS 变得越来越普遍,客户端/服务器通信变得加密,在许多情况下,路径中的代理缓存只能传输响应而不能充当缓存。
因此,在这种情况下,无需担心甚至无法看到响应的过时代理缓存的实现。另一方面,如果TLS桥接代理通过在PC上安装来自组织管理的 CA (en-US) 证书,
以中间人方式解密所有通信,并执行访问控制等,则可以查看响应的内容并将其缓存。但是,
由于证书透明度(certificate transparency)在最近几年变得很普遍,
并且一些浏览器只允许使用证书签署时间戳(signed certificate timestamp)颁发的证书,因此这种方法需要应用于企业策略。
在这样的受控环境中,无需担心代理缓存“已过时且未更新”。

托管缓存

托管缓存由服务开发人员明确部署,以降低源服务器负载并有效地交付内容。示例包括反向代理、CDN 和 service worker 与缓存 API 的组合。
托管缓存的特性因部署的产品而异。在大多数情况下,你可以通过 Cache-Control 标头和你自己的配置文件或仪表板来控制缓存的行为。
例如,HTTP 缓存规范本质上没有定义显式删除缓存的方法——但是使用托管缓存,可以通过仪表板操作、API 调用、重新启动等实时删除已经存储的响应。
这允许更主动的缓存策略。也可以忽略标准 HTTP 缓存规范协议以支持显式操作。例如,可以指定以下内容以选择退出私有缓存或代理缓存,
同时使用你自己的策略仅在托管缓存中进行缓存。

1
Cache-Control: no-store

例如,Varnish Cache 使用 VCL(Varnish Configuration Language,一种 DSL (en-US))逻辑来处理缓存存储,
而 service worker 结合缓存 API 允许你在 JavaScript 中创建该逻辑。这意味着如果托管缓存故意忽略 no-store 指令,
则无需将其视为“不符合”标准。你应该做的是,避免使用 kitchen-sink 标头,但请仔细阅读你正在使用的任何托管缓存机制的文档,
并确保你选择的方式可以正确的控制缓存。请注意,某些 CDN 提供自己的标头,这些标头仅对该 CDN 有效(例如,Surrogate-Control)。
目前,正在努力定义一个 CDN-Cache-Control 标头来标准化这些标头。

HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据。浏览器会存储 cookie 并在下次向同一服务器再发起请求时携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器——如保持用户的登录。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。

Cookie 主要用于以下三个方面:

  1. 会话状态管理:如用户登录状态、购物车、游戏分数或其他需要记录的信息
  2. 个性化设置:如用户自定义设置、主题和其他设置
  3. 浏览器行为跟踪:如跟踪分析用户行为等

Cookie 曾一度用于客户端数据的存储,因当时并没有其他合适的存储办法而作为唯一的存储手段,但现在推荐使用现代存储 API。由于服务器指定 Cookie 后,浏览器的每次请求都会携带 Cookie 数据,会带来额外的性能开销(尤其是在移动环境下)。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(localStorage 和 sessionStorage)或 IndexedDB 。

安全

跟踪和隐私

跨域资源共享(CORS)

跨源资源共享(CORS,或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。

跨源 HTTP 请求的一个例子:运行在 https://domain-a.com 的 JavaScript 代码使用 XMLHttpRequest 来发起一个到 https://domain-b.com/data.json 的请求。出于安全性,浏览器限制脚本内发起的跨源 HTTP 请求。例如,XMLHttpRequest 和 Fetch API 遵循同源策略。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应报文包含了正确 CORS 响应头。

CORS 机制允许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequest 或 Fetch)使用 CORS,以降低跨源 HTTP 请求所带来的风险。

HTTP 客户端提示(Client Hint)

客户端提示是一组响应标头,服务器可以使用它来主动从客户端请求关于设备、网络、用户以及用户代理指定的首选项的信息。
然后,服务器可以根据客户端选择提供的信息来确定发送哪些资源。

HTTP 的演变

简单描述了从早期版本的 HTTP 到现代 HTTP/2,新兴的 HTTP/3 以及未来版本的 HTTP 这个过程中发生的变更。

Mozilla Web 安全准则

一系列用于帮助运营团队创建安全的 Web 应用程序的技巧。

HTTP 消息

描述了 HTTP/1.x 和 HTTP/2 中不同种类消息的类型和结构。

典型的 HTTP 会话

展现并解释了一个常见 HTTP 会话的流程。

HTTP/1.x 中的连接管理

描述了在 HTTP/1.x 中的三种连接管理模型,以及它们的优点和缺点。

HTTP 标头

HTTP 消息标头用于描述资源或服务器或客户端的行为。标头字段保存在 IANA 注册表中。IANA 也维护一个建议的新 HTTP 消息标头的注册表。

HTTP 请求方法

可以使用 HTTP:GET、POST 方法来完成不同操作,或是一些不太常见的请求方式,像是:OPTIONS、DELETE 或 TRACE。

HTTP 状态码

HTTP 状态码用来表示指定的 HTTP 请求是否已成功完成。响应分为五类:信息响应、成功响应,重定向、客户端错误和服务器错误。

CSP 指令

Content-Security-Policy 响应标头字段允许网站管理员控制页面上哪些资源能够被用户代理程序加载。除了少数例外,此策略主要涉及指定服务器来源和脚本终端。

Firefox 开发者工具

Mozilla Observatory

RedBot

用于检查与缓存相关的 HTTP 标头的工具。

浏览器的工作原理(2011)

一篇非常全面的关于浏览器内部实现与通过 HTTP 协议的请求流的文章。可以说是所有 Web 开发者的必读内容。

TCP/IP协议群学习笔记

TCP/IP简介

计算机为了联网,就必须规定通信协议,早期的计算机网络,都是由各厂商自己规定一套协议,IBM、Apple和Microsoft都有各自的网络协议,互不兼容。
为了把全世界的所有不同类型的计算机都连接起来,就必须规定一套全球通用的协议,为了实现互联网这个目标,
互联网协议簇(Internet Protocol Suite)就是通用协议标准。
因为互联网协议包含了上百种协议标准,但是最重要的两个协议是TCP和IP协议,所以,大家把互联网的协议简称TCP/IP协议。

从字面意义上讲,有人可能会认为 TCP/IP 是指 TCP 和 IP 两种协议。实际生活当中有时也确实就是指这两种协议。
然而在很多情况下,它只是利用 IP 进行通信时所必须用到的协议群的统称。具体来说,IP 或 ICMP、TCP 或 UDP、TELNET 或 FTP、
以及 HTTP 等都属于 TCP/IP 协议。他们与 TCP 或 IP 的关系紧密,是互联网必不可少的组成部分。
TCP/IP 一词泛指这些协议,因此,有时也称 TCP/IP 为网际协议群。互联网进行通信时,需要相应的网络协议,
TCP/IP 原本就是为使用互联网而开发制定的协议族。因此,互联网的协议就是 TCP/IP,TCP/IP 就是互联网的协议。
TCP/IP

计算机网络体系结构分层

OSI&TCP/IP

数据处理流程

互联网数据流程

通信的时候,双方必须知道对方的标识,好比发邮件必须知道对方的邮件地址。互联网上每个计算机的唯一标识就是IP地址,
类似123.123.123.123。如果一台计算机同时接入到两个或更多的网络,比如路由器,它就会有两个或多个IP地址,
所以,IP地址对应的实际上是计算机的网络接口,通常是网卡。数据链路和 IP 中的地址,分别指的是 MAC 地址和 IP 地址。
前者用来识别同一链路中不同的计算机,后者用来识别 TCP/IP 网络中互连的主机和路由器。在传输层也有这种类似于地址的概念,
那就是端口号。端口号用来识别同一台计算机中进行通信的不同应用程序。因此,它也被称为程序地址。一台计算机上同时可以运行多个程序。
传输层协议正是利用这些端口号识别本机中正在进行通信的应用程序,并准确地将数据传输。

端口

每个分层中,都会对所发送的数据附加一个首部,在这个首部中包含了该层必要的信息,如发送的目标地址以及协议相关信息。
通常,为协议提供的信息为包首部,所要发送的内容为数据。在下一层的角度看,从上一层收到的包全部都被认为是本层的数据。
网络中传输的数据包由两部分组成:一部分是协议所要用到的首部,另一部分是上一层传过来的数据。首部的结构由协议的具体规范详细定义。
在数据包的首部,明确标明了协议应该如何读取数据。反过来说,看到首部,也就能够了解该协议必要的信息以及所要处理的数据。包首部就像协议的脸。

数据包

IP协议负责把数据从一台计算机通过网络发送到另一台计算机。数据被分割成一小块一小块,然后通过IP包发送出去。
由于互联网链路复杂,两台计算机之间经常有多条线路,因此,路由器就负责决定如何把一个IP包转发出去。
IP包的特点是按块发送,途径多个路由,但不保证能到达,也不保证顺序到达。

IP地址实际上是一个32位整数(称为IPv4),以字符串表示的IP地址如192.168.0.1实际上是把32位整数按8位分组后的数字表示,目的是便于阅读。

IPv6地址实际上是一个128位整数,它是目前使用的IPv4的升级版,以字符串表示类似于2001:0db8:85a3:0042:1000:8a2e:0370:7334。

TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,
然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。
许多常用的更高级的协议都是建立在TCP协议基础上的,比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。

一个TCP报文除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。端口有什么作用?在两台计算机通信时,
只发IP地址是不够的,因为同一台计算机上跑着多个网络程序。一个TCP报文来了之后,到底是交给浏览器还是QQ,就需要端口号来区分。
每个网络程序都向操作系统申请唯一的端口号,这样,两个进程在两台计算机之间建立网络连接就需要各自的IP地址和各自的端口号。
一个进程也可能同时与多个计算机建立链接,因此它会申请很多端口。

传输层中的TCP和UDP

  • TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,当应用程序采用 TCP 发送消息时,虽然可以保证发送的顺序,
    但还是犹如没有任何间隔的数据流发送给接收端。TCP 为提供可靠性传输,实行“顺序控制”或“重发控制”机制。此外还具备“流控制(流量控制)”、
    “拥塞控制”、提高网络利用率等众多功能。
  • UDP 是不具有可靠性的数据报协议。细微的处理它会交给上层的应用去完成。在 UDP 的情况下,虽然可以确保发送消息的大小,却不能保证消息一定会到达。
    因此,应用有时会根据自己的需要进行重发处理。

TCP 和 UDP 的优缺点无法简单地、绝对地去做比较:TCP 用于在传输层有必要实现可靠传输的情况;
而在一方面,UDP 主要用于那些对高速传输和实时性有较高要求的通信或广播通信。TCP 和 UDP 应该根据应用的目的按需使用。

Python知识手册(更新中)

Python解释器种类以及特点

Python是一门解释型语言,代码需要通过解释器才能执行,Python存在多种解释器,
分别基于不同语言开发,每个解释器有不同的特点,但都能正常运行Python代码。

  • CPython:官方版本的解释器:CPython。这个解释器是用C语言开发的,所以叫CPython。
    在命令行下运行python就是启动CPython解释器。CPython是使用最广且被的Python解释器。
  • IPython:IPython是基于CPython之上的一个交互式解释器,也就是说,IPython只是在交互方式上有所增强,
    但是执行Python代码的功能和CPython是完全一样的。CPython用>>>作为提示符,而IPython用In [序号]:作为提示符。
  • PyPy:PyPy是另一个Python解释器,它的目标是执行速度。PyPy采用JIT技术,对Python代码进行动态编译(注意不是解释),
    所以可以显著提高Python代码的执行速度。绝大部分Python代码都可以在PyPy下运行,但是PyPy和CPython有一些是不同的,
    这就导致相同的Python代码在两种解释器下执行可能会有不同的结果。如果你的代码要放到PyPy下执行,就需要了解PyPy和CPython的不同点。
  • Jython:Jython是运行在Java平台上的Python解释器,可以直接把Python代码编译成Java字节码执行。
  • IronPython:IronPython和Jython类似,只不过IronPython是运行在微软.Net平台上的Python解释器,可以直接把Python代码编译成.Net的字节码。

什么是pep?

pep全称是Python Enhancement Proposals,其中Enhancement是增强改进意思,Proposals则可译为提案或建议书。
Python核心开发者主要通过邮件列表讨论问题、提议、计划等,PEP通常是汇总了多方信息,
经过了部分核心开发者review和认可,最终形成的正式文档,起到了对外公示的作用。

无论你是刚入门Python的小白、有一定经验的从业人员,还是资深的黑客,都应该阅读Python增强提案,阅读PEP至少有如下好处:

  1. 了解Python有哪些特性,它们与其它语言特性的差异,为什么要设计这些特性,是怎么设计的,怎样更好地运用它们。
  2. 跟进社区动态,获知业内的最佳实践方案,调整学习方向,改进工作业务的内容。
  3. 参与热点议题讨论,或者提交新的PEP,为Python社区贡献力量。

说到底,学会用Python编程,只是掌握了皮毛。PEP提案是深入了解Python的途径,是真正掌握Python语言的一把钥匙,也是得心应手使用Python的一本指南。

Python中的命名空间是什么?

python命名空间
命名空间,即Namespace,也成为名称空间或名字空间,指的是从名字到对象的一个映射关系,类似于字典中的键值对,
实际上,Python中很多命名空间的实现用的就是字典。不同命名空间是相互独立的,没有任何关系的,
所以同一个命名空间中不能有重名,但不同的命名空间是可以重名而没有任何影响。

Python命名空间按照变量定义的位置,可以划分为以下3类:

  • Built-in,内置命名空间,python自带的内建命名空间,任何模块均可以访问,存放着内置的函数和异常。
  • Global,全局命名空间,每个模块加载执行时创建的,记录了模块中定义的变量,包括模块中定义的函数、类、其他导入的模块、模块级的变量与常量。
  • Local,局部命名空间,每个函数、类所拥有的命名空间,记录了函数、类中定义的所有变量。

一个对象的属性集合,也构成了一个命名空间。但通常使用objname.attrname的间接方式访问属性,而不是直接访问,故不将其列入命名空间讨论。
(直接访问:直接使用名字访问的方式,如name,这种方式尝试在名字空间中搜索名字name。
间接访问:使用形如objname.attrname的方式,即属性引用,这种方式不会在命名空间中搜索名字attrname,而是搜索名字objname,再访问其属性。)

不同类型的命名空间有不同的生命周期:

  • 内置命名空间在Python解释器启动时创建,解释器退出时销毁;
  • 全局命名空间在模块被解释器读入时创建,解释器退出时销毁;
  • 局部命名空间,这里要区分函数以及类定义。函数的局部命名空间,在函数调用时创建,
    函数返回结果或抛出异常时被销毁(每一个递归函数都拥有自己的命名空间);
    类定义的命名空间,在解释器读到类定义(class关键字)时创建,类定义结束后销毁。

Python的LEGB规则

  • L-Local(function):函数内的名字空间
  • E-Enclosing function locals:外部嵌套函数的名字空间
  • G-Global(module):函数定义所在模块(文件)的名字空间
  • B-Builtion(Python):Python内置模块的名字空间

Python的命名空间是一个字典,字典内保存了变量名称与对象之间的映射关系,查找变量名就是在命名空间字典中查找键值对。
Python有多个命名空间,需要有规则来规定按照总样的顺序来查找命名空间,LEGB就是用来规定命名空间查找顺序的规则。
LEGB规定了查找一个名称的顺序为:local –> enclosing functions locals –> global –> builtin。

什么是Python中的文档Docstrings?

DocStrings 文档字符串是一个重要工具,用于解释文档程序,帮助你的程序文档更加简单易懂。
我们可以在函数体的第一行使用一对三个单引号 ''' 或者一对三个双引号 """ 来定义文档字符。
Docstrings 不是技术性的注释。当 Docstrings 在模块,函数,类,或者方法的前面出现的时候,
它在字节码中结束,并且变成__doc__特殊属性的对象。
DocStrings 文档字符串使用惯例:它的首行简述函数功能,第二行空行,第三行为函数的具体描述。

1
2
3
4
""" 简述函数功能

函数的具体描述。
"""

什么是PYTHONPATH?

PYTHONPATH是Python中一个重要的环境变量,用于在导入模块的时候搜索路径,可以通过如下方式访问。

1
2
import sys
print(sys.path)

由于在导入模块的时候,解释器会按照sys.path列表的顺序搜索,直到找到第一个模块,所以优先导入的模块为同一目录下的模块。
导入模块时搜索路径的顺序也可以改变.这里分两种情况:

  1. 通过sys.path.append(),sys.path.insert()等方法来改变,这种方法当重新启动解释器的时候,原来的设置会失效。
  2. 改变PYTHONPATH,这种设置方法永久有效。

性能优化

  1. 优化算法时间复杂度
  2. 减少冗余数据
  3. 合理使用 copy 和 deepcopy
  4. 使用 dict 或 set 查找元素
  5. 合理使用生成器 和 yield
  6. 优化循环,尽量减少循环内事务
  7. 优化包含多个表达式的顺序
  8. 使用join合并迭代器中的字符串
  9. 选择合适的格式化字符方式
  10. 不借助中间变量交换两个变量的值
  11. 尽量使用if is
  12. 尽量使用级联比较 x<y<z
  13. 使用 while 1 替换 while True
  14. 使用 ** 而不是 Pow
  15. 尽量使用 C 实现相同功能的包
  16. 使用最佳的反序列换方式
  17. 使用 C 扩展
  18. 并行编程
  19. 使用加速解释器
  20. 使用性能分析工具
  21. 使用Taichi

猴子补丁

猴子补丁(Monkey Patch)的名声不太好,因为它会在运行时动态修改模块、类或函数,通常是添加功能或修正缺陷。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 定义一个Dog类
class Dog:
def eat(self):
print("A dog is eating ...")


# 在类的外部给 Dog 类添加猴子补丁
def walk(self):
print("A dog is walking ...")


Dog.walk = walk

# 调用方式与类的内部定义的属性和方法一样
dog = Dog()
dog.eat()
dog.walk()

Python学习笔记

Python 知识图谱

Python 知识图谱

Python 入门

编译型语言和解释型语言

编程都是用的高级语言,计算机不能直接理解高级语言,只能理解和运行机器语言,
所以必须要把高级语言转成机器语言,计算机才能运行高级语言别写的程序。

  • 编译型
    编译型
  • 解释型
    解释型

Python程序文件的扩展名通常为.py。执行Python程序时,首先Python解释器将.py文件中的源代码翻译成中间码.pyc文件,
再由Python虚拟机逐条将中间码翻译成机器指令执行。

Python简介

Python(发音:/ˈpaɪθən/ )是一种强大的编程语言,它简单易学,提供众多高级的数据结构,让我们可以面向对象进行编程。
Python 的语法优雅,由于是一个解释性语言,更贴近人类自然语言,符合人类惯常的认识逻辑。

Python 的应用领域

Python 跨平台,它能够运行在所有的常见操作系统上。它在近期热门的大数据、科学研究、机器学习、人工智能等领域大显身手,
同时 Python 几乎在所有领域都有所应用,对于学习它来说十分划算。

Python 的优缺点

  • 优点
    1. 代码量少:这是简洁、优雅、明确、众多三方库带来的效果,可以让我们在处理同一个需求时相较其他语言撰写更少的代码量,大大节省了我们的时间,提高了效率;
    2. 应用范围广:可以做数据处理、机器学习、AI、图形视频处理、游戏、软件、网站等等,一种技能解决更多的问题,不用为了解决某个需要去专门学习对应的语言。
  • 缺点
    1. 慢:只有在大规模工业化的使用时才突显,对于我们日常使用差别很小,同时也可以用提高硬件配置的方案对冲,因为硬件的成本对于我们的时间来说时间更为宝贵。同时,目前也有一些解决方案来处理这方面的问题;
    2. 开源:这个其实是一个优点,开源可以带去分享,让我们有更多的学习资源,网络上 Python 的资料往往是比其他语言更多的。至于之前卖给别人软件担心别人看到源代码的顾虑,现在已经越来越没有必要,大多软件服务都是提供 Saas 服务、云服务的形式,将软件部署在自己的服务器上,给客户开一个账号就可以了。

Python 安装和配置

普通安装

Python安装

使用 Anaconda

Anaconda创建激活退出删除虚拟环境

Python 的数据类型和变量

类型体系

  • 类型 type
  • 空类型 NoneType
  • 数字 numeric
    • 整型 int
      • 布尔 bool
    • 浮点 float
    • 复数 complex
  • 容器 collections
    • 序列 sequence
      • 可变序列 abc.MutableSequence
        • 列表 list
        • 字节数组 bytearray
      • 不可变序列 ImmutableSequence
        • 元组 tuple
        • 字符串 string
        • 等差数列 range
        • 字节串 bytes
        • 内存视图 memoryview
    • 集合 set
      • 可变集合 set
      • 不可变集合 frozenset
    • 映射 mapping
      • 字典 dict
  • 上下文管理器 context manager
  • 类型注解的类型 type annotation
  • 其他内置类型
    • 迭代器 iterator
      • 生成器 generator
    • 模块 module
    • 类与类实例 class/instances
    • 函数 function
    • 方法 method
    • 代码 code
    • 省略符 Ellipsis
    • 未实现 NotImplemented
    • 栈帧 frame
  • 扩展类型 (内置库)
    • 高效数组 array.array
    • 枚举 enum.Enum
    • 有理数 fractions.Fraction
    • 指定精度浮点数 decimal.Decimal
    • 时间 datetime.datetime
    • 命名元组 collections.namedtuple
    • 双向队列 collections.deque
    • 有序字典 collections.OrderedDict
    • 映射链 collections.ChainMap
    • 计数器 collections.Counter
    • 默认字典 collections.defaultdict

常用数据类型

  1. bool
    布尔类型

  2. int
    Python 对 int 类型没有最大限制(相比之下, C++ 的 int 最大为 2147483647,超过这个数字会产生溢出)在生产环境中也要时刻提防,
    避免因为对边界条件判断不清而造成 bug 甚至 0day(危重安全漏洞)。

  3. float
    Python 对 float 类型依然有精度限制。

  4. str
    字符串是由独立字符组成的一个序列,通常包含在单引号(’’)双引号(””)或者三引号之中(’’’ ‘’’或””” “””,两者一样)。
    自从 Python2.5 开始,每次处理字符串的拼接操作时(str1 += str2),Python 首先会检测 str1 还有没有其他的引用。如果没有的话,
    就会尝试原地扩充字符串 buffer 的大小,而不是重新分配一块内存来创建新的字符串并拷贝。

    把 str 强制转换为 int 请用 int(),转为浮点数请用 float()。而在生产环境中使用强制转换时,请记得加上 try except。

  5. list
    列表是动态的,长度可变,可以随意的增加、删减或改变元素。列表的存储空间略大于元组,性能略逊于元组。

  6. tuple
    元组是静态的,长度大小固定,不可以对元素进行增加、删减或者改变操作。元组相对于列表更加轻量级,性能稍优。

  7. dict
    字典是一系列由键(key)和值(value)配对组成的元素的集合,在 Python3.7+,字典被确定为有序(注意:在 3.6 中,字典有序是一个 implementation detail,
    在 3.7 才正式成为语言特性,因此 3.6 中无法 100% 确保其有序性),而 3.6 之前是无序的,其长度大小可变,元素可以任意地删减和改变。
    相比于列表和元组,字典内部的哈希表存储结构,保证了其查找、插入、删除操作的高效性。

  8. set
    集合和字典基本相同,唯一的区别,就是集合没有键和值的配对,是一系列无序的、唯一的元素组合。

鸭子类型

鸭子类型(duck typing)在程序设计中是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,
而是由”当前方法和属性的集合”决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 这是一个鸭子(Duck)类
class Duck:
def eat(self):
print("A duck is eating...")

def walk(self):
print("A duck is walking...")

# 这是一个狗(Dog)类
class Dog:
def eat(self):
print("A dog is eating...")

def walk(self):
print("A dog is walking...")

def animal(obj):
obj.eat()
obj.walk()

if __name__ == '__main__':
animal(Duck())
animal(Dog())

可变与不可变

标准的数据类型中,有些是可变的,有些是不可以变的,不可变就意味差你不能对它进行操作,只能读取。
不可变数据:Number(数字)、String(字符串)、Tuple(元组);
可变数据:List(列表)、Dictionary(字典)、Set(集合)。

数据类型判断

Python 内置的 type() 函数可以查看数据的类型,如:

1
2
3
4
5
6
type(123) # 返回 int
# int

a = "Hello"
type(a) # 返回 str
# str

也可以用 isinstance 来判断它是不是一个指定的类型:

1
2
3
4
5
6
isinstance(123, int) # 123 是不是一个数字整型
# True
isinstance('123', int)
# False
isinstance(True, bool)
# True

自定义数据类型

在 Python 中,可以使用类(class)来定义自己的数据类型。类是一种用户自定义的数据类型,它可以封装数据和方法,
从而实现面向对象编程(Object Oriented Programming,OOP)的概念。

变量

Python 程序的流程

我习惯把“条件与循环”,叫做编程中的基本功。为什么称它为基本功呢?因为它控制着代码的逻辑,可以说是程序的中枢系统。
如果把写程序比作盖楼房,那么条件与循环就是楼房的根基,其他所有东西都是在此基础上构建而成。

条件语句

if 可以单独使用,但是 elif 和 else 必须和 if 同时搭配使用;而 If 条件语句的判断,除了 boolean 类型外,其他的最好显示出来。
常见判断条件结果

循环语句

  • while 循环
    对于 while 循环,它表示当 condition 满足时,一直重复循环内部的操作,直到 condition 不再满足,就跳出循环体。

    `

    1. [ While 之前的代码 ]

    2. [ While {表达式} ]
      1.1 { While 循环代码}

    3. [ While 之后的代码 ]
      `

    逻辑解释:

    1. 开始执行
    2. 先执行 0 行
    3. 执行 1 ,如果 {表达式} 为 True,执行 1.1
    4. 执行完 1.1 后再执行 1,如此往复
    5. 如果执行到 1 {表达式} 为 False, 执行 2
    6. 结束执行
  • for循环
    Python 中的数据结构只要是可迭代的(iterable),比如列表、集合等等,那么都可以通过下面这种方式遍历:

    1
    2
    for item in <iterable>:
    ...

    字典本身只有键是可迭代的,如果我们要遍历它的值或者是键值对,就需要通过其内置的函数 values() 或者 items() 实现。
    其中,values() 返回字典的值的集合,items() 返回键值对的集合。

通常来说,如果你只是遍历一个已知的集合,找出满足条件的元素,并进行相应的操作,那么使用 for 循环更加简洁。
但如果你需要在满足某个条件前,不停地重复某些操作,并且没有特定的集合需要去遍历,那么一般则会使用 while 循环。

同时需要注意的是,for 循环和 while 循环的效率问题。比如下面的 while 循环:

1
2
3
i = 0
while i < 1000000:
i += 1

和等价的 for 循环:

1
2
for i in range(0, 1000000):
pass

究竟哪个效率高呢?

要知道,range() 函数是直接由 C 语言写的,调用它速度非常快。而 while 循环中的“i += 1”这个操作,得通过 Python 的解释器间接调用底层的 C 语言;
并且这个简单的操作,又涉及到了对象的创建和删除(因为 i 是整型,是 immutable,i += 1 相当于 i = new int(i + 1))。所以,显然,for 循环的效率更胜一筹。

条件与循环的复用

在阅读代码的时候,你应该常常会发现,有很多将条件与循环并做一行的操作,例如:

1
2
expression1 if condition else expression2 for item in iterable

将这个表达式分解开来,其实就等同于下面这样的嵌套结构:

1
2
3
4
5
for item in iterable:
if condition:
expression1
else:
expression2

熟练之后,你会发现这种写法非常方便。当然,如果遇到逻辑很复杂的复用,你可能会觉得写成一行难以理解、容易出错。那种情况下,用正常的形式表达,也不失为一种好的规范和选择。

break 和 continue

所谓 continue,就是让程序跳过当前这层循环,继续执行下面的循环;而 break 则是指完全跳出所在的整个循环体。
在循环中适当加入 continue 和 break,往往能使程序更加简洁、易读。

rang() 函数

我们通常通过 range() 这个函数,拿到索引,再去遍历访问集合中的元素。比如下面的代码,遍历一个列表中的元素,当索引小于 5 时,打印输出:

1
2
3
4
5
6
7
8
9
10
l = [1, 2, 3, 4, 5, 6, 7]
for index in range(0, len(l)):
if index < 5:
print(l[index])

1
2
3
4
5

当我们同时需要索引和元素时,还有一种更简洁的方式,那就是通过 Python 内置的函数 enumerate()。用它来遍历集合,
不仅返回每个元素,并且还返回其对应的索引,这样一来,上面的例子就可以写成:

1
2
3
4
5
6
7
8
9
10
l = [1, 2, 3, 4, 5, 6, 7]
for index, item in enumerate(l):
if index < 5:
print(item)

1
2
3
4
5

with as 上下文管理器

match case 结构化模式匹配

assert 断言

Python 函数

定义函数

函数是Python里面组织代码的最小单元。函数就是为了实现某一功能的代码段,只要写好以后,就可以重复利用。

1
2
3
4
5
6
7
def my_func(message):
print('Got a message: {}'.format(message))

# 调用函数 my_func()
my_func('Hello World')
# 输出
Got a message: Hello World

其中:

  • def 是函数的声明;
  • my_func 是函数的名称;
  • 括号里面的 message 则是函数的参数;
  • 而 print 那行则是函数的主体部分,可以执行相应的语句;
  • 在函数最后,你可以返回调用结果(return 或 yield),也可以不返回。

和其他需要编译的语言(比如 C 语言)不一样的是,def 是可执行语句,这意味着函数直到被调用前,都是不存在的。
当程序调用函数时,def 语句才会创建一个新的函数对象,并赋予其名字。

主程序调用函数时,必须保证这个函数此前已经定义过,不然就会报错。如果我们在函数内部调用其他函数,函数间哪个声明在前、哪个在后就无所谓,因为 def 是可执行语句,函数在调用之前都不存在,我们只需保证调用时,所需的函数都已经声明定义

函数的参数

  • 必选参数
  • 默认参数
  • 可变参数
  • 关键字参数
  • 命名关键字参数

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

函数变量作用域

Python 函数中变量的作用域和其他语言类似。如果变量是在函数内部定义的,就称为局部变量,只在函数内部有效。一旦函数执行完毕,局部变量就会被回收,无法访问,比如下面的例子:

1
2
3
def read_text_from_file(file_path):
with open(file_path) as file:
...

我们在函数内部定义了 file 这个变量,这个变量只在 read_text_from_file 这个函数里有效,在函数外部则无法访问。

相对应的,全局变量则是定义在整个文件层次上的,比如下面这段代码:

1
2
3
4
5
MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
if value < MIN_VALUE or value > MAX_VALUE:
raise Exception('validation check fails')

这里的 MIN_VALUE 和 MAX_VALUE 就是全局变量,可以在文件内的任何地方被访问,当然在函数内部也是可以的。不过,我们不能在函数内部随意改变全局变量的值。比如,下面的写法就是错误的:

1
2
3
4
5
6
7
MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
...
MIN_VALUE += 1
...
validation_check(5)

如果运行这段代码,程序便会报错:UnboundLocalError: local variable 'MIN_VALUE' referenced before assignment

这是因为,Python 的解释器会默认函数内部的变量为局部变量,但是又发现局部变量 MIN_VALUE 并没有声明,因此就无法执行相关操作。所以,如果我们一定要在函数内部改变全局变量的值,就必须加上 global 这个声明:

1
2
3
4
5
6
7
8
MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
global MIN_VALUE
...
MIN_VALUE += 1
...
validation_check(5)

这里的 global 关键字,并不表示重新创建了一个全局变量 MIN_VALUE,而是告诉 Python 解释器,函数内部的变量 MIN_VALUE,
就是之前定义的全局变量,并不是新的全局变量,也不是局部变量。这样,程序就可以在函数内部访问全局变量,并修改它的值了。

另外,如果遇到函数内部局部变量和全局变量同名的情况,那么在函数内部,局部变量会覆盖全局变量,比如下面这种:

1
2
3
4
5
MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
MIN_VALUE = 3
...

在函数 validation_check() 内部,我们定义了和全局变量同名的局部变量 MIN_VALUE,那么,MIN_VALUE 在函数内部的值,就应该是 3 而不是 1 了。

类似的,对于嵌套函数来说,内部函数可以访问外部函数定义的变量,但是无法修改,若要修改,必须加上 nonlocal 这个关键字:

1
2
3
4
5
6
7
8
9
10
11
12
def outer():
x = "local"
def inner():
nonlocal x # nonlocal关键字表示这里的x就是外部函数outer定义的变量x
x = 'nonlocal'
print("inner:", x)
inner()
print("outer:", x)
outer()
# 输出
inner: nonlocal
outer: nonlocal

如果不加上 nonlocal 这个关键字,而内部函数的变量又和外部函数变量同名,那么同样的,内部函数变量会覆盖外部函数的变量。

1
2
3
4
5
6
7
8
9
10
11
def outer():
x = "local"
def inner():
x = 'nonlocal' # 这里的x是inner这个函数的局部变量
print("inner:", x)
inner()
print("outer:", x)
outer()
# 输出
inner: nonlocal
outer: local

函数嵌套

函数嵌套,就是指函数里面定义函数。

函数嵌套

1
2
3
4
5
6
def f1():
print('hello')
def f2():
print('world')
f2()
f1()

函数的嵌套,主要有下面两个方面的作用。

  1. 函数的嵌套能够保证内部函数的隐私。内部函数只能被外部函数所调用和访问,不会暴露在全局作用域,
    因此,如果你的函数内部有一些隐私数据(比如数据库的用户、密码等),不想暴露在外,
    那你就可以使用函数的的嵌套,将其封装在内部函数中,只通过外部函数来访问。
  2. 合理的使用函数嵌套,能够提高程序的运行效率。

闭包(closure)

什么是闭包?在一个内部函数中,对外部作用域的变量进行引用,(并且一般外部函数的返回值为内部函数),即使外部函数已经执行完毕,
内部函数仍然可以使用这些变量和参数,那么内部函数就被认为是闭包。构成条件:

  1. 函数嵌套
  2. 外部函数返回内部函数名
  3. 内部函数使用外部函数的变量

闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def example1(x):
# 嵌套函数inner(),则是一个闭包函数
def inner(y):
return x + y # 引用外部函数的变量

return inner

print(example1(6)(5))

def example2(a, b=1):
c = 100

def useC():
print(f'调用外部函数的变量,并打印:{c}') # 100
print(a + b) # 17

useC()

example2(6, 11) # 实参值会覆盖形参的值

闭包可以保存函数的状态信息,使函数的局部变量信息依然可以保存下来,将外层函数的变量持久地保存在内存中。
和上面讲到的嵌套函数优点类似,函数开头需要做一些额外工作,而你又需要多次调用这个函数时,将那些额外工作的代码放在外部函数,
就可以减少多次调用导致的不必要的开销,提高程序的运行效率。

以一个类似棋盘游戏的例子来说明。假设棋盘大小为50*50,左上角为坐标系原点(0,0),我需要一个函数,接收2个参数,分别为方向(direction),
步长(step),该函数控制棋子的运动。 这里需要说明的是,每次运动的起点都是上次运动结束的终点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def create(pos=origin):
def go(direction,step):
new_x = pos[0] + direction[0]*step
new_y = pos[1] + direction[1]*step
pos[0] = new_x
pos[1] = new_y
return pos

return go


player = create()
print(player([1,0],10))
print(player([0,1],20))
print(player([-1,0],10))

递归函数

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

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

递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。

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

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

函数式编程

所谓函数式编程,是指代码中每一块都是不可变的(immutable),都由纯函数(pure function)的形式组成。
这里的纯函数,是指函数本身相互独立、互不影响,对于相同的输入,总会有相同的输出,没有任何副作用。

函数式编程的优点,主要在于其纯函数和不可变的特性使程序更加健壮,易于调试(debug)和测试;缺点主要在于限制多,难写。
当然,Python 不同于一些语言(比如 Scala),它并不是一门函数式编程语言,不过,Python 也提供了一些函数式编程的特性,
值得我们了解和学习。

高阶函数

  • map
    函数 map(function, iterable) 的第一个参数是函数对象,第二个参数是一个可以遍历的集合,
    它表示对 iterable 的每一个元素,都运用 function 这个函数,最后返回一个新的可遍历的集合。
  • reduce
    函数 reduce(function, iterable) 的第一个参数是函数对象,它通常用来对一个集合做一些累积操作。
    function 同样是一个函数对象,规定它有两个参数,表示对 iterable 中的每个元素以及上一次调用后的结果,
    运用 function 进行计算,所以最后返回的是一个单独的数值。
  • filter
    函数filter(function, iterable) 表示对 iterable 中的每个元素,都使用 function 判断,并返回 True 或者 False,
    最后将返回 True 的元素组成一个新的可遍历的集合。
  • sorted
    函数 sorted(iterable, key=None, reverse=False) 表示对 iterable 中的元素,进行排序,默认是升序。
    key 是一个函数对象,它规定了如何对 iterable 中的元素进行排序。用sorted()排序的关键在于实现一个映射函数。

通常来说,在我们想对集合中的元素进行一些操作时,如果操作非常简单,比如相加、累积这种,
那么我们优先考虑 map()、filter()、reduce() 这类或者 list comprehension 的形式。至于这两种方式的选择:

  1. 在数据量非常多的情况下,比如机器学习的应用,那我们一般更倾向于函数式编程的表示,因为效率更高;
  2. 在数据量不多的情况下,并且你想要程序更加 Pythonic 的话,那么 list comprehension 也不失为一个好选择。

不过,如果你要对集合中的元素,做一些比较复杂的操作,那么,考虑到代码的可读性,我们通常会使用 for 循环,这样更加清晰明了。

lambda 匿名函数

匿名函数:lambda 表达式,也叫lambda函数,是一种简单的函数,不需要声明,直接定义,然后使用。

匿名函数的格式:

1
lambda argument1, argument2,... argumentN : expression

匿名函数的关键字是 lambda,之后是一系列的参数,然后用冒号隔开,最后则是由这些参数组成的表达式。

匿名函数 lambda 和常规函数一样,返回的都是一个函数对象(function object),它们的用法也极其相似,不过还是有下面几点区别。

  • lambda 是一个表达式(expression),并不是一个语句(statement)。
    所谓的表达式,就是用一系列“公式”去表达一个东西,比如x + 2、 x**2等等;而所谓的语句,则一定是完成了某些功能,比如赋值语句x = 1完成了赋值,
    print 语句print(x)完成了打印,条件语句 if x < 0:完成了选择功能等等。

    因此,lambda 可以用在一些常规函数 def 不能用的地方,比如,lambda 可以用在列表内部,而常规函数却不能:

    1
    2
    3
    [(lambda x: x*x)(x) for x in range(10)]
    # 输出
    [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

    再比如,lambda 可以被用作某些函数的参数,而常规函数 def 也不能:

    1
    2
    3
    4
    5
    l = [(1, 20), (3, 0), (9, 10), (2, -1)]
    l.sort(key=lambda x: x[1]) # 按列表中元组的第二个元素排序
    print(l)
    # 输出
    [(2, -1), (3, 0), (9, 10), (1, 20)]

    常规函数 def 必须通过其函数名被调用,因此必须首先被定义。但是作为一个表达式的 lambda,返回的函数对象就不需要名字了。

  • lambda 的主体是只有一行的简单表达式,并不能扩展成一个多行的代码块。

    这其实是出于设计的考虑。Python 之所以发明 lambda,就是为了让它和常规函数各司其职:lambda 专注于简单的任务,而常规函数则负责更复杂的多行逻辑。
    关于这点,Python 之父 Guido van Rossum 曾发了一篇文章解释,你有兴趣的话可以自己阅读。

为什么要使用匿名函数?

理论上来说,Python 中有匿名函数的地方,都可以被替换成等价的其他表达形式。一个 Python 程序是可以不用任何匿名函数的。
不过,在一些情况下,使用匿名函数 lambda,可以帮助我们大大简化代码的复杂度,提高代码的可读性。

通常,我们用函数的目的无非是这么几点:

  1. 减少代码的重复性;
  2. 模块化代码。

对于第一点,如果你的程序在不同地方包含了相同的代码,那么我们就会把这部分相同的代码写成一个函数,
并为它取一个名字,方便在相对应的不同地方调用。

对于第二点,如果你的一块儿代码是为了实现一个功能,但内容非常多,写在一起降低了代码的可读性,
那么通常我们也会把这部分代码单独写成一个函数,然后加以调用。

装饰器(Decorator)

装饰器是Python中最吸引人的特性,装饰器本质上还是一个函数,它可以让已有的函数不做任何改动的情况下增加功能。而实际工作中,装饰器通常运用在身份认证、日志记录、输入合理性检查以及缓存等多个领域中。合理使用装饰器,往往能极大地提高程序的可读性以及运行效率。

  • 简单装饰器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def my_decorator(func):
def wrapper():
print('wrapper of decorator')
func()
return wrapper

def greet():
print('hello world')

greet = my_decorator(greet)
greet()

# 输出
wrapper of decorator
hello world

这段代码中,变量 greet 指向了内部函数 wrapper(),而内部函数 wrapper() 中又会调用原函数 greet(),因此,最后调用 greet() 时,就会先打印’wrapper of decorator’,然后输出’hello world’。

这里的函数 my_decorator() 就是一个装饰器,它把真正需要执行的函数 greet() 包裹在其中,并且改变了它的行为,但是原函数 greet() 不变。

事实上,上述代码在 Python 中有更简单、更优雅的表示:

1
2
3
4
5
6
7
8
9
10
11
def my_decorator(func):
def wrapper():
print('wrapper of decorator')
func()
return wrapper

@my_decorator
def greet():
print('hello world')

greet()

这里的@,我们称之为语法糖,@my_decorator就相当于前面的greet=my_decorator(greet)语句,只不过更加简洁。因此,如果你的程序中有其它函数需要做类似的装饰,你只需在它们的上方加上@decorator就可以了,这样就大大提高了函数的重复利用和程序的可读性。

  • 带有参数的装饰器
1
2
3
4
5
def my_decorator(func):
def wrapper(*args, **kwargs):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
  • 带有自定义参数的装饰器
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
def repeat(num):
def my_decorator(func):
def wrapper(*args, **kwargs):
for i in range(num):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
return my_decorator


@repeat(4)
def greet(message):
print(message)

greet('hello world')

# 输出:
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world
  • 保留原函数元信息

greet() 函数被装饰以后,它的元信息变了。元信息告诉我们“它不再是以前的那个 greet() 函数,而是被 wrapper() 函数取代了”。为了解决这个问题,我们通常使用内置的装饰器@functools.wrap,它会帮助保留原函数的元信息(也就是将原函数的元信息,拷贝到对应的装饰器函数里)。

  • 类装饰器

类也可以作为装饰器。类装饰器主要依赖于函数__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
class Count:
def __init__(self, func):
self.func = func
self.num_calls = 0

def __call__(self, *args, **kwargs):
self.num_calls += 1
print('num of calls is: {}'.format(self.num_calls))
return self.func(*args, **kwargs)

@Count
def example():
print("hello world")

example()

# 输出
num of calls is: 1
hello world

example()

# 输出
num of calls is: 2
hello world

...
  • 装饰器嵌套

Python 也支持多个装饰器,比如写成下面这样的形式:

1
2
3
4
5
@decorator1
@decorator2
@decorator3
def func():
...

它的执行顺序从里到外,所以上面的语句也等效于下面这行代码:

1
decorator1(decorator2(decorator3(func)))

偏函数

偏函数是Python中一个很有用的特性,它可以让一个函数的某些参数固定,从而简化函数的调用。

Python 面向对象编程

传统的命令式语言有无数重复性代码,虽然函数的诞生减缓了许多重复性,但随着计算机的发展,只有函数依然不够,
需要把更加抽象的概念引入计算机才能缓解(而不是解决)这个问题,于是 OOP 应运而生。

面型对象是一种符合人类思维习惯的编程思想。客观世界中存在多种形态的事务,这些事物之间存在各种各样的联系。
在程序中使用对象来模拟现实中的事务,使用对象之间的关系来描述事物之间的联系,这种思想就是面对对象。

面向对象编程概念

  • 类:具有相同属性和功能的对象的抽象,这里对应 Python 的 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
36
class Document():

WELCOME_STR = 'Welcome! The context for this book is {}.'

def __init__(self, title, author, context):
print('init function called')
self.title = title
self.author = author
self.__context = context

# 类函数
@classmethod
def create_empty_book(cls, title, author):
return cls(title=title, author=author, context='nothing')

# 成员函数
def get_context_length(self):
return len(self.__context)

# 静态函数
@staticmethod
def get_welcome(context):
return Document.WELCOME_STR.format(context)


empty_book = Document.create_empty_book('What Every Man Thinks About Apart from Sex', 'Professor Sheridan Simove')


print(empty_book.get_context_length())
print(empty_book.get_welcome('indeed nothing'))

########## 输出 ##########

init function called
7
Welcome! The context for this book is indeed nothing.

三大特性

  • 封装

  • 继承
    类的继承,顾名思义,指的是一个类既拥有另一个类的特征,也拥有不同于另一个类的独特特征。在这里的第一个类叫做子类,
    另一个叫做父类,特征其实就是类的属性和函数。

    首先需要注意的是构造函数。每个类都有构造函数,继承类在生成对象的时候,是不会自动调用父类的构造函数的,因此你必须在 init() 函数中显式调用父类的构造函数。它们的执行顺序是 子类的构造函数 -> 父类的构造函数。

    抽象类是一种特殊的类,它生下来就是作为父类存在的,一旦对象化就会报错。同样,抽象函数定义在抽象类之中,
    子类必须重写该函数才能使用。相应的抽象函数,则是使用装饰器 @abstractmethod 来表示。

  • 多态

面向对象 VS 面向过程

Python 的错误、调试和测试

语法错误

语法错误就是解析代码时出现的错误。当代码不符合Python语法规则时,Python解释器在解析时就会报SyntaxError语法错误,还会指出最早探测到错误的语句。
语法错误是解释器无法容忍的,必须全部纠正才能运行。

异常

异常则是指程序的语法正确,也可以被执行,但在执行过程中遇到了错误,抛出了异常。

异常处理 try except

异常处理,通常使用 try 和 except 来解决。except block 只接受与它相匹配的异常类型并执行,如果程序抛出的异常并不匹配,那么程序照样会终止并退出。
很多时候,我们很难保证程序覆盖所有的异常类型,所以,更通常的做法,是在最后一个 except block,声明其处理的异常类型是 Exception。
Exception 是其他所有非系统异常的基类,能够匹配任意非系统异常。或者,也可以在 except 后面省略异常类型,这表示与任意异常相匹配(包括系统异常等)。

需要注意,当程序中存在多个 except block 时,最多只有一个 except block 会被执行。换句话说,如果多个 except 声明的异常类型都与实际相匹配,
那么只有最前面的 except block 会被执行,其他则被忽略。

异常处理中,还有一个很常见的用法是 finally,经常和 try、except 放在一起来用。无论发生什么情况,finally block 中的语句都会被执行,
哪怕前面的 try 和 excep block 中使用了 return 语句。因此,在 finally 中,我们通常会放一些无论如何都要执行的语句。

调试程序时看某些库的源代码,发现有如下代码读不懂,不理解后面这个from干什么用的。

1
2
3
4
5
6
7
8
9
try:
...
except KeyError:
raise **Error('') from None

try:
...
except Exception as exc:
raise **Error('') from exc

先看普通写法,控制台会输出什么,结果如下。控制台输出了2个异常发生的位置和原因,同时在2个提示中间输出一句话“在处理上述异常时,又发生了另一个异常”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try:
print(1/0)
except Exception as exc:
raise RuntimeError('程序执行过程中发生错误')

Traceback (most recent call last):
File "D:/*/tests.py", line 5, in <module>
print(1/0)
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "D:/*/tests.py", line 7, in <module>
raise RuntimeError('程序执行过程中发生错误')
RuntimeError: 程序执行过程中发生错误

再看raise **Error(‘’) from exc写法,控制台输出了什么,结果如下。控制台输出了2个异常发生的位置和原因,同时在2个提示中间输出一句话“上述异常是下列异常的直接原因”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try:
print(1/0)
except Exception as exc:
raise RuntimeError('程序执行过程中发生错误') from exc

Traceback (most recent call last):
File "D:/WorkSpace/backend/user/tests.py", line 5, in <module>
print(1/0)
ZeroDivisionError: division by zero

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "D:/WorkSpace/backend/user/tests.py", line 7, in <module>
raise RuntimeError('程序执行过程中发生错误') from exc
RuntimeError: 程序执行过程中发生错误

最后看raise **Error(‘’) from None写法,控制台输出了什么,结果如下。控制台只输出了我们写的抛出异常。

1
2
3
4
5
6
7
8
9
try:
print(1/0)
except Exception as exc:
raise RuntimeError('程序执行过程中发生错误') from None

Traceback (most recent call last):
File "D:/WorkSpace/backend/user/tests.py", line 7, in <module>
raise RuntimeError('程序执行过程中发生错误') from None
RuntimeError: 程序执行过程中发生错误

总结:from 会为异常对象设置 cause 属性表明异常的是由谁直接引起的。处理异常时发生了新的异常,
在不使用 from 时更倾向于新异常与正在处理的异常没有关系。而 from 则是能指出新异常是因旧异常直接引起的。
这样的异常之间的关联有助于后续对异常的分析和排查。from 语法会有个限制,就是第二个表达式必须是另一个异常类或实例。
如果在异常处理程序或 finally 块中引发异常,默认情况下,
异常机制会隐式工作会将先前的异常附加为新异常的 __context__属性。当然,
也可以通过with_traceback()方法为异常设置上下文__context__属性,这也能在traceback更好的显示异常信息。
from 还有个特别的用法:raise … from None ,它通过设置 suppress_context 属性指定来明确禁止异常关联。
在异常处理程序或finally块中引发异常,可以通过from来指定异常因谁引起的。这些手段都是为了得到更友好的异常回溯信息,
打印清晰的异常上下文。若要忽略上下文,则可以通过 raise … from None 来禁止自动显示异常上下文。

用户自定义异常

实际工作中,如果内置的异常类型无法满足我们的需求,或者为了让异常更加详细、可读,想增加一些异常类型的其他功能,我们可以自定义所需异常类型。

1
2
3
4
5
6
7
8
9
10
11
class MyInputError(Exception):
"""Exception raised when there're errors in input"""
def __init__(self, value): # 自定义异常类型的初始化
self.value = value
def __str__(self): # 自定义异常类型的string表达形式
return ("{} is invalid input".format(repr(self.value)))

try:
raise MyInputError(1) # 抛出MyInputError这个异常
except MyInputError as err:
print('error: {}'.format(err))

异常的使用场景与注意点

通常用在你不确定某段代码能否成功执行,也无法轻易判断的情况下,比如数据库的连接、读取等等。有一点切记,我们不能走向另一个极端——滥用异常处理。
正常的 flow-control(流程控制) 逻辑,不要使用异常处理,直接用条件语句解决就可以了。

Python 文件处理和IO编程

文件输入输出

事实上,计算机内核(kernel)对文件的处理相对比较复杂,涉及到内核模式、虚拟文件系统、锁和指针等一系列概念。

Python操作文件输入输出先要用 open() 函数拿到文件的指针。其中,第一个参数指定文件位置(相对位置或者绝对位置);
第二个参数,如果是 ‘r’ 表示读取,如果是’w’ 则表示写入,当然也可以用 ‘rw’ ,表示读写都要。
a 则是一个不太常用(但也很有用)的参数,表示追加(append),这样打开的文件,如果需要写入,会从原始文件的最末尾开始写入。

在拿到指针后,我们可以通过 read() 函数,来读取文件的全部内容。代码 text = fin.read() ,即表示把文件所有内容读取到内存中,
并赋值给变量 text。这么做自然也是有利有弊:

  • 优点是方便,接下来我们可以很方便地调用text;
  • 缺点是如果文件过大,一次性读取可能造成内存崩溃。

这时,我们可以给 read 指定参数 size ,用来表示读取的最大长度。还可以通过 readline() 函数,每次读取一行,
这种做法常用于数据挖掘(Data Mining)中的数据清洗,在写一些小的程序时非常轻便。如果每行之间没有关联,
这种做法也可以降低内存的压力。而 write() 函数,可以把参数中的字符串输出到文件中,也很容易理解。

这里我需要简单提一下 with 语句。open() 函数对应于 close() 函数,也就是说,如果你打开了文件,在完成读取任务后,就应该立刻关掉它。
而如果你使用了 with 语句,就不需要显式调用 close()。在 with 的语境下任务执行完毕后,close() 函数会被自动调用,代码也简洁很多。

最后需要注意的是,所有 I/O 都应该进行错误处理。因为 I/O 操作可能会有各种各样的情况出现,而一个健壮(robust)的程序,
需要能应对各种情况的发生,而不应该崩溃(故意设计的情况除外)。

Python 的并发编程

进程

线程

协程

Python的脚本编程与系统管理

Python 的正则表达式

模块与包

Python 常用内置模块

Python 常用第三方模块

Python 图形界面

Python 网络与 Web 编程

Python 电子邮件

Python 访问数据库

Python 的 Web 开发

使用 MicroPython

Python 高级特性

切片

迭代

列表生成式

生成器(Generator)

生成器是一个特殊的迭代器,并且它也是一个可迭代对象。有 2 种方式可以创建一个生成器:

  1. 生成器表达式
  2. 生成器函数

迭代器(Iterator)

迭代器是带状态的对象,它会记录当前迭代所在的位置,以方便下次迭代的时候获取正确的元素。一个对象要想使用 for 的方式迭代出容器内的所有数据,这就需要这个类实现「迭代器协议」。也就是说,一个类如果实现了迭代器协议,就可以称之为迭代器。

在 Python 中,实现迭代器协议就是实现以下 2 个方法:

  1. __iter__:这个方法返回对象本身,即 self
  2. __next__:这个方法每次返回迭代的值,在没有可迭代元素时,抛出 StopIteration 异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A:
"""A 实现了迭代器协议 它的实例就是一个迭代器"""
def __init__(self, n):
self.idx = 0
self.n = n

def __iter__(self):
return self

def __next__(self):
if self.idx < self.n:
val = self.idx
self.idx += 1
return val
else:
raise StopIteration()

元编程

C语言扩展

开发者有如下方法在Python代码中调用C编写的函数,每种方式都有各自的利弊。要明确为什么在Python中调用C,常见原因如下:
提升代码运行速度;
C语言中有很多传统类库,这些是想用的,但不想用Python重写;
想对内存到文件接口这种底层资源进行访问;

  • ctypes
    Python中ctypes模块可能是Python调用C最简单一种方法,ctypes模块提供了和C语言兼容的数据类型和函数来加载dll文件,因此,
    在调用时不需要对源文件做任何的修改。

    1. 编写C语言代码并保存。

      1
      2
      3
      4
      5
      #include<stdio.h>

      int add_int(int num1, int num2){
      return num1+num2;
      };
    2. 将C语言代码文件编译为.so文件。gcc -shared -Wl,-soname,adder -o adder.so -fPIC add.c

    3. 在Python代码中调用.so文件。

      1
      2
      3
      4
      5
      from ctypes import *

      adder = CDLL('./adder.so')
      res_int = adder.add_int(4,5)
      print(res_int)
  • SWIG

  • Python/C API

数据结构与算法

数据编码与处理

base64

JSON

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,它的设计意图是把所有事情都用设计的字符串来表示,
这样既方便在互联网上传递信息,也方便人进行阅读(相比一些 binary 的协议)。

  • json.dumps() 这个函数,接受 Python 的基本数据类型,然后将其序列化为 string。
  • json.loads() 这个函数,接受一个合法字符串,然后将其反序列化为 Python 的基本数据类型。

记得加上错误处理。不然,哪怕只是给 json.loads() 发送了一个非法字符串,而你没有 catch 到,程序就会崩溃了。

1
2
3
4
5
try:
data = json.loads(raw_data)
....
except JSONDecodeError as err:
print('JSONDecodeError: {}'.format(err))

新版本特性

3.10 引入了 match 语句

1
2
3
4
5
6
7
8
9
10
grade = 3
match grade:
case 1:
print('一年级')
case 2:
print('二年级')
case 3:
print('三年级')
case _:
print('未知年级')

3.9 增加了合并和更新运算符 (|=)

1
2
3
4
5
6
dictA = {'A': 'a', 'B': 'b'}
dictB = {1: 1, 2: 2}
dictC = {3: 3, 4: 4}

dictA |= dictB
merge_dict = dictB | dictC

3.8 增加了 F-strings 表达式

1
2
name = "foo"
f"Hello, my name is {name}"

3.8 新增海象运算符

1
2
3
4
5
6
7
8
lis = [1, 2, 3]
length = len(lis)
if length > 3:
print("lis列表拥有{}个元素,大于3。".format(length))

# 使用海象运算符后
if length := len(lis) > 3:
print("lis列表拥有{}个元素,大于3。".format(length))

3.5 新增类型注解

1
2
3
4
5
6
age: int = 8
name: str = "zhangsan"

from typing import List, Set, Dict, Tuple
lis: List[str] = ["zhangsan","lisi"]
dic: Dict[str, int] = {"zhangsan": 18}

函数参数和返回值的类型声明

1
2
def add(a: int, b: int) -> int:
return a + b

Anaconda创建、激活、退出、删除虚拟环境

创建虚拟环境

使用命令conda create -n your_env_name python=X.X创建Python版本为X.X、名字为your_env_name的虚拟环境。
your_env_name文件可以在Anaconda安装目录envs文件下找到。在不指定python版本时,自动安装最新python版本。
注意:至少需要指定python版本或者要安装的包。

1
2
conda create -n your_env_name python=2.7  # 指定Python版本为2.7
conda create -n your_env_name numpy matplotlib python=2.7 # 指定Python版本为2.7,同时安装numpy、matplotlib包

激活虚拟环境

1
conda activate your_env_name  # 激活指定名称虚拟环境

退出虚拟环境

1
conda deactivate  # 退出当前虚拟环境

删除虚拟环境中包

1
conda remove -n your_env_name package_name  # 删除指定名称虚拟环境中的指定包

删除虚拟环境

1
conda remove -n your_env_name --all  # 删除指定名称虚拟环境

conda常用命令

1
2
3
4
conda list:查看安装了哪些包。
conda install package_name(包名):安装包
conda env list 或 conda info -e:查看当前存在哪些虚拟环境
conda update conda:检查更新当前conda

Ubuntu安装与卸载Anaconda

安装

  1. Anaconda官网miniconda 找到对应安装包链接使用wget下载。

  2. cd到服务器上安装包所在位置,用以下命令安装(不建议使用root账户安装)。

    1
    bash Anaconda3-*-Linux-x86_64.sh
  3. 点击Enter,直到出现。

    Anaconda_install_1

  4. 输入yes,出现下图。

    Anaconda_install_2

  5. 点击Enter表示在默认位置安装,按Ctrl+C放弃安装,或者之当一个不同位置,这里直接默认,等待安装完成。

    Anaconda_install_3

  6. 出现下图表示安装完成,输入yes进行初始化。

    Anaconda_install_5

  7. 初始化完成后需要打开新的终端才能生效,如果不想默认激活base环境,可输入命令conda config --set auto_activate_base false关闭 ,最后输入conda --version检测是否安装功。

    Anaconda_install_5

  8. win10安装时勾选把conda添加到系统环境变量,安装完成后以管理员身份运行 power shell 并执行以下命令。

    1
    2
    set-ExecutionPolicy RemoteSigned
    conda init powershell

卸载

  1. 由于Anaconda的安装文件都包含在一个目录中,所以直接将该目录删除即可。到Anaconda安装目录,删除整个Anaconda目录。

  2. 到安装Anaconda的用户目录下执行ls -a查看文件,删除.conda .condarc等Anaconda相关文件,并编辑目录下.bashrc,删除下图代码,保存并关闭文件。

    Anaconda_delete_1

  3. 在终端执行如下命令,使其立即生效。

    1
    source ~/.bashrc

Hello World

你好,这个世界

从来到这个世界,一直在学习。学习如何说话、学习基础知识、学习如何做人。

遇到事情时,我们又是怎样思考?如何做的呢?有没有利用好已学的知识呢?

天马行空。东一榔头,西一棒子。想着该记下一点东西,再遇到能考虑更全面,做事情更完美。