python编码问题

参考

编码基础

ascii

这是ascii编码表, 从0到127, 每一个数字(code point)对应一个字符, 编码表就是这种对应
关系的映射表, 由于计算机只能处理字节, 所以还需要将数字保存为字节, ASCII编码
规定使用一个字节

一个字节提供的编码位是从0到255, 足以存下ascii编码表中所有字符, 为了支持更多语言, 人
们使用更多的字节来定义一个字符, 例如使用两个字节的gb2312, big5, 他们分别用来处
理中文简体和中文繁体

不同的语言使用不同的编码表导致大量不同的编码表出现, 于是人们就想能不能一套编码表包含
所有语言, 于是unicode就出现了, unicode总共有1100000个编码位(code point), 目前
用到了110000个, 每种语言中的每一个字符在unicode中都使用一个编码位, 有了编码位与字
符之间的映射关系后, 还需要将编码位转成字节提供给计算机处理, ASCII编码就是简单的使
用一个字节来保存编码位, unicode标准定义了多种编码方式(encoding)来将编码位转成字
节, 例如UTF-8, UTF-16, UTF-32, UCS-2, UCS-4等等, UTF-8是我们常用的编
码, 它使用变长字节数来表示一个unicode编码位, ASCII中的字符在UTF-8中也是使用一
个字节, 且相同, 所以ASCIIUTF-8的子集

python2中的编码

python2中有两种字符串类型, 一种是str类型, 它储存的是字节, 另一种是unicode, 它储
存的是编码位, 如下:

1
2
3
4
5
6
s1 = 'hi 你好'
print(type(s1))# <type 'str'>

# use a 'u' prefix
s2 = u'hi 你好'
print(type(s2))# <type 'unicode'>

str类型的字符串保存的是字节, 所以使用len函数计算长度返回的将是字节数, 而unicode
类型的字符串保存的是编码位, 将返回编码位个数

1
2
3
print(len(s1))# 9

print(len(s2))# 5

strunicode的字符串可以互相转换, unicode字符串可以通过encode方法转变成储存
字节的str类型, str字符串可以通过decode转变成unicode

1
2
3
print(s1.decode('utf-8'))# hi 你好

print(s2.encode('utf-8'))# hi 你好

s1str类型, 保存的是计算机使用的字节码(Bytes), 它使用decode方法根据UTF-8
编码表可以解码成unicode, s2unicode类型, 它可以使用encode方法, 同样根据
UTF-8编码表可以转变成字节码(Bytes), 如果使用错误的编码类型, 将返回编码错误

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
print(s1.decode('ascii'))
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 3: ordinal not in range(128)

# 解释: s1 中的中文字符编码超出了ascii所能表示的范围, 所以解码失败

# 不提供编码参数的话将使用默认编码
# 查看环境默认编码: sys.getdefaultencoding(), 我这里是 ascii
print(s2.encode())
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# UnicodeEncodeError: 'ascii' codec can't encode characters in position 3-4: ordinal not in range(128)

# 解释: s2中的中文编码位超过了ascii所能表示的范围, 所以编码失败

print(s1.decode('ascii', 'replace'))# hi ������
print(s2.encode('ascii', 'replace'))# hi ??

# 解释: 第二个参数使用replace将会使用'?'代替不可编码/解码的字符
# decode时返回了6个问号, 而encode只有两个, 这是因为s1字符串内部储存的是字节, 而中文
# '哈哈'使用了六个Bytes, 使用ascii解码时将六个字节当成六个字符处理, 因为超过了ascii
# 所能表示的范围, 所以返回六个问号, 而s2储存的时编码位, 其内部的中文'哈哈'是两个
# unicode编码位, 使用ascii编码时是当作两个字符处理, 超过了ascii表示范围, 返回两个问
# 号

python2中很多地方都会存在隐性转换, 比如尝试将unicode字符串和str类型(字节)拼接起
来, python2先会自动将str字节字符串decode成默认编码类型的字符串, 然后再拼接

1
2
3
4
5
6
7
8
9
print(u'hi' + ' nihao')# hi nihao

print(u'hi' + ' 你好')
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 1: ordinal not in range(128)

# 解释: python2首先会将' 你好'按默认编码ASCII解码, 但是中文是超出了ASCII表示的范围,
# 所以报UnicodeDecodeError

python2中隐性的字符串类型转换在所有字符都是ascii时表现正常, 当存在其他编码字符时就
可能导致错误

python3中的编码

python3中也有两种类型的字符串, 直接由引号括起来的字符串是str类型, 普通字符串加b
前缀的字符串属于bytes类型

1
2
3
4
5
s1 = 'hi 你好'
print(type(s1))# <class 'str'>

s2 = b'hi how old are you'
print(type(s2))# <class 'bytes'>

python2中的str 等价于 python3中的bytes
python2中的unicode 等价于 python3中的str

python3中不会做任何隐性的类型转换, 如果拼接strbytes类型的字符串, 将会抛出
TypeError错误

编码问题最佳实现

编码问题并不复杂, 只要遵循以下几点就可以避免大部分的编码问题, python2/3同样适用

1.bytes on the outside, Unicode on the inside.

进出你的程序的数据必须是bytes类型, 在程序内部处理unicode字符串

2.know what you have

必须知道自己处理的数据是什么类型的字符串, 要确定到底是bytes还是unicdoe类型,
如果是bytes类型, 还需要知道它的编码是什么, 才可以根据对应的编码将bytes转换
unicode, 但是, 数据的编码是无法仅仅根据bytes类型的字符串本身得出来, 要么
从数据源处得知, 要么只能靠猜

总结

1.编码表保存编码位与字符之间映射关系, unicode支持所有的语言字符, 它兼容
ascii

2.编码位(code points)是数字, 需要转换成字节才能让计算机处理, 转换规则由编
码方式决定, 例如ASCII编码使用一个字节保存它所支持的127个字符, UTF-8使
用可变长字节数保存unicode字符, 其中, ASCII中的字符在UTF-8中是完全一
样的, 所以ASCIIUTF-8的子集

