字符编码与Python2

================

一、编码发展史

1. ASCII

  • 1967年推出
  • 128字符,7bits
  • 33个不可显示,95个可显示

2. Latin-1(ISO-8859-1)

  • 单字节编码,256个字符
  • 兼容ASCII

3. unicode(utf-8, utf-16, utf-32)

  • 为每一个字符而非字形定义唯一的代码
  • 表示方法,U+十六进制数,如“”->U+8D5E

二、utf-8

UTF-8是Unicode的实现方式之一

特点

  • 变长编码(1-4个字节)
  • 兼容ASCII
  • 易于进行网络传输和存储

编码规则

1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。

2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。

utf-8

如何对一个字符进行utf-8编码?

  1. 从上表查出需要的编码字节数
  2. 按照上表确定各字节的高位数字
  3. 填充“x”位,使用该字符unicode的character number(code point),规则是从最低位开始,依次往前填充,直到所有的x都被替换为止

例子

“赞”->U+8D5E->需要3个字节编码->1110xxxx 10xxxxxx 10xxxxxx->11101000 10110101 10011110->0xE8B59E

三、Python2中的编码

  1. 编码和解码的概念
  • 编码(encode):字符->字节,为了存储和传输
  • 解码(decode):字节->字符,为了显示和阅读
  1. Python如何看待字符串

Python字符串分为两种:strunicode

1
2
3
4
5
6
7
8
9
10
11
12
In [3]: s1 = '赞'
In [4]: s2 = u'赞'
In [5]: s1
Out[5]: '\xe8\xb5\x9e'
In [6]: s2
Out[6]: u'\u8d5e'
In [7]: type(s1)
Out[7]: str
In [8]: type(s2)
Out[8]: unicode
  1. Python解释器如何解析文件

Python interpreter 根据声明的encoding去解析这个文件

1
^[ \t\v]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/python
# -*- coding: latin-1 -*-
import os, sys
...
#!/usr/bin/python
# -*- coding: iso-8859-15 -*-
import os, sys
...
#!/usr/bin/python
# -*- coding: ascii -*-
import os, sys
  1. 解决方案
  • 搞清楚不同的编码

系统环境、网络传输、文件、Python解释器、第三方库

  • str本质上是一串二进制数据,unicode是字符

