是否需要一个“使用严格模式”的Python编译器?

32

虽然存在Python的静态分析工具,但编译时检查往往与Python所采用的运行时绑定哲学截然相反。尽管可以使用静态分析工具来强制执行类似于"use strict"的约束,但我们并没有看到广泛采用这种工具。

Python是否存在使得“use strict”行为不必要或特别不可取的因素?

另外,即使Perl被广泛采用,“use strict”行为是否也是不必要的?

注:在此,“必要”指的是“实际上必要”,而不是严格必要。显然你可以不使用“use strict”来编写Perl代码,但(根据我所见)大多数Perl程序员都会使用它。

注:Python解释器包装器不需要要求类似于“use strict”的约束-您可以使用类似于“use strict”的伪指令,该伪指令将被普通解释器忽略。我不是说要添加语言级功能。


更新:根据评论,解释了Perl中“use strict”的作用。(官方文档链接在第一段中。)

“use strict”指令有三个不同的组件,但只有其中两个是真正有趣的:

  • use strict vars: 静态地检查程序中词法范围变量的使用情况。(请记住,在Python中,基本上只有全局和本地范围。)许多Python语言检查工具都会检查此类问题。因为这是唯一可以进行静态分析的内容,所以语言检查工具假定您使用直观的词法范围,并在这方面警告看起来有问题的事情,直到您停止警告为止;

    FOO = 12
    foo += 3
    

    如果你的命名空间没有做任何特殊处理,这个技巧可以用来检查拼写错误。

  • use strict refs: 防止符号命名空间解引用。在Python中最接近的类比是使用 locals()globals() 进行符号绑定和标识符查找。

  • use strict subs: Python中没有真正的类比。


请解释一下在Perl中"use strict"的作用以及为什么需要它?为什么许多人总是使用它?如果没有它会发生什么? - hasen
请看我的回答,了解"use strict"实际上是做什么的。这篇文章和评论中似乎有些混淆它的真正含义。是的,Perl程序员喜欢使用"use strict",但这并不会让Perl更像Java。 - mpeters
当然,Python 3通过non-local声明使事情变得更加复杂(或者说更加清晰)- 就像http://www.python.org/dev/peps/pep-3104/中所述。 - popcnt
我认为这并没有使事情变得太复杂。创建新作用域的结构是相同的,你只需要显式地访问一个中间本地作用域。现在你有三个可直接访问的命名空间,而不是两个。 - cdleary
以下是两篇支持强制变量声明(即使用 use strict 'vars')的好文章(在我看来),一篇来自 Perlmonk,另一篇来自 Pythonic 视角:http://www.perlmonks.org/?node_id=755790 http://mail.python.org/pipermail/python-3000/2006-October/003968.html - Oktalist
10个回答

37

嗯,我不是很擅长Python编程,但我想答案是“是的”。

任何允许您随时使用任何名称创建变量的动态语言都可以使用“strict”指令。

在Perl中,Strict vars(Perl中严格选项之一,“use strict”同时打开所有选项)要求在使用变量之前声明所有变量。这意味着此代码:

my $strict_is_good = 'foo';
$strict_iS_good .= 'COMPILE TIME FATAL ERROR';

生成一个编译时致命错误。
我不知道有什么办法可以让Python在编译时拒绝此代码:
strict_is_good = 'foo';
strict_iS_good += 'RUN TIME FATAL ERROR';

您会收到一个运行时异常,表示“strict_iS_good”未定义。但只有在执行代码时才会出现此问题。如果测试套件没有100%的覆盖率,您可以轻松地发布此错误。
每当我使用不具有此行为的语言时(例如PHP),我都感到紧张。我不是一个完美的打字员。一个简单但难以发现的错别字可能会导致您的代码以难以追踪的方式失败。
因此,重申一下,是的,Python可以使用“严格”编译指示来打开可以在编译时检查的内容的编译时检查。我想不出需要添加的任何其他检查,但是更好的Python程序员可能会想到一些。
注意:我专注于Perl中严格变量的实用效果,并忽略了一些细节。如果您真的想了解所有详细信息,请参阅strict的perldoc
更新:对一些评论的回应
Jason Baker:像pylint这样的静态检查器很有用。但是它们代表了可能被跳过的额外步骤。在编译器中构建一些基本检查可确保这些检查始终得到执行。如果这些检查可以通过编译指示进行控制,那么即使与检查相关的成本方面的反对也将失去意义。 popcnt:我知道Python会生成运行时异常。我已经这么说了。我主张尽可能进行编译时检查。请重新阅读文章。

