你可能不知道的Python之Encoding

由一个简单的问题,引发了这么一连串的代码分析,Python设计者也确实煞费苦心。不过研究技术,还是要有刨根问底的精神,把原理弄清楚,才能做好开发。刚刚不久前才开始用Python写点小程序,没有怎么仔细研究过,然后就遇到了一个很有趣的问题。

关于字符串输出的问题,如下:

str与unicode_str一个是utf-8编码,另外一个是unicode,但是通过print输出来的都是同样的“毛毛熊”,我所用的终端是SecureCRT连接的远程的centos,并且编码采用的是utf-8,但是为什么两个不一样的字符串输出都是同样的utf-8呢?

于是在window打开了cmd进行同样的操作,这个时候解码方式得用gbk(windows与linux下不一样),否则会报错,输出的结果如下

不一样的编码,在不一样的环境下,都print函数可以正确地输出字符串,这到底是一种什么样的机制在背后工作?

想了半天,还是想不出来个所以然,只好下载源代码来研究一下了。

由于最近工作中老在折腾的python版本是2.7.10,于是就去www.python.org官网下载了对应源代码。

源代码不大,一下就下载完了,解压,./configure –help看看有哪些编译选项,结果也没什么有用的,就直接./configure了,再就是make,用不了几分钟,就编译完了,在源代码目录下面生成了python这个可执行文件,一切比较顺利。

既然要分析print函数的工作原理,那就需要找到print的C语言实现,通过grep很快就在./Python/bltinmodule.c文件中找到了对应的实现

里面会通过调用PyFile_WriteObject来打印每一个参数

那么我们就来研究一下PyFile_WriteObject的实现,位于./Objects/fileobject.c中

PyFile_WriteObject会根据传递的file参数的f_encoding来对对象v来编码,而转到print的实现,发现传递过来的file对象其实就是sys.stdout

那么结果很明了了,stdout的encoding决定了对象的输出编码方式,Linux的shell编码是utf-8而windows的cmd编码是gbk,所以同样是unicode,在不同编码的终端都能够正确输出。废话不多说,直接验证一下

在linux下:

Windows下面:

输出的是cp936,其实就是gbk的别名罢了,可以自行百度。

虽然到此大概知道了print输出的方式,但是还是想刨根问底,python是如何判断stdout的encoding呢?是编译的时候就确定了还是运行的时候动态获取呢?

下面开始分析了:

既然可以通过sys.stdout.encoding获取stdout的编码,那么肯定python在C语言层面初始化的时候会对stdout这个对象的encoding属性进行赋值,这样python代码可以直接读取,所以要找到python可执行文件的入口,一步步分析python初始化的过程,找到对stdout赋值的地方。

通过grep工具,很快就在Modules/python.c找到了python的main入口

Main调用的Py_Main,在./Modules/main.c中找到实现,然后间接调用Py_Initialize进行初始化操作

Py_Initialize位于./Python/pythonrun.c里面

大概的流程也就是:先读取环境变量PYTHONIOENCODING,如果PYTHONIOENCODING有设置,那么就使用对应的编码方式,否则在linux平台下面,如果没有设置Py_FileSystemDefaultEncoding,会调用nl_langinfo来获取对应的codeset,而Py_FileSystemDefaultEncoding的定义位于./Python/bltinmodule.c里面

说明Linux下面的FileSystemDefaultEncoding是NULL,所以codeset都是通过nl_langinfo来获取的。

而windows上面,则是调用GetConsoleOutputCP来获取对应的编码方式。

在获取到codeset之后,会调用PyFile_SetEncodingAndErrors来设置stdin,stdout,stderr三个对象的encoding,函数位于./Objects/fileobject.c中

可以看出,函数直接对file的f_encoding字段进行赋值,与PyFile_WriteObject成功对应上了。

为了验证一下分析的正确性,在Linux上写个程序验证一下,代码如下

简单gcc编译,运行

输出的是utf-8,与设想比较重合,继续在windows上面验证一下,用VS新建了一个C++的Console工程

运行,得到结果

与之前python运行的结果也是一致的。

好了,到此,基本之前发现的疑问都解决了。

但是,代码里隐藏了另外一个问题,不知道大家发现没有,在设置stdin[stdout,stderr]的时候,判断了一下isatty这个变量,如下

也就是说,如果stdout的isatty方法返回的是true,才会设置对应的codeset,那什么样的时候,isatty返回的不是true呢?

经过分析,如果shell通过管道进行传输的话,那么isatty就是false,这个时候,对应的stdout.encoding就会设置为None,这里先验证一下isatty的使用,编写一段代码

通过两种方式运行

在正常运行的情况下,isatty返回的是1,而通过管道把stdout输出到1.log文件,isatty返回的是0,也就是说,如果python的stdout通过管道重定向到其他地方,stdout.encoding就没有设置,默认为None,下面写个代码来验证一下

vi一个test_encoding.py文件,输入

然后还是以两种方式运行

与设想的一样,如果通过管道重定向到1.log文件,那么stdout的encoding就是None,那这种结果会带来什么影响呢?下面再写个代码来测试一下

vi一个test.py

首先直接运行

运行正常,然后把stdout重定向到文件

很明显,报错了,在print(str_unicode)的时候报错,因为str_unicode.__repr__()返回的是字符串,而str_unicode是unicode类型,python在print的时候发现类型是unicode并且enc是None的时候会去调用PyUnicode_AsEncodedString函数(这个流程可以自行看源代码),该函数位于./Objects/unicodeobject.c中,encoding参数传递是的NULL,函数实现为

容易看出,如果encoding是NULL,则会获取默认的encoding

而默认的unicode_default_encoding定义如下

所以,默认的encoding是ascii,而ascii的编码器在处理unicode字符时,遇到了非ascii可打印字符的话,就报错了,与我们刚刚执行的结果完全一致。

既然print无法通过这种方式输出unicode,那要如何解决这种问题呢?回想一下,刚刚分析的代码里面,如果设置了环境变量PYTHONIOENCODING,python默认就会采取这种编码,而不通过其他方式获取编码,那就设置PYTHONIOENCODING为utf-8,然后再执行刚刚的代码

成功执行,不报错了,输出结果也是正确的。不过这样用毕竟不能从根本上解决问题,只能用print的时候考虑一下输出的对象,如果是unicode就得小心一点了。

到这里,基本就可以结束了。由一个简单的问题,引发了这么一连串的代码分析,Python设计者也确实煞费苦心。不过研究技术,还是要有刨根问底的精神,把原理弄清楚,才能做好开发。

Spread the word. Share this post!

Meet The Author

1 Comments

Leave Comment