前言

从这篇开始我会不定期写一些在实际工作中把项目代码从 Python2.7 迁移到最新的 Python 3.7 的经验。

这篇先介绍 pyupgrade - 一个修改代码中 Python 2 语法到最新版本写法的工具,同时它还可以作为 pre-commit 钩子,可以在代码提交或者 push 时拒绝引入旧的用法。

为什么需要这么一个工具呢?3 个理由:

  1. 替换代码中旧版本 Python 的用法。例如 '% s % s' % (a, b) 这种百分号的字符串格式化写法
  2. 替换成 Python 3 的新语法。例如在 Python 3 中 super 不再需要传递 self、字符串格式化在 Python 3.6 及以后可以直接用 f-strings
  3. 迁移后不再需要支持 Python2,所以应该去掉 six 模块的相关使用,直接用 Python3 的代码写才是正途。

我日常维护的项目中 Python 代码都在几千到上百万行级别,可以设想一下,如果人工来做代码替换将是一个极为浩大的工程。

在现有的 Python 世界,过去只有 lib2to3 模块和其衍生品(之后我会专门讲),但是效果有限,pyupgrade 是一个很好的补充,我们来了解一下它都实现了那些功能

集合

set(())              # set()
set([])              # set()
set((1,))            # {1}
set((1, 2))          # {1, 2}
set([1, 2])          # {1, 2}
set(x for x in y)    # {x for x in y}
set([x for x in y])  # {x for x in y}

左面是替换前的代码,后面井号后的注释部分是替换后的效果。set 相关的部分算是统一用法,并不是左面的写法在 Python3 已经不可用。

字典解析

dict((a, b) for a, b in y)    # {a: b for a, b in y}
dict([(a, b) for a, b in y])  # {a: b for a, b in y}

同上,属于统一用法

Python2.7+ Format 说明符

'{0} {1}'.format(1, 2)    # '{} {}'.format(1, 2)
'{0}' '{1}'.format(1, 2)  # '{}' '{}'.format(1, 2)

从 Python2.7 开始,不再强制指定索引

使用 str.format 替代 printf 风格的字符串 format 写法

'%s %s' % (a, b)                  # '{} {}'.format(a, b)
'%r %2f' % (a, b)                 # '{!r} {:2f}'.format(a, b)
'%(a)s %(b)s' % {'a': 1, 'b': 2}  # '{a} {b}'.format(a=1, b=2)

后面的是 Python2.7 推荐的写法。但是可以传入--keep-percent-format忽略这类修改。

Unicode literals

u'foo'      # 'foo'
u"foo"      # 'foo'
u'''foo'''  # '''foo'''

在 Python3 中,u'foo' 其实已经是字符串的 'foo',默认是不会修改这个类型数据的,除非传入--py3-plus或者--py36-plus:

❯ cat unicode_literals.py
u'foo'      # 'foo'
u"foo"      # 'foo'
u'''foo'''  # '''foo'''

❯ pyupgrade --py36-plus unicode_literals.py
Rewriting unicode_literals.py

❯ cat unicode_literals.py
'foo'      # 'foo'
"foo"      # 'foo'
'''foo'''  # '''foo'''

Invalid escape sequences

现在 flake8 已经会检查出这个类型错误 (W605):

# strings with only invalid sequences become raw strings
'\d'    # r'\d'
# strings with mixed valid / invalid sequences get escaped
'\n\d'  # '\n\\d'
# `ur` is not a valid string prefix in python3
u'\d'   # u'\\d'

 cat escape_seq.py
'\d'    # r'\d'

 flake8 escape_seq.py
escape_seq.py:1:2: W605 invalid escape sequence '\d'

 pyupgrade escape_seq.py
Rewriting escape_seq.py

 cat escape_seq.py
r'\d'    # r'\d'

is / is not

is/is not从 Python3.8 开始会抛出 SyntaxWarning 错误,应该使用==/!=替代:

 python