mpeters:对代码进行的计算机分析无法找出所有错误,这等同于解决停机问题。更糟糕的是,为了在作业中找到拼写错误,编译器需要知道你的意图,并找到你的意图与代码不符的地方。这显然是不可能的。

然而,这并不意味着不应该进行任何检查。如果有易于检测的问题类别,则有意义去捕获它们。

我不太熟悉pylint和pychecker可以发现哪些类型的错误。正如我所说,我在Python方面非常没有经验。

这些静态分析程序很有用。但是,我认为,除非它们复制了编译器的功能,否则编译器始终将处于比任何静态检查器更了解程序的位置。利用这一点尽可能减少错误似乎是浪费的。

更新2:

cdleary - 理论上,我同意您的观点,静态分析器可以执行编译器能够执行的任何验证。在Python的情况下,这应该足够了。

然而,如果您的编译器足够复杂(特别是如果您有许多改变编译方式的预处理指令,或者像Perl一样,可以在编译时运行代码),则静态分析器必须接近编译器/解释器的复杂性才能进行分析。

哎呀,所有这些关于复杂编译器和在编译时运行代码的讨论都显示了我的Perl背景。

我理解Python没有编译指示,并且不能在编译时运行任意代码。因此,除非我错了或这些功能被添加,否则静态分析器中相对简单的解析器就足够了。强制在每次执行时进行这些检查肯定会有所帮助。当然,我会使用编译指示来实现这一点。
一旦你将编译指示加入到混合中,你就开始走下一个滑坡,你的分析器的复杂性必须与你提供的编译指示的能力和灵活性成比例地增长。如果不小心,你可能会像Perl一样,然后“只有Python可以解析Python”,这是我不想看到的未来。
也许命令行开关是添加强制静态分析的更好方式;)
(当我说Python不能像Perl那样干扰编译时行为时,我绝不打算贬低Python的能力。我有一种预感,这是一个经过深思熟虑的设计决策,我可以看出其中的智慧。Perl在编译时的极大灵活性,在我看来,既是一种巨大的优势,也是一种可怕的语言缺陷;我也看到了这种方法的智慧。)

