顺序后果

2020-08-20 10:24:15

在编程职业生涯中-至少有一个像我这样四处游荡的人-在这个时候,学习一门新的编程语言几乎不会成为一种障碍。我说的不是像APL这样让人头脑融化的不同语言,而是你的普通主流对象/命令式混搭。拿一份语法小抄,浏览一下标准库文档,然后就可以出发了。

最近,我的个人咨询业务把我带入了Python的巢穴,这是一种我在过去15年里一直设法避免的语言。令我有些吃惊的是,我发现我相当喜欢它。我不会说它很棒,但它很容易拿起来并完成工作。打包和命名空间是一团糟,但并不比同时代的任何其他语言差。

我发现Python有足够的函数范例来防止我将其抛出窗口。当然,没有什么能与Clojure相比,但已经足够了。

然后我撞上了绊脚石。我正在编写一个API,它需要接收一组东西。我需要使用集合的第一项进行一些初始设置,然后迭代整个集合。它看起来足够直截了当:

Def do_work(Things):First_Thing=Things[0]打印(";初始设置为%r";%first_thing)in Things:Print(";使用%r";%Thing做有用的事情)。

Do_work([1,2,3])初始设置1做一些有用的事情,1做一些有用的事情,2做一些有用的事情,3做一些有用的事情。

然后,在另一段代码中,我决定推动我对Python日益增长的信心,并尝试一下我听说过的这些新奇的生成器之一:

好吧,你抓到我了,我在学习一门新语言时犯了一个最大的错误:把它当作另一门我已经会的语言。在本例中,我试图以与使用Clojure序列相同的方式使用Python生成器。

Clojure序列是不可变的,只要生成的值是可以访问的,它就会自动缓存它们。在Clojure中,我会编写如下内容:

(Defn do-work[Things](let[First-Things(First Things)](println";Initial Setup with";First-Thing)(Doseq[Things Things](println";Doing Thing with";Thing)。

不变性使事情变得更简单:仅仅因为我做了(第一件事)并不会改变事物的价值。

Python生成器更像是挂起的计算;一旦它们产生一个值,就无法将其取回。因此,Python不会让我为生成器“下标”-就像在Things[0]中那样-因为生成器不能满足列表的预期行为,也就是说,元素在您查看之后保持不变。

我很快就理解了Python错误消息。然后,我花了几分钟在互联网上四处寻找等同于Clojure的不变序列的Python。可能有这样的野兽,但我没找到。

当然,有很多解决方法,其中最简单的是将生成器强制到列表中:

Do_work(LIST(GENERATE(3)初始设置使用';生成值0';做有用的事情;使用';生成值0';做有用的事情使用';生成值1';做有用的事情使用';生成值2';

我本可以停在那里,过得很好。但是那个列表()卡在我的审美师的胃口里了。强迫整个生成器在内存中实现,这样我就可以抓住第一个元素,这感觉很浪费。谁知道呢,也许有一天,当它递给一台生产数千件物品的发电机时,它甚至会破裂。

从itertools导入chaindef do_work(Things):Iterator=ITER(Things)First_Thing=Next(Iterator)Iterator=Chain([First_Thing],Iterator)打印(";使用%r";%First_Thing的初始设置)in Iterator:print(";使用%r";%Thing做一些有用的事情)。

这不是我所说的优雅。但是它可以工作,而且很灵活:对象可以是任何可迭代类型,包括生成器或常规列表。

(对于房间里的闭合专家来说,chain()类似于Clojure的Concat:它用两个可迭代的东西创建一个新的迭代器。)。

在我完成了关于愚蠢的语言(不把不变的值作为普遍的缺省值)的必要论述之后,我开始思考:Clojure的不变序列是优雅和强大的,但是它们的设计有一些微妙的后果,经常会让人绊倒。

几乎每个学习Clojure的人都发现了Head错误,这是序列缓存的直接结果。坚持足够长的时间,你最终会遇到堆叠序列的问题。这些bug在开发过程中可能不会出现;只有当它们在生产中达到足够大的序列时,它们才会回来咬您。

此外,需要了解幕后发生的事情-了解Clojure序列的实际工作方式,而不是Surface API-才能理解问题。在内存泄漏导致问题之前识别它们通常需要经验和模式识别。对于那些刚刚拿到语法小抄并浏览了标准库文档的人来说,很有可能编写该错误,但知道如何修复它的可能性不大。

相比之下,Python的生成器和迭代器更像…。机械的。你可以看到东西是如何工作的,因为大部分部件都暴露在外面,让你看。虽然我确信有一些方法可以用Python编写内存泄漏,但我猜迭代器不是常见的来源。

这并不是说一种方法比另一种方法更好。我不会再重复第n次越糟越好,也不会重复泄露抽象法则。我只是注意到,无论哪种方式,都是有取舍的。更“优雅”的抽象可以生成更短、更具表现力的程序,但代价是隐藏的复杂性。暴露的机械装置更容易观察,但会迫使您将注意力花在机械细节上。