首发于个人博客,转载请注明出处html
- 本文为科普文
- 本文中的例子在 Ubuntu 14.04 / Python 2.7.11 下运行成功,Python 3+ 的接口有些许不一样,须要读者自行转换
先看一段代码:python
example.py
:less
# -*- coding=yi -*- 从 math 导入 sin, pi 打印 'sin(pi) =', sin(pi)
这是什么?!是 Python 吗?能够运行吗?——想必你会问。函数
我能够明确告诉你:这不是 Python,但它能够用 Python 解释器运行。固然,若是你愿意,能够叫它 “Yython” (易语言 + Python)。ui
怎么作到的?也许你已经注意到第一行的奇怪注释——没错,秘密全在这里。编码
这种黑魔法,还要从 PEP 263 提及。spa
我相信 99% 的中国 Python 开发者都曾经为一个问题而头疼——字符编码。那是每一个初学者的梦靥。命令行
还记得那天吗?当你试图用代码向它示好:设计
print '你好'
它却给你当头一棒:code
SyntaxError: Non-ASCII character '\xe4' in file chi.py on line 1, but no encoding declared
【一脸懵逼】
因而,你上网查找解决方案。很快,你便有了答案:
# -*- coding=utf-8 -*- print '你好'
其中第一行的注释用于指定解析该文件的编码。
这个特新来自 2001 年的 PEP 263 -- Defining Python Source Code Encodings,它的出现是为了解决一个反响普遍的问题:
In Python 2.1, Unicode literals can only be written using the Latin-1 based encoding "unicode-escape". This makes the programming environment rather unfriendly to Python users who live and work in non-Latin-1 locales such as many of the Asian countries. Programmers can write their 8-bit strings using the favorite encoding, but are bound to the "unicode-escape" encoding for Unicode literals.
Python 默认用 ASCII 编码解析文件,给 15 年前的非英文世界开发者形成了不小的困扰——看来 Guido 老爹有些我的主义,设计时只考虑到了英文世界。
提案者设想:使用一种特殊的文件首注释,用于指定代码的编码。这个注释的正则原型是这样的:
^[ \t\v]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)
也就是说 # -*- coding=utf-8 -*-
并非惟一的写法,只是 Emacs 推荐写法而已。诸如 # coding=utf-8
、# encoding: utf-8
都是合法的——所以你没必要惊讶于他人编码声明与你不一样。
正则的捕获组 ([-_.a-zA-Z0-9]+)
将会被用做查找编码的名称,查找到的编码信息会被用于解码文件。也就是说,import example
背后其实至关于有以下转换过程:
with open('example.py', 'r') as f: content = f.read() encoding = extract_encoding_info(content) # 解析首注释 exec(content.decode(encoding))
问题其实又回到咱们经常使用的 str.encode
和 str.decode
上来了。
可 Python 怎么这么强大?!几乎全部编码它都认得!这是怎么作到的?是标准库?仍是内置于解释器中?
一切,都是 codecs
模块在起做用。
codecs
算是较为冷门的一个模块,更为经常使用的是 str
的 encode
/decode
的方法——但它们本质都是对 codecs
的调用。
打开 /path/to/your/python/lib/encodings/
目录,你会发现有许多以编码名称命名的 .py
文件,如 utf_8.py
、latin_1.py
。这些都是系统预约义的编码系统,实现了应对各类编码的逻辑——也就是说:编码系统其实也是普通的模块。
除了内置的编码,用户也能够 自行定义编码系统。codecs
暴露了一个 register
函数,用于注册自定义编码。register
签名以下:
codecs.register(search_function)
Register a codec search function. Search functions are expected to take one argument, the encoding name in all lower case letters, and return a CodecInfo object having the following attributes:
- name: The name of the encoding;
- encode: The stateless encoding function;
- decode: The stateless decoding function;
- incrementalencoder: An incremental encoder class or factory function;
- incrementaldecoder: An incremental decoder class or factory function;
- streamwriter: A stream writer class or factory function;
- streamreader: A stream reader class or factory function.
encode
和 decode
是无状态的编码/解码的函数,简单说就是:前一个被编解码的字符串与后一个没有关联。若是你想用 codecs
系统进行语法树解析,解析逻辑最好不要写在这里,由于代码的连续性没法被保证;incremental*
则是有状态的解析类,能弥补 encode
、decode
的不足;stream*
是流相关的解析类,行为一般与 encode
/decode
相同。
关于这六个对象的具体写法,能够参考 /path/to/your/python/lib/encodings/rot_13.py
,该文件实现了一个简单的密码系统。
那么,是时候揭开真相了。
黑魔法其实并不神秘,照猫画虎定义好相应的接口便可。做为例子,这里只处理用到的关键字:
yi.py
:
# encoding=utf8 import codecs yi_map = { u'从': 'from', u'导入': 'import', u'打印': 'print' } def encode(input): for key, value in yi_map.items(): input = input.replace(value, key) return input.encode('utf8') def decode(input): input = input.decode('utf8') for key, value in yi_map.items(): input = input.replace(key, value) return input class Codec(codecs.Codec): def encode(self, input, errors="strict"): input = encode(input) return (input, len(input)) def decode(self, input, errors="strict"): input = decode(input) return (input, len(input)) class IncrementalEncoder(codecs.IncrementalEncoder): def encode(self, input, final=False): return encode(input) class IncrementalDecoder(codecs.IncrementalDecoder): def decode(self, input, final=False): return decode(input) class StreamWriter(Codec, codecs.StreamWriter): pass class StreamReader(Codec, codecs.StreamReader): pass def register_entry(encoding): return codecs.CodecInfo( name='yi', encode=Codec().encode, decode=Codec().decode, incrementalencoder=IncrementalEncoder, incrementaldecoder=IncrementalDecoder, streamwriter=StreamWriter, streamreader=StreamReader ) if encoding == 'yi' else None
在命令行里注册一下,就能够看到激动人心的结果了:
>>> import codecs, yi >>> codecs.register(yi.register_entry) >>> import example sin(pi) = 1.22464679915e-16
有时,对习觉得常的东西深刻了解一下,说不定会有惊人的发现。