2
Python确实会“拒绝”代码,用你的术语来说,但是在运行时。一个好的编辑器可以帮助避免这样的错误,在Perl和Python中都是如此——这就是要点->避免在源代码中包含意外的错误。 - popcnt
1
但这并不能捕捉到你的作业中的拼写错误。如果你在代码中稍后更改变量的值,但是你打错了名字,解释器在运行时不会捕捉到它。那么 pylint 或 pychecker 能否捕捉到它呢? - mpeters
1
Python可以在运行时编译/运行任意代码。请查看eval (http://docs.python.org/library/functions.html#eval)。Python具有pragma。请参阅__future__ bitflags (http://docs.python.org/library/__future__.html?highlight=__future__#module-__future__)。 - cdleary
1
Evals使得静态分析变得不可能。如果我们使用eval,通常不会关心静态检查 - 大多数静态检查器在看到eval时会放弃。我只谈论OP中可选的、按文件、编译时、基于作用域的语法检查。这是PyLint/PyFlakes所做的子集。 - cdleary
1
那个例子是有效的Python代码。如果该例子在函数内部,则strict_iS_good是一个全局变量,静态分析无法知道该名称在代码执行时不会在全局范围内。 - Peter Graham
显示剩余6条评论

11

Python确实有些东西可以改变脚本语法:

from __future__ import print_function

Python的语法比历史悠久的Perl更加严格、稳定和定义明确;未声明引用和未声明子程序等情况被“strict refs”和“strict subs”禁止,这些在Python中从未存在。

“strict vars”主要防止拼写错误的引用和缺少'my'关键字导致意外全局变量的产生(用Perl术语来说就是包变量)。在Python中,裸赋值默认为本地声明,而裸符号则会引发异常,因此不可能发生这种情况。

(仍有一种情况是用户无意中尝试向未声明为全局变量的变量进行写入操作,从而导致意外的局部变量或更常见的UnboundLocalError异常。这往往很快就能学会,但这是一个争议性的问题,即是否需要声明本地变量以减少这类错误。虽然很少有经验丰富的Python程序员会接受这种可读性负担。)

其他与语法无关的语言和库变化通过warnings系统处理。


我喜欢这个答案的方向。我认为使用globals()[]或locals()[]类似于strict refs,尽管我同意strict subs。不过,在拼写错误方面似乎需要进行检查——为什么人们没有使用静态分析工具来检查他们的拼写错误呢? - cdleary

9
“Python所采用的运行时绑定哲学使得‘使用严格模式’的行为是不必要的,尤其是不可取的。”
“很好的总结。谢谢。”
“基本上就是这样。静态分析工具对Python的帮助不够值得。”

编辑

“我希望我们能反思一下为什么我们不需要它,以及与此相关的,为什么Perl程序员认为他们需要它。”

之所以不需要它,正是你已经给出的原因。显然,你不喜欢这个答案,但没有更多可说的了。编译时或预编译时检查根本没有帮助。

然而,既然你花时间再次提问,我会提供更多证据来支持你已经给出的答案。

我写Java的时间几乎和我写Python的时间一样长。 Java的静态类型检查不能防止任何逻辑问题;它不利于满足性能要求;它也无法帮助满足使用案例。 它甚至不能减少单元测试的数量。

虽然静态类型检查确实可以发现偶尔的方法误用,但在Python中你同样可以很快地发现这一点。在Python中,你会在单元测试时发现它,因为它无法运行。注意:我并不是说通过大量聪明的单元测试可以找到错误的类型,我是说大多数错误的类型问题都是通过未处理的异常发现的,因为它无法运行到测试断言。

Pythonista们不浪费时间进行静态检查的原因很简单。我们不需要它。它没有任何价值。这是一种没有经济效益的分析水平。它不能使我更能够解决真实人们在处理他们的真实数据时遇到的实际问题。
看看最流行的与Python语言相关的SO Python问题。
"foo is None"和"foo == None"之间有什么区别吗?-- == vs. is。没有任何静态检查可以帮助解决这个问题。此外,请参见Is there a difference between `==` and `is` in Python?

双星号(**)和星号(*)在参数中有什么作用? -- *x 表示列表,**x表示字典。如果您不知道这一点,当您尝试对这些类型进行不合适的操作时,程序会立即崩溃。"如果您的程序从未执行任何“不合适”的操作怎么办?" 那么您的程序就可以正常工作了。没什么好说的。

如何在Python中表示'Enum'(枚举) -- 这是对某种有限域类型的请求。一个具有类级别值的类几乎可以完成这项工作。 "如果有人更改了赋值怎么办"。很容易构建。覆盖__set__以引发异常。是的,静态检查可能会发现这一点。不,实际上不会发生有关枚举常量和变量的混淆;当他们这样做时,在运行时很容易发现。 "如果逻辑从未执行怎么办"。那就是糟糕的设计和糟糕的单元测试。抛出编译器错误并放入从未测试过的错误逻辑与在动态语言中从未测试时发生的情况没有什么不同。

生成器表达式 vs. 列表推导式 -- 静态检查无法解决这个问题。

为什么 1+++2 = 3? -- 静态检查无法发现这个问题。在 C 中,1+++2 是完全合法的,尽管经过所有编译器的检查。在 Python 中,它与 C 中的不同,但同样合法。同样令人困惑。

子列表中的列表更改会意外地反映在其他子列表中 -- 这完全是一个概念性的问题。静态检查也无法帮助解决此问题。Java 的等效代码也会编译并表现糟糕。


9
你的回答基本上是:“这并不必要。” 我理解很多Python代码在没有使用它的情况下也能够成功编写 - 我想让我们反思一下为什么我们不需要它,以及与此相关的,为什么Perl程序员认为他们需要它。 - cdleary
6
大多数编写“脚本”(严格意义上)的人不编写单元测试,通常是因为有很多具有副作用的I/O。单元测试对于应用程序/框架编程是有意义的,但对于脚本编写场景,使用编译器标志检查标识符似乎是有用的,不是吗? - cdleary
9
@S.Lott: 没错,但是你不希望在执行上述脚本的过程中因为一个可以在编译时检测到的NameError而中途失败,对吧?是否有用通过一个解释器包装器,在编译时可选择运行此分析? - cdleary
6
任何声称测试和脚本编写是对立的人都是在拒绝测试。他们需要开始一份远离软件的不同职业。 - S.Lott
14
这只是一场怒骂,且客观上是错误的。其他具有类似哲学(如Perl、JavaScript)的语言从“use strict”中获得了明显的好处,以至于它已经成为一个既定的最佳实践,始终使用它。Python也将受益匪浅。 - Konrad Rudolph
显示剩余13条评论

7
我认为有些评论中存在对"use strict"的混淆。它不会开启编译时类型检查(像Java一样)。在这个意义上,Perl程序员和Python程序员是一致的。正如S.Lott所说,这些检查不能防止逻辑错误,不能减少需要编写的单元测试数量,我们也不喜欢束缚式编程。
下面是"use strict"的功能列表:
  1. 使用符号引用会导致运行时错误。这可以防止您做一些疯狂的事情(但有时很有用),比如:
  2. $var = 'foo';

    $foo = 'bar';

    print $$var; # 这将包含$foo的内容,除非使用strict运行

  3. 使用未声明的变量会导致运行时错误(这意味着您需要在使用变量之前使用"my"、"our"或"local"来声明变量的作用域)。
  4. 所有裸字都被视为编译时语法错误。裸字是指未声明为符号或子例程的单词。这主要是为了禁止历史上曾经做过但现在被认为是错误的事情。

不要减少你需要编写的单元测试数量:除非你认为为了检查微不足道的拼写错误而单元测试每个catch-and-rethrow-with-a-better-message块是程序员时间的巨大浪费。根据我见过的大多数Pythonista编写的单元测试来看,这并不是一个普遍的观点。 - Alice Purcell

5

这个原始答案是正确的,但在实际情况下可能没有解释清楚。

Python存在静态分析工具,但编译时检查往往与Python所采用的运行时绑定哲学截然相反。

在Perl中,“use strict”提供了确保拼写错误或变量名(通常)在编译时被捕获的能力。这确实提高了代码可靠性并加快了开发速度。但为了使这样的事情值得做,您需要声明变量。而Python风格似乎不鼓励这样做。

因此,在Python中,只有在您注意到未进行分配的赋值或表达式似乎解析为意外值时,才会了解有关拼写错误的变量的信息。捕获此类错误可能会耗费时间,特别是当程序变得庞大时,并且人们被迫维护由他人开发的代码时。

Java和C/C++更进一步,具有类型检查功能。动机是实用而非哲学上的。如何尽可能快地捕获尽可能多的错误,并确保在发布代码到生产之前消除所有错误?每种语言似乎都采取了特定的策略,并根据他们认为重要的内容进行了运行。在像Perl这样不支持运行时绑定的语言中,利用“use strict”使开发变得更容易是有意义的。


在Python中,这要比Java少受很多伤害。例如,在LongNamedFunctionNeededInJava中打字错误时,您不会轻易地看到它。如果您在从假设的winter模块(from winter import user_name)导入的user_name中出现拼写错误,您几乎可以立即发现它。并且它将在加载时可见,因为那通常是您导入东西的地方。归结为使用命名空间:它们允许保持代码可读性。 - Arne Babenhauserheide

4

Python没有真正的词法作用域,因此严格变量并不是很明智。据我所知,它没有符号引用,因此不需要严格引用。它没有裸字,因此不需要严格变量。

说实话,我只是想念词法作用域。其他两个我认为是Perl中的瑕疵。


双下划线属性(obj.__attr)不被视为词法作用域吗? - popcnt
2
属性与词法作用域关系不大。 - Leon Timmermans
你是什么意思?Python 明显具有词法作用域(至少自 2.1 版本以来),但我不认为它与需要严格声明有太大关系。你指的是其他什么吗? - Brian
3
是的,Python 是具有词法作用域的。我认为这指的是只有在特定结构中才会创建新的词法作用域:模块、类和函数。例如,for 循环没有独立的封闭作用域。如果你在 for 循环中绑定了一个标识符,它会一直保持绑定状态,直到超出循环范围。 - cdleary
2
@cdleary:是的,那就是我想说的意思。 - Leon Timmermans
显示剩余2条评论

3

我认为Perl中的'use strict'更像是一个编译器指令,正如你所暗示的:它改变了编译器的行为。

Perl语言的哲学与Python不同。在Perl中,你被赋予了足够的绳子,可以反复地上吊自己。

Larry Wall非常注重语言学,因此我们从Perl中得到了所谓的TIMTOWTDI(发音为tim-toe-dee)原则,而Python则有Zen of Python:

应该有一种——最好只有一种——明显的方法来做到这一点。

你可以很容易地使用pylint和PyChecker为Python创建自己的use strict风格(或类似于perl -cw *scriptname*的东西),但由于语言设计中的不同哲学,你不会在实践中广泛遇到这种情况。

根据你对第一篇文章作者的评论,你熟悉Python的import this。里面有很多东西可以阐明为什么你在Python中看不到use strict的等效物。如果你沉思Python之禅中的公案,你可能会为自己找到启示。 :)


