个性化阅读
专注于IT技术分析

转到Python 3并再次转回来:是否值得进行切换?

本文概述

Python 3已经存在七年了, 但有些人仍然更喜欢使用Python 2而不是较新的版本。对于初次接触Python的新手而言, 这尤其是个问题。我是在以前的工作环境中与完全相同的同事一起意识到这一点的。他们不仅不知道两个版本之间的差异, 甚至不知道自己已安装的版本。

不可避免地, 不同的同事安装了不同版本的口译员。如果他们后来试图盲目地分享他们之间的脚本, 那将是一场灾难。

相反, 这并不是他们的错。为了消除有时会影响我们选择的FUD面纱(恐惧, 不确定性和怀疑), 需要加大记录和提高意识的努力。因此, 本文是为他们而设计的, 或者是为那些已经使用Python 2但不确定要升级到下一版本的人的, 可能是因为他们只是在版本3不够完善且对库的支持更差的开始才尝试版本3。

两种方言, 一种语言

首先, Python 2和Python 3是不同的语言是真的吗?这不是一个小问题。即使有人用”不, 这不是一门新语言”来解决这个问题, 事实上, 也有一些提议破坏了兼容性而不产生重要优势的提议被拒绝了。

Python 3是Python的新版本, 但不一定要与为Python 2编写的代码向后兼容。与此同时, 可以编写与两个版本兼容的代码, 这不是偶然的, 而是对它的明确承诺。起草了几个PEP(Python扩展提案)的Python开发人员。在少数几种语法不兼容的情况下, 由于Python是一种语言, 我们可以在运行时动态修改代码, 因此我们可以解决该问题, 而不必依赖预处理器, 而该语法与该语言的其余部分完全无关。

因此语法不是问题(尤其是忽略3.3之前的Python 3版本)。另一个很大的不同是代码的行为, 其语义以及仅针对两个版本之一的大库的存在/不存在。这确实是一个严重的问题, 但对于已经具有其他编程语言经验的人来说, 这并不是唯一的或新的。你可能已经碰巧获得了一个旧的代码库/库, 但该库/库无法使用最初使用的同一编译器的最新版本进行构建。在这种情况下, 编译器本身将为你提供帮助(在Python中, 帮助将来自你自己的测试套件)。

为什么要使新版本与众不同?这些变化将为我们带来什么好处?

一个具体的例子

假设我们要编写一个程序来读取当前目录中的文件/目录的所有者(在Unix系统上)并在屏幕上打印它们。

# encoding: utf-8

from os import listdir, stat

# to keep this example simple, we won't use the `pwd` module
names = {1000: 'dario', 1001: u'олга'}

for node in listdir(b'.'):
    owner = names[stat(node).st_uid]
    print(owner + ': ' + node)

一切正常吗?显然是的。我们为包含源代码的文件指定了编码, 如果我们在目录中有一个由олга(uid 1001)创建的文件, 则其名称将被正确打印, 即使我们具有非ASCII名称的文件也将被正确打印。

仍然有一个我们尚未涉及的情况:由олгаAND创建的文件, 名称中包含非ASCII字符…

su олга -c "touch é"

让我们尝试再次启动我们的小脚本, 我们将获得:

UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 0: ordinal not in range(128)

如果考虑一下, 可能会遇到类似的情况:编写了程序(数千行, 而不是本例中的四行), 你开始收集一些用户, 其中一些甚至来自非英语国家/地区具有更多的异国名称。一切正常, 直到这些用户之一决定创建一个文件, 具有更普通名称的用户可以毫无问题地创建该文件。现在, 你的代码将引发错误, 服务器可能会以500的错误回答该用户的每个请求, 并且你需要挖掘代码库以了解为什么突然出现这些错误。

Python 3如何帮助我们呢?如果你尝试执行相同的脚本, 则会发现Python能够在你即将执行危险操作时立即检测到。即使没有具有特殊名称和/或由特殊用户创建的文件, 你也将立即收到以下异常:

`TypeError: Can't convert 'bytes' object to str implicitly`

相关行:

print(owner + ': ' + node)

我认为错误消息更易于理解。 str对象是所有者, 而node是字节对象。知道了这一点, 显然问题是由于listdir向我们返回了一个字节对象列表。

