Python设计的错误

42
不久之前,我在学习Javascript时,学习了Javascript: the good parts,尤其喜欢其中关于坏部分和丑陋部分的章节。当然,我并不完全同意其中的所有内容,因为总结一种编程语言的设计缺陷在某种程度上是主观的,尽管例如,我猜每个人都会认为Javascript中的关键字with是一个错误。尽管如此,我发现阅读这样的评论很有用:即使一个人不同意,也有很多可以学习的东西。
是否有一篇博客文章或一些描述Python设计错误的书籍?例如,我猜测有些人会认为缺乏尾调用优化是一个错误;可能还有其他值得学习的问题(或非问题)。

9
尾调用优化并非语言设计问题。 - user395760
8
根据一些人的说法,问题在于缺乏它,这确实不仅是优化问题:它阻止使用递归编程风格,因为你很快就会超过最大递归限制。如果您在谷歌上搜索Python尾递归优化,您将会找到很多关于此的讨论,包括Guido撰写的几篇博客文章。 - Andrea
12
是的,但是优化尾调用或不进行优化并不是语言设计问题,而是实现细节。在不改变语言本身的情况下,完全可以进行尾调用的优化。不同的实现方式(例如PyPy),以及CPython的未来版本可能会对此进行优化。 - user395760
8
实际上这不是一个实现细节,请阅读Guido的文章了解详情(http://neopythonic.blogspot.com/2009/04/tail-recursion-elimination.html)。 - Björn Pollex
8
如果一个语言规范没有明确规定它必须具有尾递归优化,那么你就不能依赖它。如果你不能依赖它,那么你就不能真正编写使用它的代码,因为在没有这个功能的实现上会引发错误。知道不能使用尾递归优化意味着你需要以不同方式编写代码。这不仅仅是“编写最干净的代码,让实现使其更快”的问题。 - RHSeeger
显示剩余10条评论
13个回答

33

4
+1 是指一个没有抨击 Python 的理由,但列举了许多相对明显可以被认为是错误的事情的来源。 - user395760
3
哎呀,我认为 Python 最大的优点之一就是它打破了向后兼容性。我希望更多的编程语言有这个胆量。试想一下,如果我们仍然必须使用 VHS 因为 DVDs 打破了向后兼容性,那该怎么办呢? - Konrad Rudolph
4
虽然这些决定已经无关紧要,但有可能会有人不同意。设计Python 3的人认为这些是错误 - 他们是聪明的Python程序员,但这并不意味着他们选择不保留的内容就不是一个问题。 - user395760
3
@Konrad Rudolph - 我希望我能装备你并将你送回到C99标准化时期。 - detly
2
@S.Lott请阅读Guido的邮件,了解他停止推动Python版本并稳定Python3(至少2-3年)并使其被采用的原因。在SO上,我看到人们甚至还没有习惯于bytesstr的更改。 - ismail
显示剩余9条评论

33
你要求提供链接或其他来源,但实际上并没有。信息分散在许多不同的地方。什么才真正构成了设计错误?您会将语言定义中的句法和语义问题以及平台和标准库问题和特定实现问题纳入其中吗?从性能角度来看,可以说Python的动态性是一种设计错误,因为它使得编写简单高效的实现变得困难,同时也使得编写带有代码完成、重构和其他好用功能的IDE(我并没有说完全不可能)变得困难。与此同时,您也可以为动态语言的优点进行辩护。
也许开始思考这个问题的一个方法是看看Python 2.x到3.x的语言变化。当然,有些人会认为 print 作为一个函数是不方便的,而另一些人则认为这是一种改进。总的来说,变化并不是很多,大部分变化都非常小而微妙。例如, map()filter()返回迭代器而不是列表,range()的行为类似于以前的xrange()dict方法如 dict.keys()返回视图而不是列表。然后,还有一些与整数相关的更改,而最大的变化之一是二进制/字符串数据处理。现在它是文本和数据,而文本始终是Unicode。有几个句法更改,但它们更多的是关于一致性而不是重新设计整个语言。

从这个角度来看,自2.x版本以来,Python在语言(语法和语义)层面上已经被设计得非常好了。你可以对基于缩进的块语法进行争论,但我们都知道那样做没有任何意义... ;-)