3
希望不要看到只是告诉我“import this”的答案,而没有给出真正的解释。一个检查标识符作用域的编译器如何违反Python哲学主题?运行时查找如何使静态词法分析足够无用?我们知道这些哲学不同。 - cdleary
如果你使用的是不太先进的编辑器,无法帮助你发现命名不当的变量(在Perl或Python中等),或者无法在一开始就防止它们,那么静态词汇分析可能会有积极的好处。Pylint和PyChecker似乎可以充分覆盖这个领域? - popcnt
是的,我的问题是,如果您给编译器一个指令,是否需要自动调用这种静态分析,为什么/为什么不需要。另外,您提到的是哪些编辑器?无论使用哪个编辑器,您肯定不会自动完成每个变量名称。 - cdleary
@cdleary:emacs - 是的,我对每个变量都使用自动完成(当然第一次除外),我喜欢它! - popcnt
@popcnt:有趣的是,我觉得这实际上会减慢我的打字速度,因为每次都要验证和/或选择自动完成。这会让你变慢吗? - cdleary

1

我发现我只关心检测到未声明变量的引用。Eclipse 通过 PyDev 集成了 pylint,虽然 pylint 远非完美,但在这方面做得相当不错。

它有点违背 Python 的动态特性,当我的代码变得聪明时,我偶尔需要添加 #IGNORE。但我发现这种情况并不经常发生,所以我很满意。

