移植到python3过程中遇到的pymysql的问题

近来在把项目从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的字符串机制不同有关,网上提供的解决方案大致有两种:

  1. 在pymysql连接时设置charset参数。(例如此贴
  2. 在执行excute时对sql字符创做encoding处理。(例如此贴

经验证,这两种方法都是有效的,但我关心的是问题是怎么发生的。

跟踪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_unicodetrue,但其实这是没有必要的。在Connection的构造函数里能看到以下注释:

    use_unicode:
      Whether or not to default to unicode strings.
      This option defaults to true for Py3k.

说明该参数在python3下是默认为true的,实际测试也是如此。