并非所有人都知道的一个细节是listdir返回一个字节对象或unicode字符串的列表, 具体取决于用作输入的对象的类型。我避免完全使用listdir(‘。’)在Python 2和Python 3上获得相同的行为, 否则在Python 3上这将是一个unicode字符串, 这将使该错误消失。

如果我们尝试将单个字符从listdir(b’。’)更改为listdir(u’。’), 我们将能够看到代码现在在Python 3和Python 2上如何工作。为了完整性, 我们应该也将” dario”更改为” u’dario”。

但是, Python 2和Python 3在行为上的差异由两个版本处理字符串类型的方式上的根本差异所支持, 这种差异主要在从一个版本移植到另一个版本时就可以看出。

在我看来, 这种情况是格言的象征:”分割器比分割器更容易集总”。在Python 2中集中在一起的内容(unicode字符串和默认字节字符串, 可以自由地强制在一起)已在Python 3中拆分。

自动转换工具

因此, 像2to3这样的工具即使编写得很好并且对于自动转换其他所有差异非常有用, 但也存在一些局限性。通过字节/ unicode进行拆分, 可以在运行时在行为表面上有所不同, 因此, 如果你具有将这两种类型混合在一起的庞大的Python 2代码库, 那么只能进行解析/静态分析的工具将无法为你省钱。你必须收拾行囊并正确设计API, 以决定到目前为止是否可以不加选择地接受任何类型的字符串的函数现在仅适用于其中某些(以及哪些)字符串。相反, 尽管使用量减少了很多, 但从Python 3转换为Python 2的工具的寿命要短得多。让我们来看一个例子:

前一段时间, 我写了一个玩具HTTP服务器(仅依赖项:python-magic), 这是Python 2的版本(从Python 3自动转换而无需手动更改):https://gist.github。 com / berdario / 8abfd9020894e72b310a

现在, 如果你愿意, 可以直接查看使用2to3转换为Python 3的代码, 也可以直接在系统上进行转换。尝试执行该错误时, 你将意识到可以尝试手动修复的每个错误与字节/ unicode拆分有关。

你可以手动应用如下更改:https://gist.github.com/berdario/34370a8bc39895cae139/revisions

因此, 你可以使程序再次在Python 3上运行。这些变化并不是复杂的更改, 但是它们仍然需要推理出函数所使用的数据类型以及控制流。它在120项更改中有13项更改, 这个比例不太容易处理:要移植成千上万行代码, 你很容易最终要进行数百次修改。

如果你感到好奇, 则可以尝试将刚带到Python 3的代码转换回Python2。使用3to2, 你将获得以下信息:https://gist.github.com/berdario/cbccaf7f36d61840e0ed。其中唯一需要手动应用的更改是第55行的.encode(‘utf-8’)。

从Python 3开始(如果你需要将其转换回Python 2), 则更加容易。但是, 如果你需要使代码在另一个版本上工作, 那么像这样的完整转换不是最佳选择。保持与两个Python版本的兼容性要好得多。为此, 你可以依靠诸如futurize之类的工具。

Python 3不仅仅与Unicode有关

即使你没有机会在生产环境中使用Python 3(也许你正在使用的库之一庞大且仅与Python 2兼容), 我还是建议你保持代码与Python 3兼容。你甚至可以对不兼容的库进行存根/模拟, 以使你可以在两个版本上连续运行测试。这将使你日后最终准备好迁移到Python 3时更加轻松, 更不用说它如何帮助你更好地设计API或像本文开头的示例中那样识别错误。发布。

所有这些关于移植和字节/ unicode差异的讨论, 即使你最初对使用/启动Python 3持怀疑态度, 也可能使你将其视为较小的邪恶, 而不是将来解决移植。但是, 如果移植是棍子, 那胡萝卜在哪里?它是语言及其标准库中添加的新功能吗?

好吧, 从最后一个次要版本的Python 2发布开始, 经过5年的时间, 大量有趣的花絮正在堆积。例如, 我发现自己经常依赖诸如仅关键字的新参数之类的东西。

可选关键字参数

当我想编写一个将任意数量的词典合并在一起的函数时(类似于dict.update所做的, 但不修改输入内容), 我发现添加一个函数参数让调用者自定义逻辑是很自然的。这样, 可以按以下方式调用此函数, 以通过将值保留在最右边的字典中来简单地合并多个字典。