3.python2有两种类型的字符串, strunicode,
str保存的是bytes,
unicode保存的是unicode,
str通过decode方法可以转换成unicode,
unicdoe通过encode方法可以转换成bytes,

4.python3同样有两种类型的字符串, strbytes,
str保存的是unicode,
bytes保存的是bytes,
str通过encode方法转换成bytes
bytes通过decode方法转换成str

5.python2中存在隐性转换字符串类型的现象, 例如拼接不同类型的字符串, 则会根据
默认编码自动将bytes转换成unicode再拼接, python3中拼接不同类型字符串则会
报错, tip1: 不要依赖自动转换, 任何时候都需要清楚的知道自己处理的字符串类型

6.程序内部处理的字符串都应该是unicode类型, 出去的都应该是bytes类型

python中的迭代器和生成器

复习一下python的高级特性

可迭代对象

当需要迭代一个对象时, 会自动调用iter函数, iter函数会检查对象是否实现了
__iter__方法, 如果有则调用对象的__iter__方法获取一个迭代器, 没有则继续检查对象
是否实现了__getitem__方法, 如果有, python会自动创建一个迭代器, 使用从0开始的索引
为参数调用__getitem__, 该方法将返回对应索引的元素, 如果__getitem__方法也不存
在, 则抛出TypeError异常, 提示对象不可迭代

可迭代对象(iterable)就是使用内置函数iter可以获取迭代器的对象, 该对象要么实现了能
够直接返回迭代器的__iter__方法, 要么实现了接受从0开始的索引为参数的__getitem__
方法

由于__iter__方法需要返回迭代器, 先给出实现__getitem__方法的可迭代对象示例

1
2
3
4
5
6
7
8
9
10
class it1:
def __init__(self, text):
self.text = text
def __getitem__(self, index):
return self.text[index]

obj = it1('abcdefg')
for i in obj:
# 依次打印 a, b, c, d, e, f, g
print(i)

迭代器

迭代器(iterator)就是实现了__next__方法的对象, 该方法也是返回序列的当前元素, 它跟
__getitem__的区别就是, 当前元素的索引是保存在迭代器对象实例中, 而__getitem__
由python解释器自动创建的迭代器提供的索引

迭代器实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class it2:
def __init__(self, text):
self.text = text
self.index = 0
def __next__(self):
try:
curr_char = self.text[self.index]
except IndexError:
raise StopIteration()
self.index += 1

obj = it2('abcdefg')
print(next(i2))# 输出 a
print(next(i2))# 输出 b
print(next(i2))# 输出 c
print(next(i2))# 输出 d
print(next(i2))# 输出 e
print(next(i2))# 输出 f
print(next(i2))# 输出 g

使用for迭代的过程

我们使用for语句迭代一个对象(obj)过程如下:

  1. 使用内置函数iter获取obj对象的迭代器 (分两种情况, 第一种是通过__iter__直接
    获取对象的迭代器, 第二种是通过python解释器自动创建迭代器
  2. 获得迭代器后, 循环调用next(iter)获取元素, next函数调用迭代器内部的__next__
    方法以获得序列中的元素, 直到__next__方法抛出StopIteration结束循环

生成器

生成器是迭代器的子集, 所以它也属于迭代器, 只是它的实现方式不一样, 生成器更简洁, 而且
它不需要像迭代器一样维护一个索引, 它是通过yield语句来实现的

生成器函数实例

1
2
3
4
5
6
7
8
def it3(text):
for curr_char in text:
yield curr_char

obj = it3('abcdefg')
for i in obj:
# 依次打印 a, b, c, d, e, f, g
print(i)

代码在执行到yield时, 将返回其后的值, 然后并不像return语句一样退出, 而是挂起函数
的状态, 下次再从当前胃继续执行

生成器表达式

类似列表推导式, 只是方括号替换成大括号

1
2
3
4
5
6
7
8
9
10
11
12
# 列表推导式
li = [x**2 for x in range(5)]
print(li)# 输出 [0, 1, 4, 9, 16]
# 生成器表达式
li = (x**2 for x in range(5))
print(li)# <generator object <genexpr> at 0x7ff563aa6938>
print(next(li))# 输出 0
print(next(li))# 输出 1
print(next(li))# 输出 4
print(next(li))# 输出 9
print(next(li))# 输出 16
print(next(li))# 自动抛出StopIteration异常

生成器除了实现简洁外, 它还有节省内存的优点, 它不是一次性构建整个结果列表, 它有延迟计
算的特点, 按需产生结果

select poll epoll 初探

select poll epoll 初探

这三个都是所谓的synchronous I/O multiplexing(同步I/O多路复用),在学习这个之前,有必要了解一下常见的I/O模型,
而在了解I/O模型前又有必要了解一下内核空间及用户空间。

1 内核空间/用户空间

所谓的内核空间就是操作系统使用的内存空间,用户空间就是用户进程使用的内存空间,为了操作系统的安全,用户进程不能直接
操作操作系统内存空间,所以,内核空间和用户空间的数据交换需要通过内核提供的一组接口进行,也就是常说的系统调用。

2 I/O模型

I/O模型就是I/O操作的方式,I/O操作就是用户进程与外界数据交换的操作,而与外界进行数据交换需要操作系统内核提供支持,
常见的I/O模型有四种,阻塞I/O、非阻塞I/O、多路复用I/O、信号驱动I/O、异步I/O。

2.1 阻塞I/O(blocking I/O)

阻塞I/O就是进程请求I/O操作时会阻塞,具体一点就是用户进程向操作系统发起I/O请求,例如read操作中调用了系统函数recvfrom,
内核收到请求后就开始了I/O操作的第一个阶段————准备数据,如果是网络I/O请求,那么内核需要等待足够的数据到来,这时数据
保存在内核缓冲区,在这这个等待过程中,用户进程一直处于阻止塞状态(当然用户的I/O请求中也可以设置非阻塞,下面会提到),
当数据准备好了,内核就会将数据从内核缓冲区中拷贝到用户空间。用户感知到的就是调用recvfrom后,进程阻塞,直到数据准备
好了返回给用户进程。这就是最易理解的I/O模式。

2.2 非阻塞I/O(nonblocking I/O)

可以设置socket使之成为non-blocking,继续以读操作为例子,在非阻塞I/O模式中,当用户进程调用recvfrom时,内核立即返回,
在内核还未准备好数据时返回error,用户进程可以隔一段时间再次调用recvfrom,直到内核准备好数据,就会把数据考哦被到用户
空间,完成I/O操作。

2.3 I/O多路复用

I/O多路复用类似非阻塞I/O,不一样的是使用多路复用机制可以使一个进程同时处理多个网络连接的I/O,基本原理是通过select
poll epoll会不断轮询多个socket,当其中某个I/O就绪就会通知用户进程。

2.3 异步I/O(Asynchronous I/O)

异步I/O是一种完全不会阻塞的I/O模型,之所以说完全不会阻塞是因为使用这种方式发起I/O请求后,内核立即返回,之后内核准备
数据,数据准备好后直接拷贝至用户空间,最后向用户进程发送一个signal,告诉进程I/O操作完成。而非阻塞I/O模型中,非阻塞
的是内核数据准备阶段,在数据拷贝阶段还是会阻塞用户进程。

2.4 信号驱动I/O(Signal drived I/O)

使用信号驱动I/O时,当网络套接字可读后,内核通过发送SIGIO信号通知应用进程,于是应用可以开始读取数据。有时也称此方式为
异步I/O。但是严格讲,该方式并不能算真正的异步I/O,因为实际读取数据到应用进程缓存的工作仍然是由应用自己负责的,即需要
用户主动调用系统函数去读取数据。

3 I/O多路复用之select poll epoll

select poll epoll都属于I/O多路复用模式的实现。

3.1 select

1
2
3
4
5
6
7
8
9
10
/*
* @param n: readfds writefds exceptfds中的fd数量不能超过n+1
* @param readfds: 需要监控的读操作fd
* @param writefds: 需要监控的写操作fd
* @param exceptfds: 需要监控的异常fd
* @param timeout: 指定select可以等待的时间间隔,如果设成0秒和0毫秒,则select不等待,立即测试fd集中的所有fd,
* 如果设成NULL,等待无限久,可以被信号中断
* @return 成功:返回三个集合中所有被设置的bit位数量|超时:返回0|失败:返回-1
*/
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

fd_set是以位图的形式来存储这些文件描述符,n为位图的长度。
select优点:
目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点
select缺点:
1.每次调用 select(),都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大,同时每次调用 select() 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大。
2.单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低

3.2 poll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* @param fds: 需要监控的文吉安描述符及需要监控的事件
* @param nfds: fds中的文件描述符总数
* @param timeout: 和select一样但是两个字段单位分别是秒和微秒
*
* @return 成功:返回正整数,返回又时间发生的fd数量|超时:返回0(也表示无事件发生)|失败:返回-1
*/
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};

