近来在把项目从python2迁移到python3的过程中遇到了不少坑,这些坑大致分几类:
- 函数被弃用(如:
dict.iteritems()
) - 包位置发生变更(如:urllib2被合并到urllib;HTMLParser被迁移到html.parser;等)
- 字符串相关问题
在python2中,字符串有string, unicode两种类型,python3对字符串机制做了些改革,字符串只有string一种类型(但它是unicode编码的),另外增加bytes类型支持二进制数据。这本来是个不错的改进,但对于从python2移植过来的程序来说,有可能会产生不少问题。
我遇到的问题与pymysql相关。原本在python2下能正常运行的程序,在python3下产生以下异常:
'latin-1' codec can't encode characters in position 42-43: ordinal not in range(256)
上网搜了一下,大致认定问题与python2、3的字符串机制不同有关,网上提供的解决方案大致有两种:
经验证,这两种方法都是有效的,但我关心的是问题是怎么发生的。
跟踪pymysql的源代码,能看到Cursor.excute
最终执行的是Connection.query
,代码如下:
def query(self, sql, unbuffered=False):
# if DEBUG:
# print("DEBUG: sending query:", sql)
if isinstance(sql, text_type) and not (JYTHON or IRONPYTHON):
if PY2:
sql = sql.encode(self.encoding)
else:
sql = sql.encode(self.encoding, 'surrogateescape')
self._execute_command(COMMAND.COM_QUERY, sql)
……
这几行代码能解释之前所发生的现象。在python3中,如果在connect
的时候没有指定charset
,则会按照默认编码(latin-1
)进行encoding,默认编码容纳不了中文字符,所以当字符串中含有中文时就会出现之前提到的异常,这就是 方法1 能够奏效的原理。另外,如果采用方法2,那么条件isinstance(sql, text_type)
便不会被触发,因此也不会产生异常,这就是 方法2 能够奏效的原理。
同时,了解原理之后很容易理解 方法1 是更简单高效的解决方案。
补充:
有些网上的帖子提到在python3中除了要指定charset
以外,还要设置use_unicode
为true
,但其实这是没有必要的。在Connection
的构造函数里能看到以下注释:
use_unicode: Whether or not to default to unicode strings. This option defaults to true for Py3k.
说明该参数在python3下是默认为true的,实际测试也是如此。