merge_dicts({'a':1, 'c':3}, {'a':4, 'b':2}, {'b': -1})
# {'b': -1, 'a': 4, 'c': 3}

同样, 通过添加值进行合并:

from operator import add
merge_dicts({'a':1, 'c':3}, {'a':4, 'b':2}, {'b': -1}, withf=add)
# {'b': 1, 'a': 5, 'c': 3}

要在Python 2中实现这样的API, 需要定义** kwargs输入并寻找withf参数。但是, 如果调用者确实将参数键入为(例如)withfun, 则错误将被忽略。相反, 在Python 3中, 最好在变量参数后添加一个可选参数(并且只能与其关键字一起使用):

def second(a, b):
    return b

def merge_dicts(*dicts, withf=second):
    newdict = {}
    for d in dicts:
        shared_keys = newdict.keys() & d.keys()
        newdict.update({k: d[k] for k in d.keys() - newdict.keys()})
        newdict.update({k: withf(newdict[k], d[k]) for k in shared_keys})
    return newdict

开箱操作员

从Python 3.5开始, 天真的合并实际上可以使用新的拆包运算符完成。但即使在3.5之前, Python仍具有改进的解压缩形式:

a, b, *rest = [1, 2, 3, 4, 5]
rest
# [3, 4, 5]

从3.0开始就可以使用此功能。与解构类似, 这种解压缩是功能语言(通常用于流控制)中模式匹配的有限/即席形式, 也是动态语言(如Ruby和Javascript)的常见功能(支持适用于EcmaScript 2015)。

更简单的可迭代API

在Python 2中, 许多处理可迭代对象的API都是重复的, 并且默认的API具有严格的语义。现在, 一切都会根据需要生成值:zip(), dict.items(), map(), range()。你是否要编写自己的枚举版本?在Python 3中, 这就像将标准库中的函数组合在一起一样简单:

zip(itertools.count(1), 'abc')

等效于enumerate(‘abc’, 1)。

功能注释

你是否要像这样简单地定义HTTP API?

@get('/balance')
def balance(user_id: int):
    pass
    
from decimal import Decimal

@post('/pay')
def pay(user_id: int, amount: Decimal):
    pass

不再使用” <int:user_id>”即席语法, 并且无需定义自己的转换器即可在路由内使用任何类型/构造函数(如Decimal)的功能。

这样的事情已经实现了, 你看到的是有效的Python语法, 利用新的注释使编写也具有自我说明性的API更加方便。

本文总结

这些只是几个简单的示例, 但是这些改进意义深远, 可以最终帮助你编写更强大的代码。一个示例是默认情况下启用的异常链回溯, 在Ionel CristianMăriethe的恰当命名的文章” Python 3中最被低估的功能”中进行了展示, Aaron Maxwell在另一篇文章中也对此进行了介绍, 以及更严格的Python比较语义3, 新的超级行为。

这还不是全部。还有很多其他改进, 这些是我认为每天影响最大的改进:

  • 现在有几个函数/构造函数返回上下文管理器, 从而简化了关闭对象和管理错误的过程:gzip.open(早于2.7), mmap, ThreadPoolExecutor, memoryview, FTP, TarFile, socket.create_connection, epoll, NNTP, SMTP, aifc.open, Shelf , 和更多。
  • lzma, 与gzip相比压缩率更高
  • asyncio, 用于Python中的异步编程
  • pathlib, 一种更pythonic且更具表现力的方式来操作路径
  • IP地址
  • lru_cache, 自动缓存昂贵函数的结果
  • 模拟(与上述模块相同, 以前只能从PyPI获得)
  • 内存中更有效的字符串表示
  • OrderedDict, 每个人至少需要一次
  • pdb中的自动补全
  • __pycache__目录, 有助于避免在其他项目文件夹中堆满.pyc文件

可以从文档的”新增功能”页面获得更全面的全景图, 或者对于更改的其他概述, 我也建议Aaron Maxwell撰写的另一篇文章以及Brett Cannon的这些幻灯片。

Python 2.7将在2020年之前受支持, 但请不要等到2020年再使用新的(更好的)版本!

相关:Python设计模式:适用于时尚代码

赞(0)
未经允许不得转载:srcmini » 转到Python 3并再次转回来:是否值得进行切换?

评论 抢沙发

评论前必须登录!