poll使用链表保存文件描述符,因此没有了监视文件数量的限制。

3.3 epoll

epoll实现机制和select/poll完全不一样,设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?
在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。

1
2
3
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epoll的用法一句话描述就是三步曲:
第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。
第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。
第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。

原码、反码、补码学习

原码、反码、补码学习

思考:

计算机中数的表示是怎样的?
计算机是如何做运算?

计算机是二进制世界

众所周知,计算机世界里只有0和1,那么计算机中的数只能用二进制来表示,
举几个二进制数字例子:

括号中为二进制,其余为十进制
(0000 0001) = 1
(1111 1111) = 257

如何表示一个负数呢?很自然就能想到,使用一位用来表示符号,一般二进制数的第一位来表示符号,
0表示正数,1表示负数,例子:

(0000 0001) => 1
(1000 0001) => -1

这种表示方式就是所谓的原码,它很简单,容易被人脑所理解。不过使用它进行运算时,对计算机并不太友好,

计算机数的运算

计算机是使用逻辑电路进行运算,下图就是简单的加法器:
1-bit full adder
图 一位简单全加器

这个加法器有三个输入两个输出,一个加法器可以完成两位的加法运算,串联多个加法器可以做多位加法。
实现了加法器,但是如何做减法运算呢?难道还要实现一个减法器?答案是不需要,我们知道a + b = a + (-b)
我们可以把减法转换成加法来计算,不过这个时候,使用原码就有问题了,不信我们试试看:

(84-81) = 84+(-81) = 3
(0101 0100) 84的原码
(1101 0001) -81的原码
————————————
1(0010 0101)

结果显然是错的,原码没办法使用加法器做加法运算,反码便出现了。

反码

原码和反码可以互相转换,转换规则就是:

正数的反码和原码相同
负数的反码是原码除符号位外,其余位取反的结果

举个例子:

(15-125) = 15+(-125)
(0000 1111) 15的原码 => (0000 1111) 15的反码
(1111 1101) -125的原码 => (1000 0010) -125的反码
————————————————————
(1001 0001)

得出来的结果也是反码,转换成原码后是(1110 1110),即为-110,结果正确!
不过,使用反码需要注意两点:

1.相加得到的结果也是反码,需要转换成原码
2.溢出一位后需要在最低位+1

使用补码,就没有问题了。

补码

正数的补码就是其本身,负数的补码就是其反码的最低位加1。

2-1 = 2+(-1) = 1
(0000 0010) 2的原码 => (0000 0010) 2的反码 => (0000 0010) 补码
(1000 0001) -1的原码 => (1111 1110) -1的反码 => (1111 1111) 补码
————————————————————————————————
(0000 0001)

隐藏其中的数学原理

todo

PEP333——Python Web服务器网关接口

PEP 333——Python Web服务器网关接口 v1.0

Original: PEP 333——Python Web Server Gateway Interface v1.0

