Py学习  »  Python

从Python2迁移到Python3实战(一) - pyupgrade

Python之美 • 4 年前 • 387 次点击  

前言

从这篇开始我会不定期写一些在实际工作中把项目代码从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是一个很好的补充,我们来了解一下它都实现了那些功能

集合

  1. set(()) # set()

  2. set([]) # set()

  3. set((1,)) # {1}

  4. set((1, 2)) # {1, 2}

  5. set([1, 2]) # {1, 2}

  6. set(x for x in y) # {x for x in y}

  7. set([x for x in y]) # {x for x in y}

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

字典解析

  1. dict((a, b) for a, b in y) # {a: b for a, b in y}

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

同上,属于统一用法

Python2.7+ Format说明符

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

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

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

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

  1. '%s %s' % (a, b) # '{} {}'.format(a, b)

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

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

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

Unicode literals

  1. u'foo' # 'foo'

  2. u"foo" # 'foo'

  3. u'''foo''' # '''foo'''

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

  1. cat unicode_literals.py

  2. u'foo' # 'foo'

  3. u"foo" # 'foo'

  4. u'''foo''' # '''foo'''


  5. pyupgrade --py36-plus unicode_literals.py

  6. Rewriting unicode_literals.py


  7. cat unicode_literals.py

  8. 'foo' # 'foo'

  9. "foo" # 'foo'

  10. '''foo''' # '''foo'''

Invalid escape sequences

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

  1. # strings with only invalid sequences become raw strings

  2. '\d' # r'\d'

  3. # strings with mixed valid / invalid sequences get escaped

  4. '\n\d' # '\n\\d'

  5. # `ur` is not a valid string prefix in python3

  6. u'\d' # u'\\d'


  7. cat escape_seq.py

  8. '\d' # r'\d'


  9. flake8 escape_seq.py

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


  11. pyupgrade escape_seq.py

  12. Rewriting escape_seq.py


  13. cat escape_seq.py

  14. r'\d' # r'\d'

is / isnot

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

  1. python

  2. Python 3.8.0a4+ (heads/master:289f1f80ee, May 9 2019, 07:16:38)

  3. [Clang 10.0.0 (clang-1000.11.45.5)] on darwin

  4. Type "help", "copyright", "credits" or "license" for more information.

  5. >>> 1 is 1

  6. <stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?

  7. True

  8. >>> 1 is not 1

  9. <stdin>: 1: SyntaxWarning: "is not" with a literal. Did you mean "!="?

  10. False

  11. >>>

pyupgrade会做如下替换:

  1. x is 5 # x == 5

  2. x is not 5 # x != 5

  3. x is 'foo' # x == foo

ur字符串文字

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

  1. ur'foo' # u'foo'

  2. ur'\s' # u'\\s'

  3. # unicode escapes are left alone

  4. ur'\u2603' # u'\u2603'

  5. ur'\U0001f643' # u'\U0001f643'

数字的L后缀

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

  1. 5L # 5

  2. 5l # 5

  3. 123456789123456789123456789L # 123456789123456789123456789

八进制数字

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

  1. # Python 2

  2. In : import os


  3. In : !touch 1.txt


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


  5. In : ll 1.txt

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


  7. # Python 3

  8. In : os.chmod('1.txt', 0644)

  9. File "", line 1

  10. os.chmod('1.txt', 0644)

  11. ^

  12. SyntaxError: invalid token



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


  14. In : ll 1.txt

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

pyupgrade会帮助修复这个问题:

  1. 0755 # 0o755

  2. 05 # 5

super()

  1. class C(Base):

  2. def f(self):

  3. super(C, self ).f() # super().f()

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

新式类

  1. class C(object): pass # class C: pass

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

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

移除six相关兼容代码

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

  1. six.text_type # str

  2. six.binary_type # bytes

  3. six .class_types # (type,)

  4. six.string_types # (str,)

  5. six.integer_types # (int,)

  6. six.unichr # chr

  7. six.iterbytes # iter

  8. six.print_(...) # print(...)

  9. six.exec_(c, g, l) # exec(c, g, l)

  10. six.advance_iterator(it) # next(it)

  11. six.next(it) # next(it)

  12. six.callable(x) # callable(x)


  13. from six import text_type

  14. text_type # str


  15. @six.python_2_unicode_compatible # decorator is removed

  16. class C:

  17. def __str__(self):

  18. return u'C()'


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


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


  21. isinstance(..., six.class_types) # isinstance(..., type)

  22. issubclass(..., six.integer_types) # issubclass(..., int)

  23. isinstance(..., six.string_types) # isinstance(..., str)


  24. six.b('...') # b'...'

  25. six.u('...') # '...'

  26. six.byte2int(bs) # bs[0]

  27. six.indexbytes(bs, i) # bs[i]

  28. six.iteritems(dct) # dct.items()

  29. six.iterkeys(dct) # dct.keys()

  30. six.itervalues(dct) # dct.values()

  31. six.viewitems(dct) # dct.items()

  32. six.viewkeys(dct) # dct.keys()

  33. six.viewvalues(dct) # dct.values()

  34. six.create_unbound_method(fn, cls) # fn

  35. six.get_unbound_method(meth) # meth

  36. six.get_method_function(meth) # meth.__func__

  37. six.get_method_self(meth) # meth.__self__

  38. six.get_function_closure(fn) # fn.__closure__

  39. six.get_function_code(fn) # fn.__code__

  40. six.get_function_defaults(fn) # fn.__defaults__

  41. six.get_function_globals(fn) # fn.__globals__

  42. six.assertCountEqual(self, a1, a2) # self.assertCountEqual(a1, a2)

  43. six.assertRaisesRegex(self, e, r, fn) # self.assertRaisesRegex(e, r, fn)

  44. six.assertRegex(self, s, r) # self.assertRegex(s, r)

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

f-strings

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

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

  2. '{} {}'.format(foo, bar) # f'{foo} {bar}'

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

后记

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

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

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/32747
 
387 次点击