另一种方法是看看其他Python实现尝试解决什么问题。它们中的大多数以某种方式解决性能问题,有些解决平台问题,有些通过对语言本身进行添加或更改来更有效地解决某些任务。Unladen swallow 通过优化运行时字节编译和执行阶段来显著加快Python的速度。Stackless 添加了用于高效、大量线程应用程序的功能,如微线程和任务、通道以允许双向任务通信、调度以协同或抢占地运行任务以及序列化以暂停和恢复任务执行。Jython 允许在Java平台上使用Python,而IronPython 则在.Net平台上使用。Cython 是一种允许调用C函数和声明C类型的Python方言,使编译器能够从Cython代码生成高效的C代码。Shed Skin 将隐式静态类型引入Python,并为独立程序或扩展模块生成C++。PyPy 在Python的子集中实现了Python,并更改了一些实现细节,例如添加垃圾回收而不是引用计数。其目的是通过更高级别的语言使Python语言和实现开发变得更加高效。Py V8 通过V8 JavaScript引擎桥接Python和JavaScript,可以说它正在解决平台问题。Psyco 是一种特殊的JIT,动态生成正在处理的数据的运行代码的特殊版本,这可以为您的Python代码提供加速,而无需编写优化的C模块。

PEP-3146可以了解到Python的现状,该文档概述了Unladen Swallow如何被合并到CPython中。这个PEP已经被接受,因此Python开发人员认为这是目前最可行的方向。请注意,它解决的是性能问题,而不是语言本身。

所以我认为Python主要的设计 问题 在于性能领域 - 但这些基本上是任何动态语言都必须面对的挑战,Python语言和实现家族正在努力解决这些问题。至于像Javascript: the good parts中列出的明显设计 错误,我认为“错误”的含义需要更明确地定义,但您可能想查看以下内容以获取思考和意见:


23

我对Python最大的不满是标准库中缺乏适当的命名规范,而这在迁移到3.x版本时并没有得到真正的解决。

例如,为什么datetime模块本身包含一个名为datetime的类?(更不用说为什么我们有单独的datetimetime模块,但也有一个datetime.time类!)为什么datetime.datetime是小写字母,但decimal.Decimal是大写字母呢?另外,请告诉我,在xml命名空间下为什么存在那么混乱的情况:xml.sax,而xml.etree.ElementTree却不同-这是怎么回事?


2
我理解你的观点,但这与语言设计关系不大。 - Andrea
1
标准库存在命名错误,我希望一贯遵循PEP8标准(例如类名,包括datetime应当使用CamelCase写法)。但公正地说,Python 3已经修复了部分这些问题(例如tkinter模块不再以大写字母开头等)。 - user395760
不幸的是,库/ API 往往就是这样。惯例反映了个人偏好,而且由于现有的代码库,通常无法对库进行重构。就我个人而言,我更喜欢 Smalltalk 命名惯例,这也是我编写 Python 应用程序的方式。PEP8 使人们更容易理解库,但正如您所说,在代码审查方面还可以做得更多。也许在 Python4 中会有所改进! - CyberFonic
1
另一个PEP 8违规是在方法和函数名称中:内置函数和大部分标准库不使用下划线分隔单词。例如:isinstancestartswithissubsetdeleteaclendheaders... - mernen

5