PEP: 333
Title: Python Web Server Gateway Interface v1.0
Author: Phillip J. Eby
Discussions-To: Python Web-SIG
Status: Final
Type: Informational
Created: 07-Dec-2003
Post-History: 07-Dec-2003, 08-Aug-2004, 20-Aug-2004, 27-Aug-2004, 27-Sep-2010
Superseded-By: 3333

前言

注意:此份规范的更新版本支持python3.x、包含社区勘误表、附录表,以及说明,如果这是你想要的,那么去看PEP 3333

摘要

这篇文档介绍了在服务器和python web应用程序(或框架)之间建立一套标准接口,使python web应用程序可以在多种web服务器之间运行,提高可移植性。

原理及目标

目前python拥有大量web应用框架,
例如Zope,Quixote,Webware,SkunkWeb,PSO,以及Twisted Web,
等等1
数量众多的选择对新手来说是个头疼的问题,
因为,他们对web框架的选择将会限制所能使用的web服务器,反之亦然。

相比之下,尽管java也有大量web应用框架,
但是java servlet API使得任何使用java框架写的应用程序都可以在任何支持java sevlet的web服务器上运行。

python也需要那样一套可用的广泛传播的API,
无论服务器是用python语言写的(Medusa),
或者把python封装进去(mod_python),
或者通过网关协议调用python解释器来执行程序(CGI,FastCGI,etc)都将使得对框架的选择脱离web服务器的限制,
让用户自由选择最适合他们的组合,
而且还能使框架和web服务器的开发者们专注于做好他们擅长的工作。

因此,这份PEP提议在web服务器和web应用程序或框架之间建立一套简单而通用的接口:
Python Web服务器网关接口(WSGI)。

但是仅仅起草一份WSGI规范并不能解决python web服务器和框架现有的状况,
服务器、框架作者和维护人员必须真正的实现WSGI才会有效果。

然而,由于还没有任何服务器或者框架支持WSGI,
最初实现对WSGI的支持对作者来说并没有立即产生效果。
因此,WSGI规范必须简洁,
至少能使作者实现接口的成本比较低。

因此,服务器和框架两端的接口实现起来都比较简单,这对WSGI接口的实用性都非常重要,
对任何设计决策来说都是基本原则。

注意,然而,框架实现对WSGI的支持并不像使用WSGI接口那般简单。
WSGI提供一个完全没有多余功能的接口给框架作者,
因为多余的类似响应对象和cookie的处理将会阻碍框架中已实现的相同功能。
再一次,WSGI的目标是使服务器和应用或框架之间交互更简单,而不是创造一个新的框架。

还要注意的是,这个目标阻止WSGI需要任何已部署python版本中不存在的东西。
因此,这份规范并不提议或者要求使用新标准库,
另外WSGI不需要任何高于python2.2.2版本的特性。
(对未来python版本中,最好是在标准库提供的web服务器中实现对接口的支持。)

另外,对现存以及未来的框架和服务器实现起来都应该简单,
它还要能够简单创建请求预处理器和响应后处理器(?),
其他基于WSGI的“中间件”对他们的服务器来说像一个应用,
对他们的应用来说却像一个服务器。

如果中间件能够既简单又强健,
而且WSGI被服务器和框架广泛支持,
那么它将允许一个全新的python web应用框架:
一个由低耦合WSGI中间件组件组成的框架。
甚至框架作者可能选择重构他们框架中的服务以提供上述架构,
使用WSGI更像使用一个库,
而不是一个庞大的框架。
这将使得应用开发者能够选择最佳组件以应对特殊的应用场景,
而不是不得不接受同一个框架中不好的一面。

当然,要实现上述愿景,
还有一条很长的路要走。
在此期间,有足够的短期目标使WSGI能够在任何框架和服务器上使用。

最后,需要提到的是,目前的WSGI版本并没有规定任何特殊的结构来使用web服务器或网关部署一个应用。
目前,这是由服务器或网关定义的。
当足够多的服务器和框架实现了WSGI,可以提供多种不同的部署需求,
那么需要创建另一份PEP,为WSGI服务器和应用框架描述一个部署标准。

规范概览

WSGI接口有两端:服务器或网关端,应用或框架端。
应用端提供一个可调用对象给服务器端调用。
如何提供这个可调用对象的细节则由服务器或网关决定。
假设一些服务器或网关会要求应用提供一个部署脚本,用来提供给服务器或网关创建一个实例,
来提供一个应用程序对象。
其他的服务器或网关可能使用配置文件或者其他结构来指定从哪里引入(或者说获得)应用程序对象。

另外为了使通信双方更加纯粹,
可以使用中间件来同时实现WSGI的两端。
那么他对服务器来说是一个应用,
对应用过来说则是扮演服务器的角色,
还能用来提供扩展APIs,内容转换,导航,以及其他有用的功能。

贯穿这篇规范说明,
我们会使用“可调用对象”来表示一个“函数”,“方法”,“类”或者一个有call方法的实例。
至于这个“可调用搞对象”具体是什么,这取决于服务器,网关,或者应用根据需要选择合适的技术来实现可调用对象。

应用端/框架端

应用对象就是一个简单的接受两个参数的可调用对象,
术语“对象”并不是特指一个真正的对象实例:
一个函数,方法,类或者实现了call方法的实例都可以作为一个应用对象。
应用对象必须要能够调用多次,
几乎所有的服务器或网关(除了CGI)都会有重复的请求。

(注意:
虽然我们称它为应用对象,
但是应用开发者不应该把WSGI当作一个web programming API!
应用开发者们还是应该继续使用现有的,高级框架服务来开发他们的应用。
WSGI是框架和服务器开发者们的工具,而且不打算直接支持应用开发者。)

这里有两个应用对象的例子;一个是函数,另一个是类:

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
def simple_app(environ, start_response):
   """最简单的应用对象"""
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return ['Hello world!\n']