Python 3.8.0a4+ (heads/master:289f1f80ee, May  9 2019, 07:16:38)
[Clang 10.0.0 (clang-1000.11.45.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 1 is 1
<stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
True
>>> 1 is not 1
<stdin>:1: SyntaxWarning: "is not" with a literal. Did you mean "!="?
False
>>>

pyupgrade 会做如下替换:

x is 5      # x == 5
x is not 5  # x != 5
x is 'foo'  # x == foo

ur字符串文字

ur'...'这种用法在 python3 已经不可用了:

ur'foo'         # u'foo'
ur'\s'          # u'\\s'
# unicode escapes are left alone
ur'\u2603'      # u'\u2603'
ur'\U0001f643'  # u'\U0001f643'

数字的 L 后缀

在 Python2 数字后面会有 L 后缀,在 Python3 不再支持了:

5L                            # 5
5l                            # 5
123456789123456789123456789L  # 123456789123456789123456789

八进制数字

这个最常见的用法是修改文件权限,在 Python2 中可以直接使用 0755,但是 Python3 中这样是错误的:

# Python 2
In : import os

In : !touch 1.txt

In : os.chmod('1.txt', 0755)

In : ll 1.txt
-rwxr-xr-x 1 dongwm 0 May  9 07:26 1.txt*  # 755权限正常

# Python 3
In : os.chmod('1.txt', 0644)
  File "<ipython-input-4-46ae418e46c4>", line 1
    os.chmod('1.txt', 0644)
                         ^
SyntaxError: invalid token


In : os.chmod('1.txt', 0o644)

In : ll 1.txt
-rw-r--r-- 1 dongwm 0 May  9 07:26 1.txt

pyupgrade 会帮助修复这个问题:

0755  # 0o755
05    # 5

super()

class C(Base):
    def f(self):
        super(C, self).f()   # super().f()

在 Python3 中,使用 super 不再需要手动传递 self,传入--py3-plus或者--py36-plus会修复这个问题。

新式类

class C(object): pass     # class C: pass
class C(B, object): pass  # class C(B): pass

Python3 中只有新式类,传入--py3-plus或者--py36-plus会修复这个问题。

移除 six 相关兼容代码

当完全迁移到 Python3 之后,就没必要兼容 Python2 了,可以传入--py3-plus或者--py36-plus去掉 six 相关代码:

six.text_type             # str
six.binary_type           # bytes
six.class_types           # (type,)
six.string_types          # (str,)
six.integer_types         # (int,)
six.unichr                # chr
six.iterbytes             # iter
six.print_(...)           # print(...)
six.exec_(c, g, l)        # exec(c, g, l)
six.advance_iterator(it)  # next(it)
six.next(it)              # next(it)
six.callable(x)           # callable(x)

from six import text_type
text_type                 # str

@six.python_2_unicode_compatible  # decorator is removed
class C:
    def __str__(self):
        return u'C()'

class C(six.Iterator): pass              # class C: pass

class C(six.with_metaclass(M, B)): pass  # class C(B, metaclass=M): pass

isinstance(..., six.class_types)    # isinstance(..., type)
issubclass(..., six.integer_types)  # issubclass(..., int)
isinstance(..., six.string_types)   # isinstance(..., str)

six.b('...')                            # b'...'
six.u('...')                            # '...'
six.byte2int(bs)                        # bs[0]
six.indexbytes(bs, i)                   # bs[i]
six.iteritems(dct)                      # dct.items()
six.iterkeys(dct)                       # dct.keys()
six.itervalues(dct)                     # dct.values()
six.viewitems(dct)                      # dct.items()
six.viewkeys(dct)                       # dct.keys()
six.viewvalues(dct)                     # dct.values()
six.create_unbound_method(fn, cls)      # fn
six.get_unbound_method(meth)            # meth
six.get_method_function(meth)           # meth.__func__
six.get_method_self(meth)               # meth.__self__
six.get_function_closure(fn)            # fn.__closure__
six.get_function_code(fn)               # fn.__code__
six.get_function_defaults(fn)           # fn.__defaults__
six.get_function_globals(fn)            # fn.__globals__
six.assertCountEqual(self, a1, a2)      # self.assertCountEqual(a1, a2)
six.assertRaisesRegex(self, e, r, fn)   # self.assertRaisesRegex(e, r, fn)
six.assertRegex(self, s, r)             # self.assertRegex(s, r)

目前还有six.add_metaclass这个点没有实现,其他的都可以了~

f-strings

这是我最喜欢的一个功能,现在迁移到 Python3 都会迁到 Python3.6+,所以可以直接使用--py36-plus参数,字符串格式化不需要用 str.format,而是直接用 f-strings:

'{foo} {bar}'.format(foo=foo, bar=bar)  # f'{foo} {bar}'
'{} {}'.format(foo, bar)                # f'{foo} {bar}'
'{} {}'.format(foo.bar, baz.womp}       # f'{foo.bar} {baz.womp}'

后记

项目地址:https://github.com/asottile/pyupgrade

我已经在酱厂最大的几个项目之一应用了 pyupgrade,已经达到生产环境使用的标准,请放心使用~