encoding always takes a Unicode string and returns a bytes sequence, and decoding always takes a bytes sequence and returns a Unicode string”.

  • unicode只存在于内部

  • 隐式转码问题

    print语句

    读写文件(使用codecs.open),第三方库chardet.detect

    字符串操作(+、==)

    sys.setdefaultencoding的作用范围(慎用)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import sys
    reload(sys)
    sys.setdefaultencoding('utf-8')
    # 三行代码,改变一切
    def welcome_message(byte_string):
    try:
    return u"%s runs your business" % byte_string
    except UnicodeError:
    return u"%s runs your business"%
    unicode(byte_string,encoding=detect_encoding(byte_string))
    print(welcome_message(u"Angstrom (Å®)".encode("latin-1"))

为什么sys.setdefaultencoding()会破坏代码

================

原文Why sys.setdefaultencoding() will break code

我知道更聪明、更有经验的Python程序员之前已经向python-dev提了相关问题,但每次当我需要向别人引用其中一个时,我发现很难找得到。今天当我在Google上搜索这个问题时,发现最相关的条目是我自己在2011年发给yum-devel的一个帖子。我知道以后我肯定有必要向别人证明不应该使用setdefaultencoding()方法,所以为了避免下次再去网络上搜索,我决定在这里发表我的论证。

一些背景

15年以前:支持Unicode的Python问世

对Python2而言,一定程度下我们可以将字节串(str type)和字符串(unicode type)混为一谈,例如:

1
2
3
4
>>> u'Toshio' == 'Toshio'
True
>>> print(u'Toshio' + ' Kuratomi')
Toshio Kuratomi

当你执行这些操作时,Python发现一边是unicode类型,另一边是str类型,于是它取出str的值,将其解码成一个unicode类型,然后继续执行相应的操作。解析这些字节码的编码就是我们说的defaultencoding(根据sys.getdefaultencoding()命名,通过这个函数你可以查看当前的默认编码)。

当Python开发者第一次试验与str截然不同的unicode字符串时,他们不确定defaultencoding应该设置成什么。因此他们创建了sys.setdefaultencoding方法,这个方法在Python程序启动时被调用以试验不同的defaultencoding值带来的不同影响。Python的作者们通过改变自己的site.py文件,观察设置不同的默认编码对代码行为的影响,从而获取更多经验。

最终在2000年8月(写本文时已经过去了14年半),上述的Python试验版本正式成为Python-2.0,它的作者们决定将这个敏感的配置defaultencoding设置成ascii。

我知道今天再次去评价ascii的决定很容易,但是在14年以前字符编码风格比今天更混乱。新出现的编程语言和API已经针对unicode固定两个字节的编码规则进行了优化。但是针对特定自然语言的非unicode一字节编码在那时使用更加广泛。许多数据(甚至在今天)可以包含非ascii文本,而不去声明解码方式。在那个年代,任何游离ascii编码王国之外的人都需要被警醒:他们正进入一片编码恶魔肆意游荡的土地。ascii在许多跨越边界的情况下抛出错误,从而警告人们必须严加看管自己的代码。

然而,在Python-2.0带来unicode功能的同时,Python的作者们却渐渐发现有一个疏忽带来了很不好的影响。这个疏忽便是他们没有删除sys.setdefaultencoding()这个方法。为了弥补这个疏漏,他们在site.py中删除了sys的这一属性,从而避免人们在初始化以外的地方使用setdefaultencoding(),但是他们仍然可以在自己的site.py中改变defaultencoding。

sys.setdefaultencoding()的滥用

随着时间的推移,utf-8编码在Unix-like操作系统和网络传输中占据着统治地位。很多只需处理utf-8编码文本的人厌倦了字符串和字节串混在一起带来的错误。于是他们发现了setdefaultencoding()这根稻草,开始尝试用这种方式摆脱他们遇到的麻烦。

起初,有能力的程序员通过更新Python安装的全局文件site.py来使用setdefaultencoding (),这也是Python官方文档建议的用法,这只在用户自己的机器上有用。不幸的是,这些用户通常都是程序员,他们的程序需要在其他人的机器上运行,比如IT部门、客户以及遍布整个互联网的用户。这意味着更新site.py文件会使他们处于比以前更糟糕的境地:他们的代码在自己的机器上似乎工作良好,却在正真使用该软件的人那里运行奔溃。

由于程序员的关注点仅限于别人能否使用他们的软件,所以他们认为如果自己的软件可以将设置默认编码作为其初始化的一部分,那事情就好办多了。他们不必再强迫别人修改自己的Python安装,因为他们的软件会在运行时做出决定。于是乎他们重新审视了一下sys.setdefaultencoding()这个方法。虽然Python的作者们尽最大努力让这个方法在python启动后不可用,但程序员还是想到了获取这个功能的妙方:

1
2
3
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

一旦这段代码运行,强制字节串转换成字符串的的默认编码将变为utf-8。这意味着当utf-8编码的字节串与unicode字符串混合时,Python将成功地将str类型数据转换为unicode类型,并将它们合并成一个unicode字符串。 这就是新一代的程序员对于他们大部分数据所期待的样子,所以用这几行(不可否认非常的hack)代码解决问题的想法对他们来说非常有吸引力。 不幸的是,这样做有很明显的缺点。

为什么sys.setdefaultencoding()会破坏你的代码

(1)编写一次,改变一切

sys.setdefaultencoding()带来的第一个问题乍一看不是很明显。当你使用这个方法时,即将运行的代码都将受到影响。你的代码,标准库的代码以及不受你管控的第三方代码都将在你设置的默认编码下运行。有些不是你负责的代码依赖的默认编码是ascii,此时它就不会抛出错误,很可能制造一些垃圾数据。比如,你依赖的第三方库有如下代码:

1
2
3
4
5
6
7
8
def welcome_message(byte_string):
try:
return u"%s runs your business" % byte_string
except UnicodeError:
return u"%s runs your business" % unicode(byte_string,
encoding=detect_encoding(byte_string))
print(welcome_message(u"Angstrom (Å®)".encode("latin-1"))

如果没有改变默认编码,这段代码将无法通过ascii解码”Å”,随后进入异常处理,猜测编码并将其正确的转换成unicode字符串,程序会打印出 Angstrom (Å®) runs your business。一旦你将defaultencoding设置为utf-8,代码将使用utf-8解码数据,打印Angstrom (Ů) runs your business

当然,如果这段代码是在你自己的软件中,你完全有能力去处理这个编码问题。但是你并不能对第三方库做这些事情。

(2)我们正破坏字典

设置utf-8为默认编码带来的最严重的问题是破坏了字典的一些行为约定。我们来看下面这段代码:

1
2
3
4
5
6
7
8
9
10
def key_in_dict(key, dictionary):
if key in dictionary:
return True
return False
def key_found_in_dict(key, dictionary):
for dict_key in dictionary:
if dict_key == key:
return True
return False

你认为输入参数相同两个函数的输出会一致吗?在Python中,如果你没有滥用sys.setdefaultencoding()这个方法,那问题答案是肯定的。

1
2
3
4
5
6
7
8
9
10
11
12
>>> # Note: the following is the same as d = {'Café': 'test'} on
>>> # systems with a utf-8 locale
>>> d = { u'Café'.encode('utf-8'): 'test' }
>>> key_in_dict('Café', d)
True
>>> key_found_in_dict('Café', d)
True
>>> key_in_dict(u'Café', d)
False
>>> key_found_in_dict(u'Café', d)
__main__:1: UnicodeWarning: Unicode equal comparison failed to convert both arguments to Unicode - interpreting them as being unequal
False

但是如果我们使用sys.setdefaultencoding('utf-8') 又会发生什么呢?答案是上面的行为会遭到破坏:

1
2
3
4
5
6
7
8
9
10
11
12
>>> import sys
>>> reload(sys)
>>> sys.setdefaultencoding('utf-8')
>>> d = { u'Café'.encode('utf-8'): 'test' }
>>> key_in_dict('Café', d)
True
>>> key_found_in_dict('Café', d)
True
>>> key_in_dict(u'Café', d)
False
>>> key_found_in_dict(u'Café', d)
True

在使用in操作时,程序计算key的hash值然后对比hash值是否相等。在utf-8编码下,只有在ascii编码体系里的字符串的unicode和str的hash值是相等的,其他的字符集下字符串的unicode和str的hash值是不相等的。==则会将字节串解码成unicode然后再比较二者。当你调用sys.setdefaultencoding('utf-8')后,你便允许字节串以utf-8的方式转换成unicode,然后两个字符串对比后发现相等。这样做的后果是in==的测试产生了不同的结果,这与人们习惯的行为相差甚远,大多数人认为这打破了语言的基本约定。

所以Python 3是如何修复这个问题的呢?

你或许已经知道Python 3将默认编码从ascii转变成utf-8,那它如何避免==in带来的问题呢?答案是Python 3不再进行字节串(python3 bytes type)和字符串(python3 str type)之间的隐式转码了。由于这两种类型现在是完全分离的,所以上文进行的“包含测试”和“相等测试”都会返回False

1
2
3
4
5
6
$ python3
>>> a = {'A': 1}
>>> b'A' in a
False
>>> b'A' == list(a.keys())[0]
False

起初在Python 2中,ascii编码体系下字节串和字符串是相等的,这看起来有些滑稽。但是请记住字节串只是一种数字类型,下面的代码并不能像你期望的那样工作:

1
2
3
4
5
>>> a = {'1': 'one'}
>>> 1 in a
False
>>> 1 == list(a.keys())[0]
False

Python装饰器的执行顺序

================

引子

之所以写这篇博客,是因为今天在写代码时遇到了装饰器顺序带来的问题。我用Flask写web后端,需要对一个view方法进行多次装饰,其中一个装饰器用到了另一个装饰器产生的上下文,这就需要那个被依赖的装饰器先执行。所以在编写有相互依赖的装饰器时,我们的确需要多加小心。

一些基础

1. 它的原理

装饰器是Python的语法糖,它可以帮助你写成简洁、易懂、优雅的代码。在Python中一切皆对象,我们来看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
def foo():
print 'hello world'
foo()
# output: hello world
fn = foo
type(fn)
# output: function
fn()
# output: hello world

这里我们定义了函数foo,我们甚至可以将foo赋值给fn,然后调用fn()会和直接调用foo()产生同样的效果。

闲话不说,我们继续看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def decorator(func):
def wrapper():
print 'Do something before func.'
return func()
return wrapper
@decorator
def foo():
print 'hello world'
foo()
# output:
Do something before func.
hello world

我们看到,装饰器能够很容易地在调用被装饰函数之前做一些“其他有用的事情”。其实质就是公用代码的抽离,比如web后端的鉴权行为等。这样做的好处大概就是:

  1. 提高公用代码的利用率
  2. 优雅、易读、好维护
  3. 防止无关代码侵入被装饰函数

我们使用装饰器时,实际上发生了什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
def decorator(func):
def wrapper():
print 'Do something before func.'
return func()
return wrapper
def foo():
print 'hello world'
decorator(foo)()
# output:
Do something before func.
hello world

装饰器的写法只是一个语法糖,上面的代码才是正真的行为,其实就是把待装饰的函数作为参数传入装饰器,做一些其他的事再返回这个函数。

2. 一些改进

当我们单纯的用上面的方式写装饰器时,代码的行为可能并不能使我们满意,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def decorator(func):
def wrapper(*args, **kwds):
"""this is wrapper"""
return func(*args, **kwds)
return wrapper
@decorator
def foo():
"""this is foo"""
pass
print foo.__name__
# output: wrapper
print foo.__doc__
# output: this is wrapper

我们的函数看起来明明是foo,但实际上已经被偷梁换柱了!这样的信息丢失不利于调试,我们自己看起来也很不爽。

幸运的是,强大的Python提供了一个很有用的工具方法functools.wraps,这个方法能使包装函数wrapper看起来更像是被包装的函数。注意我的用词是“”,说明通过wraps装饰,我们只是将func的一些元信息(metadata)赋给了wrapper,仅此而已!我们再对上述代码进行改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwds):
"""this is wrapper"""
return func(*args, **kwds)
return wrapper
@decorator
def foo():
"""this is foo"""
pass
print foo.__name__
# output: foo
print foo.__doc__
# output: this is foo

所以,这样使用装饰器才是正确的打开方式。

多装饰器的执行顺序

回到我们最初关心的问题—多装饰器的执行顺序。还是先看一段代码:

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
def first(func):
def wrapper(*args, **kwargs):
print 'first decorator called.'
return func(*args, **kwargs)
return wrapper
def second(func):
def wrapper(*args, **kwargs):
print 'second decorator called.'
return func(*args, **kwargs)
return wrapper
@first
@second
def foo():
print 'foo called.'
def bar():
print 'bar called.'
if __name__ == '__main__':
foo()
print '======================='
first(second(bar))()
# 运行结果
first decorator called.
second decorator called.
foo called.
=======================
first decorator called.
second decorator called.
bar called.

可以看出,当多个装饰器装饰同一个函数时,从下往上,每个装饰器都只负责装饰它下面的函数。听起来可能会很绕,我们看例子,foo同时被fisrtsecond装饰,则实际的行为是first(second(foo))()first装饰的函数是second装饰foo后得到的结果。

前面我们一直强调装饰器的作用就是做一些“其他有用的事情”,当有多个装饰器时,每一个都要做它自己“有用的事情”。在实际的开发中,这些“有用的事情”可能是相互依赖的,这时候如果我们搞不清楚放置装饰器的顺序,就会出现麻烦!

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
IS_LOGIN = False # 判断登录状态
def login_change(func):
@wraps(func)
def wrapper(*args, **kwargs):
global IS_LOGIN
IS_LOGIN = True
return func(*args, **kwargs)
return wrapper
def login_check(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not IS_LOGIN:
return unauth()
return func(*args, **kwargs)
return wrapper
@login_change
@login_check
def view1():
print 'view1 called'
@login_check
@login_change
def view2():
print 'view2 called'
def unauth():
print 'unauth called'
if __name__ == '__main__':
view1()
IS_LOGIN = False # reset login state
print '======================='
view2()
# 执行结果
view1 called
=======================
unauth called

上面我简单模拟了web后端处理登录认证的流程,全局变量IS_LOGIN代表登录状态,装饰器login_change做的“事情”是将IS_LOGIN置为True,装饰器login_check做的“事情”是检查登录状态,如果未登录转到unauth,否则放行到相应的view。显然这两个装饰器做的“事情”是相互影响的,它们的顺序也就变得至关重要了,view1view2不同的执行结果就是最有力的证明。

总结

装饰器是Python最实用的语法糖之一。使用装饰器能使我们的代码变得优雅、易读,结构清晰。但如果我们想正确使用它,就必须理解它的本质,尤其在多装饰器环境下,有些上下文的处理不当会带来意想不到的后果。所以当你在写这类程序时,一定要把顺序放在心上!