class AppClass:
   """实现和上面例子同样功能的应用对象,但是使用类来实现

   (注意:‘AppClass’是一个应用,通过调用它返回一个‘AppClass’的实例,
   一个通过可迭代返回值的应用对象。)

   如果我们想使用‘AppClass’的实例作为应用对象,那就应该实现__call__
   方法,那样就能用过调用实例来执行应用,而且需要创建一个实例供服务器
   或网关使用。
   """

def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response

def __iter__(self):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
self.start(status, response_headers)
yield "Hello world!\n"

服务器端/网关端

HTTP客户端每次发送一个请求,服务器或网关都会调用应用对象一次,
为了好理解,这里有一个简单的CGI网关,
通过一个接受应用对象的函数实现。
注意这个简单的例子只能处理有限的错误状况,
因为一个未捕捉的异常默认将会通过sys.stderr输出,并且由服务器记录日志。

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
import os, sys

def run_with_cgi(application):

environ = dict(os.environ.items())
environ['wsgi.input'] = sys.stdin
environ['wsgi.errors'] = sys.stderr
environ['wsgi.version'] = (1, 0)
environ['wsgi.multithread'] = False
environ['wsgi.multiprocess'] = True
environ['wsgi.run_once'] = True

if environ.get('HTTPS', 'off') in ('on', '1'):
environ['wsgi.url_scheme'] = 'https'
else:
environ['wsgi.url_scheme'] = 'http'

headers_set = []
headers_sent = []

def write(data):
if not headers_set:
raise AssertionError("write() before start_response()")

elif not headers_sent:
# Before the first output, send the stored headers
status, response_headers = headers_sent[:] = headers_set
sys.stdout.write('Status: %s\r\n' % status)
for header in response_headers:
sys.stdout.write('%s: %s\r\n' % header)
sys.stdout.write('\r\n')

sys.stdout.write(data)
sys.stdout.flush()

def start_response(status, response_headers, exc_info=None):
if exc_info:
try:
if headers_sent:
# Re-raise original exception if headers sent
raise exc_info[0], exc_info[1], exc_info[2]
finally:
exc_info = None # avoid dangling circular ref
elif headers_set:
raise AssertionError("Headers already set!")

headers_set[:] = [status, response_headers]
return write

result = application(environ, start_response)
try:
for data in result:
if data: # don't send headers until body appears
write(data)
if not headers_sent:
write('') # send headers now if body was empty
finally:
if hasattr(result, 'close'):
result.close()

中间件:同时扮演两端角色的组件

“中间件”面对应用的时候扮演服务器的角色,而面对服务器的时候扮演应用的角色,
它提供以下功能:

  • 在相应的重写环境之后,根据目标URL把请求路由到不同的应用对象。
  • 允许多个应用或者框架并行的运行在同一个进程中。
  • 通过转发请求以及网络响应来实现负载平衡和远程处理。
  • 执行内容预处理,例如应用XSL样式表。

总之,中间件的存在是使“服务器/网关”端和“应用/框架”端之间透明,不需要任何特殊支持使两端能够交互。
想把中间件合并到应用的用户只需提供中间件组件给服务器,
中间件就好像是一个应用,配置好中间件使之去调用应用,
中间件又好像是一个服务器,代替服务器去调用应用。
当然,中间件封装的“应用”可能是另一个中间件封装的另一个应用,
如此循环,创建了所谓的“中间件堆栈”。

在极大程度上,中间件必须符合WSGI规范服务端和应用端的限制和要求。
在某些例子,对中间件的约束比单纯对服务器或应用还要严格,
这些要点将在规范中注明。

下面是一个中间件例子(非正式),它的作用是把文本转换成pig Latin式的儿童黑话(…?),
代码保存在Joe Strout的piglatin.py文件中。
(注意:一个健壮的中间件可能会使用更粗鲁的方式检查内容类型,
以及内容的编码格式。
另外,这个例子忽略了一个单词可能跨越块边界。)

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
from piglatin import piglatin

class LatinIter:

"""Transform iterated output to piglatin, if it's okay to do so

Note that the "okayness" can change until the application yields
its first non-empty string, so 'transform_ok' has to be a mutable
truth value.
"""

def __init__(self, result, transform_ok):
if hasattr(result, 'close'):
self.close = result.close
self._next = iter(result).next
self.transform_ok = transform_ok

def __iter__(self):
return self

def next(self):
if self.transform_ok:
return piglatin(self._next())
else:
return self._next()

class Latinator:

# by default, don't transform output
transform = False

def __init__(self, application):
self.application = application

def __call__(self, environ, start_response):

transform_ok = []

def start_latin(status, response_headers, exc_info=None):

# Reset ok flag, in case this is a repeat call
del transform_ok[:]

for name, value in response_headers:
if name.lower() == 'content-type' and value == 'text/plain':
transform_ok.append(True)
# Strip content-length if present, else it'll be wrong
response_headers = [(name, value)
for name, value in response_headers
if name.lower() != 'content-length'
]
break

write = start_response(status, response_headers, exc_info)

if transform_ok:
def write_latin(data):
write(piglatin(data))
return write_latin
else:
return write

return LatinIter(self.application(environ, start_latin), transform_ok)


# Run foo_app under a Latinator's control, using the example CGI gateway
from foo_app import foo_app
run_with_cgi(Latinator(foo_app))

规范详述