但我可以看到一些类似 pylint 的功能以命令行标志的形式变得可用的实用性。就像 Python 2.6 的 -3 开关一样,它可以识别 Python 2.x 和 3.x 代码之间的不兼容点。


1
是的,这正是我也在考虑的方向——用一个额外的标志来包装常规解释器,以打开“使用严格模式”编译。我从未见过类似的东西,但Perl思想家倾向于认为它是必要的/可取的。Python思想家则不太关心。为什么我们不关心呢? - cdleary

0
在Perl中,如果没有使用'use strict',编写大型程序会非常困难。如果你再次使用一个变量,并且因为拼写错误而遗漏了一个字母,程序仍然可以运行。如果没有测试用例来检查结果,你永远无法发现这样的错误。由于这个原因,找出为什么会得到错误的结果可能非常耗时。
我的一些Perl程序由5000到10000行代码组成,分成几十个模块。没有'use strict',就不能真正进行生产编程。我绝不会允许使用不强制“变量声明”的语言在工厂安装生产代码。
这就是为什么Perl 5.12.x现在将'use strict'作为默认行为的原因。你可以关闭它们。
PHP由于没有变量声明强制执行,给我带来了很多问题。所以你需要限制自己在这种语言中编写小程序。
这只是一个观点...
abcParsing

1
这就是为什么Perl 5.12.x现在将'use strict'作为默认行为。你可以将它们关闭。咦? - Evan Carroll

-1

Perl是一种自由的语言,正如他们所说的:)。因此,在声明之前可以使用变量;例如:如果您使用一个变量名“is_array”,但输入“is_arrby”,则编译器不会报告错误,而没有“use strict”。因此,在编写长程序时,最好使用“use strict”语句。当然,对于运行一次脚本少于50行的情况,没有必要使用。


1
我有点困惑。我们是在谈论Python还是Perl? - ccjmne
@ccjmne,哈哈,是的。我认为如果没有语法错误,Perl可以让你自由地构建程序;在Python中,有一点限制,但已经足够了... :) - BinDu

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