1
但是,在阅读(此或任何其他有关此主题的文本)时,需要注意以下几点:(1)提出的相当一部分抱怨与语言设计无关,(2)许多其他抱怨被大多数关心语言设计和可能的替代方案的人认为是好的或较小的恶(例如,请参见Alex Martelli的答案),(3)其余大部分是权衡取舍,不容易决定,并且两种方式都可以接受,因此最终取决于个人偏好。 - user395760
3
在我看来,“空格问题”并不是缺点,反而是一种优势。尽管我曾是一名C++程序员,在开始接触时可能会感到不安,但它现在已成为我最喜欢的语言特性之一。由于不必编写冗余的 }/fi/end/endif 语句,我节省了无数小时。 - Cerin
空格问题实际上纯粹是个人偏好的事情。我讨厌它,很多人也是... 同样也有很多人喜欢它。它既有好处又有缺点。 - RHSeeger

5

4

我个人在语言方面有一个小问题,就是关于lambdas / local functions的名称绑定:

fns = []
for i in range(10):
    fns.append(lambda: i)

for fn in fns:
    print(fn()) # !!! always 9 - not what I'd naively expect

在我的看法中,声明时查找lambda引用的名称更好。我理解它现在的工作方式的原因,但还是觉得不太方便...

你目前需要通过在函数闭包中将i绑定到一个值不会改变的新名称来解决这个问题。


还有一个 lambda i=i: i 的技巧。 - Peaker
你说得对,我没有提到那个解决方法。不过,那个方法相当丑陋,不是吗? - Tom Whittock
是的,大多数情况下,您希望使用词法作用域来捕获值,而不是变量。但捕获变量更加强大... - Peaker
这个问题在 JavaScript 中也非常普遍;在设计语言时避免它的方法是为循环(以及 if 块等)和函数调用提供一个新的词法作用域,而没有闭包的语言(例如 C)通常会这样做。不幸的是,在 JS 中使用的解决方法,例如像 Array::forEach 这样使每次迭代成为函数调用并因此给它们自己的词法作用域,对于 Python 来说不是一个选项,因为它的 lambda 函数太弱了。 - 00dani

2

这是语言的一个小问题,而不是基本错误,但是:属性覆盖。如果您覆盖属性(使用getter和setter),没有简单的方法可以获取父类的属性。


1

是的,这很奇怪,但我想这就是使用可变变量的后果。

我认为原因在于“i”指的是一个具有可变值的盒子,而“for”循环会随着时间的推移改变该值,因此稍后读取盒子值将得到唯一剩下的值。 除非将其变成没有可变变量的函数式编程语言(至少没有未经检查的可变变量),否则我不知道如何解决这个问题。

我使用的解决方法是创建一个带有默认值的新变量(Python中的默认值在定义时计算,这在其他时候很麻烦),这会导致将值复制到新盒子中:

fns = []
for i in range(10):
    fns.append(lambda j=i: j)

for fn in fns:
    print(fn()) # works

1
这似乎是对Tom Whittock答案的评论。 - Max
Danny,变异的是“名称”,而不是“值”(我认为你指的是对象)。问题在于查找名称,就像PEP 227中所述,而不是在声明时绑定到方法中。 - Tom Whittock
Max: 是啊...虽然看起来我好像不能把它移到那里 :-(@Tom: 是吗?我以为问题在于for循环变量实际上被改变了,而名称i仍然指向同一个盒子(其内容随时间而改变)。如果只是一个名称问题,lambda j=i不应该解决它吗?我现在已经阅读了PEP 227,但还没有看出你的意思... - Danny Milosavljevic
Danny,每个整数都有一个全局唯一的对象表示,它不会发生变化。例如,您可以使用1 is (4-3)进行验证。因此,该对象不会发生变异,但名称“i”已更新为引用新对象。这就是我所指的错误。将j=i分配会导致名称“j”引用在那个时间点 i 所引用的对象。j是存储在新lambda对象上的名称,并且不会被重新定义为引用其他任何内容,因此对整数对象的命名引用保持不变。 - Tom Whittock

1

2
我认为这更多是当前实现的限制,而不是语言本身设计上的错误。 - bgw
@PiPeep - 没错。例如,Jython没有GIL。 - apg

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接