一个应用对象必须接受两个位置参数。
为了好理解,
我们分别给这两个参数取名为environ和start_response,
但是这也不是必须的。
服务器或网关必须用这两个位置参数去调用一个应用(例如,通过调用result = application(environ, start_response)

environ参数是一个字典对象,
包含类似CGI环境参数。
这个对象必须是一个python内置的字典对象(不是一个子类,自定义字典类或者其他模拟字典类),
而且应用能够随意修改字典的内容。
字典还必须包含必要的WSGI变量(下节将会介绍),
以及包含服务器端扩展变量,将根据下面描述的来命名。

start_response参数是一个可调用对象,它接受两个必选参数和一个可选参数。
为了更好的理解,
我们分别命名这些参数status,response_headers,和exc_info,
但是他们同样不是必须叫这个名,
应用必须调用start_response对象,并且使用位置参数(例如,start_response(status, response_headers)).

status参数是一个表状态的字符串,类似“999 Message here”,
response_headers是一个包含http响应头的元祖。
可选的exc_info参数在下节start_reponse()可调用对象和错误处理中介绍。
它仅在当应用程序出错或者打算在浏览器上显示错误信息的时候使用。

start_response()必须返回一个write(主体数据)可调用对象,它接受一个位置参数:
HTTP响应主体的一部分字符串。
(注意:write()可调用对象仅用于某些现有框架必须的输出APIs;
如果可以避免,新的应用或框架就不应该适用这个对象。
参阅Buffering and Stream章节获取更多信息。)

当服务器调用时,
应用对象必须返回一个可迭代对象,返回0或更多字符串。
这可以通过很多方式来实现,
例如返回字符串列表
或者通过一个生成器函数实现应用程序对象,
或者用一个可迭代类实例来实现应用程序对象。
无论它时怎么实现的,
应用程序对象必须返回一个可迭代对象,返回0或更多字符串。

服务器或网关必须以非缓存的方式返回生成的字符串到客户机,
在另一个请求前,完成每一个字符串的返回。
(换句话说,应用必须自己做好缓存,
参阅Buffering and Stream章节获取更多有关应用程序输出处理的信息。)

服务器或网关按二进制字节序列来处理来处理生成的字符串:
尤其是,它应该确保行尾结束符不能被修改。
应用对象响应的数据必须必须适合在客户端展现。
(服务器或网关可能应用HTTP传输编码。或者其他实现HTTP功能的传输方式,例如字节范围传输。参阅下面的Other HTTP Features章节获取更多信息。)

如果能够调用len(iterable)
服务器必须能够信任结果的准确度。
如果可迭代对象是通过应用提供的__len__()方法返回的,
则必须返回一个准确的结果。
(参阅Handling the Content-Length Header章节获取更多信息。)

如果应用返回的可迭代对象有一个close()方法,
服务器或网关必须调用那个方法以完成当前请求,
无论请求是否已经正常完成,或者由于错误提前终止
(这是用来支持应用程序的资源发布)。
这个协议是补充PEP 325的生成器支持章节,
以及其他常见有close()方法的可迭代对象。

(注意:应用必须在可迭代开始生成主题字符串之前调用start_response(),使得服务器能够在主体内容之前发送headers。)
然而,这个调用可以通过可迭代对象的首次迭代来调用实现,
所以,服务器必须不假设应用首先调用start_response(),然后在开始迭代。)

最后,服务器和网关必须不直接使用应用返回的可迭代对象的其他属性。
除非它在服务器或网关中是一个特别的实例,有自有的特别属性,
例如wsgi.file_wrapper返回“file wrapper”
(参阅Optional Platform-Specific File Handling)。
通常情况下,只有这里指定的属性,
或者通过例如PEP 234中的迭代APIs。

变量environ

environ字典需要包含CGI环境变量,
定义在Common Gateway Interface规范2中。
下面的变量必须提供,
除非他们的值是空字符串,
则可能可以省略,除了额外注明的变量。

  • REQUEST_METHOD
    The HTTP request method, such as “GET” or “POST”. This cannot ever be an empty string, and so is always required.

  • SCRIPT_NAME
    The initial portion of the request URL’s “path” that corresponds to the application object, so that the application knows its virtual “location”. This may be an empty string, if the application corresponds to the “root” of the server.

  • PATH_INFO
    The remainder of the request URL’s “path”, designating the virtual “location” of the request’s target within the application. This may be an empty string, if the request URL targets the application root and does not have a trailing slash.

  • QUERY_STRING
    The portion of the request URL that follows the “?”, if any. May be empty or absent.

  • CONTENT_TYPE
    The contents of any Content-Type fields in the HTTP request. May be empty or absent.

  • CONTENT_LENGTH
    The contents of any Content-Length fields in the HTTP request. May be empty or absent.

  • SERVER_NAME, SERVER_PORT
    When combined with SCRIPT_NAME and PATH_INFO, these variables can be used to complete the URL. Note, however, that HTTP_HOST, if present, should be used in preference to SERVER_NAME for reconstructing the request URL. See the URL Reconstruction section below for more detail. SERVER_NAME and SERVER_PORT can never be empty strings, and so are always required.

  • SERVER_PROTOCOL
    The version of the protocol the client used to send the request. Typically this will be something like “HTTP/1.0” or “HTTP/1.1” and may be used by the application to determine how to treat any HTTP request headers. (This variable should probably be called REQUEST_PROTOCOL, since it denotes the protocol used in the request, and is not necessarily the protocol that will be used in the server’s response. However, for compatibility with CGI we have to keep the existing name.)

  • HTTP_ Variables
    Variables corresponding to the client-supplied HTTP request headers (i.e., variables whose names begin with “HTTP_”). The presence or absence of these variables should correspond with the presence or absence of the appropriate HTTP header in the request.

A server or gateway should attempt to provide as many other CGI variables as are applicable. In addition, if SSL is in use, the server or gateway should also provide as many of the Apache SSL environment variables [5] as are applicable, such as HTTPS=on and SSL_PROTOCOL. Note, however, that an application that uses any CGI variables other than the ones listed above are necessarily non-portable to web servers that do not support the relevant extensions. (For example, web servers that do not publish files will not be able to provide a meaningful DOCUMENT_ROOT or PATH_TRANSLATED.)

A WSGI-compliant server or gateway should document what variables it provides, along with their definitions as appropriate. Applications should check for the presence of any variables they require, and have a fallback plan in the event such a variable is absent.

Note: missing variables (such as REMOTE_USER when no authentication has occurred) should be left out of the environ dictionary. Also note that CGI-defined variables must be strings, if they are present at all. It is a violation of this specification for a CGI variable’s value to be of any type other than str.

In addition to the CGI-defined variables, the environ dictionary may also contain arbitrary operating-system “environment variables”, and must contain the following WSGI-defined variables:

