资讯专栏INFORMATION COLUMN

Python “黑魔法” 之 Encoding & Decoding

邹强 / 3498人阅读

摘要:我可以明确告诉你这不是,但它可以用解释器运行。这种黑魔法,还要从说起。提案者设想使用一种特殊的文件首注释,用于指定代码的编码。暴露了一个函数,用于注册自定义编码。所谓的黑魔法其实并不神秘,照猫画虎定义好相应的接口即可。

首发于我的博客,转载请注明出处

写在前面

本文为科普文

本文中的例子在 Ubuntu 14.04 / Python 2.7.11 下运行成功,Python 3+ 的接口有些许不同,需要读者自行转换

引子

先看一段代码:

example.py

# -*- coding=yi -*-

从 math 导入 sin, pi

打印 "sin(pi) =", sin(pi)

这是什么?!是 Python 吗?可以运行吗?——想必你会问。

我可以明确告诉你:这不是 Python,但它可以用 Python 解释器运行。当然,如果你愿意,可以叫它 “Yython” (易语言 + Python)。

怎么做到的?也许你已经注意到第一行的奇怪注释——没错,秘密全在这里。

这种黑魔法,还要从 PEP 263 说起。

古老的 PEP 263

我相信 99% 的中国 Python 开发者都曾经为一个问题而头疼——字符编码。那是每个初学者的梦靥。

还记得那天吗?当你试图用代码向它示好:

print "你好"

它却给你当头一棒:

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 老爹有些个人主义,设计时只考虑到了英文世界。

提案者设想:使用一种特殊的文件首注释,用于指定代码的编码。这个注释的正则原型是这样的:

^[ 	v]*#.*?coding[:=][ 	]*([-_.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.encodestr.decode 上来了。

可 Python 怎么这么强大?!几乎所有编码它都认得!这是怎么做到的?是标准库?还是内置于解释器中?

一切,都是 codecs 模块在起作用。

codecs

codecs 算是较为冷门的一个模块,更为常用的是 strencode/decode 的方法——但它们本质都是对 codecs 的调用。

打开 /path/to/your/python/lib/encodings/ 目录,你会发现有许多以编码名称命名的 .py 文件,如 utf_8.pylatin_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.

encodedecode 是无状态的编码/解码的函数,简单说就是:前一个被编解码的字符串与后一个没有关联。如果你想用 codecs 系统进行语法树解析,解析逻辑最好不要写在这里,因为代码的连续性无法被保证;incremental* 则是有状态的解析类,能弥补 encodedecode 的不足;stream* 是流相关的解析类,行为通常与 encode/decode 相同。

关于这六个对象的具体写法,可以参考 /path/to/your/python/lib/encodings/rot_13.py,该文件实现了一个简单的密码系统。

那么,是时候揭开真相了。

所谓的 “Yython”

黑魔法其实并不神秘,照猫画虎定义好相应的接口即可。作为例子,这里只处理用到的关键字:

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
结语

有时,对习以为常的东西深入了解一下,说不定会有惊人的发现。

References

codecs - Codec registry and base classes

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/38074.html

相关文章

  • Python魔法 Meta Classes

    摘要:幸而,提供了造物主的接口这便是,或者称为元类。接下来我们将通过一个栗子感受的黑魔法,不过在此之前,我们要先了解一个语法糖。此外,在一些小型的库中,也有元类的身影。 首发于 我的博客 转载请注明出处 接触过 Django 的同学都应该十分熟悉它的 ORM 系统。对于 python 新手而言,这是一项几乎可以被称作黑科技的特性:只要你在models.py中随便定义一个Model的子类,Dj...

    LeoHsiun 评论0 收藏0
  • 关于解决Python乱码问题的终极解决方案 (TL;DR)

    摘要:关于解决乱码问题的终极解决方案有个特别好玩的现象,当我们为了编码头疼的时候,几乎搜索到所有的文章都会先发一通牢骚。另外,关于的乱码问题,又是一个新的较长篇章。 关于解决Python乱码问题的终极解决方案 (TL;DR) showImg(https://segmentfault.com/img/remote/1460000013229494?w=809&h=184); 有个特别好玩的现象...

    Lemon_95 评论0 收藏0
  • Python魔法 Generator Coroutines

    摘要:主程序通过唤起子程序并传入数据,子程序处理完后,用将自己挂起,并返回主程序,如此交替进行。通过轮询或是等事件框架,捕获返回的事件。从消息队列中取出记录,恢复协程函数。然而事实上只有直接操纵的协程函数才有可能接触到这个对象。 首发于 我的博客 转载请注明出处 写在前面 本文默认读者对 Python 生成器 有一定的了解,不了解者请移步至生成器 - 廖雪峰的官方网站。 本文基于 Pyth...

    李文鹏 评论0 收藏0
  • 从hello world看JavaScript隐藏的魔法

    摘要:如果类型转换你还不是很了解,可以先读下这篇来理解一下从看隐式强制转换机制。函数可对通过编码的字符串进行解码。而作者封装的也是基于这两者来实现输出黑魔法字符串的。同时通过,返回了一个匿名函数形成了闭包。为了达到装逼的效果。 写在最前 事情的起因是这段看起来不像代码的代码: showImg(https://segmentfault.com/img/remote/14600000126810...

    cnio 评论0 收藏0
  • 经验拾忆(纯手工)=> Python__魔法__

    摘要:类的继承类继承有三种调用方式,其实是有区别的,听我慢慢道来第一种父类方法参数直接调用第二种方法参数直接调用在谁的类下调用,就找此类对应的下一个就是要继承的第三种方法参数找类名对应的的下一个,就是继承的,一般写本身的类名上下文管理器上下文管理 类的继承 类继承有三种调用方式,其实是 有区别 的,听我慢慢道来 class A: def say(self, name): ...

    tulayang 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<