Variable Value
wsgi.version The tuple (1, 0), representing WSGI version 1.0.
wsgi.url_scheme A string representing the “scheme” portion of the URL at which the application is being invoked. Normally, this will have the value “http” or “https”, as appropriate.
wsgi.input An input stream (file-like object) from which the HTTP request body can be read. (The server or gateway may perform reads on-demand as requested by the application, or it may pre- read the client’s request body and buffer it in-memory or on disk, or use any other technique for providing such an input stream, according to its preference.)
wsgi.errors An output stream (file-like object) to which error output can be written, for the purpose of recording program or other errors in a standardized and possibly centralized location. This should be a “text mode” stream; i.e., applications should use “\n” as a line ending, and assume that it will be converted to the correct line ending by the server/gateway.

For many servers, wsgi.errors will be the server’s main error log. Alternatively, this may be sys.stderr, or a log file of some sort. The server’s documentation should include an explanation of how to configure this or where to find the recorded output. A server or gateway may supply different error streams to different applications, if this is desired.
wsgi.multithread This value should evaluate true if the application object may be simultaneously invoked by another thread in the same process, and should evaluate false otherwise.
wsgi.multiprocess This value should evaluate true if an equivalent application object may be simultaneously invoked by another process, and should evaluate false otherwise.
wsgi.run_once This value should evaluate true if the server or gateway expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a gateway based on CGI (or something similar).

Finally, the environ dictionary may also contain server-defined variables. These variables should be named using only lower-case letters, numbers, dots, and underscores, and should be prefixed with a name that is unique to the defining server or gateway. For example, mod_python might define variables with names like mod_python.some_variable.

Input and Error Streams

参考

[1] The Python Wiki “Web Programming” topic (http://www.python.org/cgi-bin/moinmoin/WebProgramming)
[2] The Common Gateway Interface Specification, v 1.1, 3rd Draft (http://ken.coar.org/cgi/draft-coar-cgi-v11-03.txt)

散列

参考《数据结构与算法分析(C语言描述)》 第二版 第五章 散列

什么是散列表?

散列表是一种数据结构,它支持对一组元素进行各种操作,不过它只支持二叉查找树所允许的一部分操作。
散列表的实现叫做散列,散列是一种可以用常数时间执行插入、删除和查找的技术,但是它对排序操作是不支持的。

理想的散列表

理想的散列表就是一个包含关键字的,具有固定大小的数组。每个关键字被映射到从 0 到 tableSize-1 这个范围中的某个数,
这个映射叫做散列函数,理想的情况下,任何两个不同的的关键字,映射到不同的单元。我们需要找一个散列函数,使能够在单元
间均匀的分配关键字。

散列函数

如果关键字是整数,那么直接返回 key % tableSize

1
2
3
4
int hash(int key, int tableSize)
{
return key % tableSize;
}

然而当关键字的取值具有一些不理想的性质时,上面这个散列函数就是一个不好的选择,例如,当tableSize = 10时,关键字都是
个位数为 0 的整数,那么上面这个散列函数返回的值永远是0,也就是说,所有的关键字都被映射到单元0了,这是不好的。为了使
分配的更均匀,tableSize最好为素数。
一般,关键字的的值都是字符串,那么有以下几种方法来实现散列函数

  • 第一种,把字符的ASCII码值加起来,然后再模tableSize。这样的缺点就是当tableSize很大时,分配就不会均匀。
  • 第二种,假设关键字是由三个英文字母组成,那么三个字符有17576种组合(26的三次方),如果表大小是10007,
    这就是一个合理的均匀分配,但是,英文并不是随机组合,三个字幕组成有效的单词大概只有2851个,即使没有冲突,
    也只有表的28%被散列到,所以也是不合适的。
  • 第三种涉及到字符串中所有字符,所以当关键字特别长时,计算会花费过多时间,解决方法就是不计算所有字符,而挑选其中的字符,
    比如只计算奇数位上的字符。

冲突

当两个关键字计算后得出的散列值相同时,就会产生冲突,解决冲突有两种方法:分离链接法 & 开放定址法

分离链接法

该方法将所有散列值相同的元素保留到一个表中,这种方法的缺点就是使用了指针,由于给新单元分配指针需要时间,因此导致算法速度
较慢,另外,它还依赖另一种数据结构(链表)。

开放定址法

开放定址法中如果遇到了冲突,则尝试选择另外的单元,直到找到空的单元为止。

再散列

使用前面两种方法解决冲突时,当表的元素过多时,操作的运行时间将过长,而且insert操作可能失败。解决方案就是扩展表,使表增大
一倍,并使用一个新的散列函数,扫描原表中的元素,重新计算散列值并插入到新表中,这个操作就叫再散列。

可扩散列

fuck,看不懂!

散列表的应用

  • 编译器中使用散列表跟踪源代码声明的变量,这种数据结构叫做符号表
  • 使用散列表解决图论问题。
  • 游戏编程中
  • 在线拼写检测程序

总结

散列表以常数时间实现insert和find操作,但是当表中元素过多时,表的性能将下降。另外,当关键字的类型以及性质比较特殊时,选择
合适的散列散列函数也会很大程度上影响表的性能。

python多线程编程

python多线程编程

  1. 为啥使用多线程?
  • 有时候计算机要处理的任务之间没有因果关系,都是独立的,也就是说任务的结果不会影响其他任务,我们就会希望计算机并行的处理多个任务,这就要使用多线程。当然还有其他方式来实现多任务并行处理,比如说串行程序使用一个或多个计时器来实现一个多路复用的方案,一个串行程序需要从每个I/O终端通道来检查用户的输入,然而程序在读取I/O时不能阻塞,因为用户的输入是不确定时间的,阻塞会影响其他I/O通道的处理,串行程序必须使用非阻塞I/O或拥有计时器的阻塞I/O。由于串行程序只有一个执行线程,为了防止执行其中某一个任务占用太多时间,并对用户的响应进行合理分配,实现起来会有非常复杂的控制流,难以维护。
  1. 进程和线程到底是啥?
  • 进程(重量级进程)是一个执行中的程序,每个进程都有自己的地址空间、内存、数据栈、以及其他一些用于跟踪执行的辅助数据,操作系统管理其上所有进程的执行,为这些进程分配时间。进程可以通过派生(fork或spawn)新的进程来执行其他任务,由于每个进程都有自己的地址空间和内存。所以进程间只能通过进程间通信的方式共享信息。线程(轻量级进程)与进程类似,但是它们是在一个进程空间下面执行的,并共享上下文,可以将它们认为是在一个主进程中并行运行的一些迷你进程。
  1. python和线程和全局解释器锁
  • python代码是由python虚拟机执行的,python虚拟机其实就是模拟cpu核心,所以,同一时刻只能由一个线程在执行,多线程执行实质上是虚拟机不停的切换执行中的线程,为了保证同一时刻只有一个线程在执行,这里就用到全局解释器锁,所以对虚拟机的访问是由全局解释器锁来控制的。
    1
    2
    3
    4
    5
    6
    7
    8
    1. 设置GIL(Global Interpreter Lock)
    2. 切换一个线程去执行
    3. 执行
    a. 指定数量的字节码指令
    b. 线程主动让出控制权
    4. 切换出线程
    5. 解锁GIL
    6. 重复上述步骤
    当调用外部代码(例如C/C++拓展内置函数),GIL会保持锁定,因为这期间没有python字节码计数,但是可以靠在拓展代码中手动解锁GIL。

python源码解析 读书笔记II

python源码解析 读书笔记II —— python虚拟机

python执行过程

python源代码 输入 -> python解释器 编译 -> python字节码 输入 -> python虚拟机 执行

python编译编出来的到底是什么玩意?

python是一门解释型语言,它编译后得到的不是机器码,而是叫字节码,就是以.pyc为后缀的文件。

python源代码中的静态信息(字符串、常量值)储存在一个运行时的对象中,这个对象就是PyCodeObject,python编译的结果其实就是这个对象,而.pyc文件就是这个对象在硬盘上的表现形式。

PyCodeObject对象的声明
code.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct{
PyObject_HEAD
int co_argcount; // Code block中位置参数的个数(位置参数?)
int co_nlocals; // Code block中局部变量的个数(包括位置参数)
int co_stacksize; // 执行这个code block需要的栈空间
int co_flags;
PyObject *co_code; // code block编译所得字节码的指令序列,以PyStringObject的形式存在
PyObject *co_consts; // 保存该code block中所有常量,以PyTupleObject的形式存在
PyObject *co_names; // 保存该code block中所有符号(?),以PyTupleObject的形式存在
PyObject *co_varnames;// 保存该code block中左右局部变量名
PyObject *co_freevars; // Python实现闭包需要用到的东西
PyObject *co_cellvars; // code block中内部嵌套函数所引用的局部变量名集合
/* THe rest doesn't count for hash/cmp */
PyObject *co_filename; // code block中对应的py文件完整路径
PyObject *co_name; // code block的名字,通常是函数名或类名
int co_firstlineno; // code block对应py文件中的起始行
PyObject *co_lnotab; // 字节码指令与py文件中source code行号的对应关系,以PyStringObject的形式存在
} PyCodeObject;

python代码中每一个code block都会创建一个PyCodeObject对象与之对应。code block就是一个 作用域/命名空间,例如一个 函数/类/源文件。

怎样生成.pyc文件

  • import一个模块时,会生成该模块的pyc文件
  • python标准库中的py_compile、compiler工具

import一个模块时,python会在设定好的path中按顺序查找以这个模块名命名的dll和pyc文件,如果只找到.py文件,那就会把源代码编译成PyCodeObject对象,并创建.pyc文件,把对象写入文件中,接下来python才会对.pyc文件进行加载操作。实际上就是把pyc文件的Code对象在内存上重新复制出来。

co_lnotab域储存的是字节码指令与py文件中source code行号的对应关系,而在python2.3以前,有一条字节码指令,叫SET_LINENO,这条字节码会记录py文件中source code的位置,python2.3之后编译不会再产生这条字节码,因为字节码代表的是运行时的行为,而记录源代码行号的动作完全可以在编译时完成,所以python会在编译时直接将这个信息记录到co_lnotab中。

python中可以访问PyCodeObject对象。

1
2
3
source = open('demo.py').read()
co = compile(source, 'demo.py', 'exec') # co就是PyCodeObject
type(co) # <type 'code'>

pyc文件的生成

python的import机制会产生pyc文件,那么import时到底做了哪些操作?
import.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void write_compiled_module(PyCodeObject *co, char *cpathname, long mtime)
{
FILE *fp;
 fp = open_exclusive(cpathname); // 排他性打开文件
 // 【1】写入Python magic number
 PyMarshal_WriteLongToFile(pyc_magic, fp, Py_MARSHAL_VERSION);
 // 【2】写入时间信息
PyMarshal_WriteLongToFile(mtime, fp, Py_MARSHAL_VERSION);
 // 【3】写入PyCodeObject对象
PyMarshal_WriteObjectToFile((PyObject *)co, fp, Py_MARSHAL_VERSION);

fflush(fp);
fclose(fp);
}

magic number是啥?
他是python定义的一个整数值,一般不同的版本的magic number不同,,这个值用来保证python的兼容性的。python加载pyc文件时首先会检查这个值,如果不同则会拒绝加载这个pyc文件。
另外这个值定义在import.c中。

为什么要写如时间信息?
这主要是为了保证pyc文件与源文件同步,当用户修改了源文件,python加载了较旧的pyc文件后,会检查发现pyc的时间早于py文件,于是会自动重新编译源文件,生成新的pyc文件。

PyCodeObject怎么写入文件中?
根据对象中不同类型的域调用不同的方法去写文件,在写对象之前都会先写入对象的类型,这样对pyc文件再次加载有重要作用。