Python是否类型安全?

48

根据维基百科,计算机科学家认为,如果一种语言不允许违反类型系统规则的操作或转换,则被称为“类型安全”的语言。

由于Python运行时检查确保满足类型系统规则,因此我们应该将Python视为一种类型安全的语言。

Jason Orendorff和Jim Blandy在编程Rust中也提出了同样的观点:

请注意,类型安全与语言在编译时还是运行时检查类型无关:C在编译时检查,但不是类型安全的;Python在运行时检查,并且是类型安全的。

两者分别表示静态类型检查和类型安全的概念。

这正确吗?


2
Python是一种鸭子类型的语言。如果它走起来像鸭子,声音像鸭子,看起来像鸭子——那么它就是一只鸭子。我不会说它是类型安全的——请查看此线程https://dev59.com/wm855IYBdhLWcg3wrGZY。 - Chen A.
2
是的,打字可以是静态或动态的。一种语言可以是类型安全的或非类型安全的。C是静态类型的,但它绝不是类型安全的。 - juanpa.arrivillaga
1
@Vinny:我认为鸭子类型与这个讨论无关。在我看来,Python 是类型安全的:'1' + 2。例如,Javascript 就不是类型安全的。 - Eric Duminil
1
@Vinny:我提供的Python示例没有进行自动转换:它会引发异常。你提供的示例只展示了静态类型和动态类型之间的区别,这与问题无关。 - Eric Duminil
@EricDuminil 懂了。你是对的,我把两个搞混了。谢谢。 - Chen A.
显示剩余4条评论
8个回答

71
许多程序员会将静态类型检查等同于类型安全:
- "语言A具有静态类型检查,因此它是类型安全的" - "语言B具有动态类型检查,因此它不是类型安全的"
可悲的是,事情并不是那么简单。
在现实世界中,
例如,C和C++不是类型安全的,因为您可以通过Type punning破坏类型系统。 此外,C/C++语言规范广泛允许undefined behaviour (UB)而不是明确处理错误,这已成为安全漏洞的源头,例如stack smashing攻击和format string attack。在类型安全的语言中不应该存在这样的漏洞。早期版本的Java存在一个类型错误,涉及其Generics,证明它并非完全类型安全。
到目前为止,对于像Python、Java、C++等编程语言来说,要证明这些语言是完全类型安全的仍然很困难,因为这需要进行数学证明。这些语言非常复杂,并且编译器/解释器经常会出现报告和修复的错误。

[维基百科] 另一方面,许多语言对于人工生成的类型安全证明来说过于庞大,因为它们通常需要检查成千上万种情况。...由于实现中的错误或其他语言编写的链接库中的错误,某些错误可能会在运行时发生;这些错误可能导致特定情况下给定实现的类型不安全。

在学术界

虽然类型安全和类型系统适用于现实世界的编程,但它们的根源和定义来自于学术界 - 因此,在谈论实际使用的真实编程语言时,“什么是”类型安全的正式定义很难确定。学者们喜欢对称建立小型编程语言,这些语言被称为 玩具语言。只有对于这些语言才能够正式地展示它们是类型安全的(并且证明它们的操作在逻辑上是正确的)。

[维基百科] 类型安全通常是学术编程语言研究中所提出的任何玩具语言 的要求。

例如,学者们努力证明 Java 是类型安全的,因此他们创建了一个较小的版本称为 Featherweight Java,并在一篇论文中证明它是类型安全的。同样,Christopher Lyon Anderson 的博士论文选取了 JavaScript 的子集,称其为 JS0 并证明了它是类型安全的。

事实上,像 Python、Java、C++ 这样的正式语言并不完全是类型安全的,因为它们太庞大了。一个微小的错误很容易发生,这会破坏类型系统。

总结

  • ,Python 可能不是 完全类型安全的 - 没有人能够证明它是完全类型安全的,这太难了。你更有可能发现语言中的一个微小错误,证明它不是类型安全的。
  • 实际上,大多数编程语言 可能不是 完全类型安全的 - 这是由于相同的原因(只有玩具学术语言被证明是)。
  • 你真的不应该相信 静态类型的语言 一定是 类型安全的。它们通常比动态类型的语言更加安全,但是说它们是完全类型安全的是错误的,因为没有证据支持这一点。

参考文献: http://www.pl-enthusiast.net/2014/08/05/type-safety/https://en.wikipedia.org/wiki/Type_system


3
几乎所有动态类型语言在访问变量内容之前通过动态类型检查来确保类型安全。因此,它们是类型安全的。您引用的任何内容都没有解释为什么静态类型语言通常更安全。我不知道有任何流行的不安全的动态类型语言,但至少有两种静态类型语言(C / C ++)是不安全的。 - CoronA
1
你在总结中的最后一句话是完全错误的。类型安全与静态或动态类型无关。如果使用 ctypes,Python 就不是类型安全的。 - Daniel
1
“通过类型转换来破坏类型系统。”这会如何破坏类型系统? - curiousguy
有趣。前几天我发现了Pony(https://www.ponylang.io),它声称拥有类型安全的数学证明,但我不明白为什么这很重要。他们提供的证明:https://www.ponylang.io/media/papers/fast-cheap-with-proof.pdf - Jerry Jeremiah
1
总结简单来说,是误导性的(可能是故意的),并且完全错误。当一个糟糕的程序员(没有适当的工程知识/正式教育和经验)制作翻转发票的廉价即兴创作时,没有什么是安全的。除此之外,编译器是你的朋友。编译器所做的是神圣的,并且节省了宝贵的时间来进行适当的单元和覆盖测试。如果没有这些,产品就无法得到适当的维护,隐含地与某些语言的进入壁垒一样没有价值(零)。我确实喜欢Python,不要误解我的话! - Dan Marinescu
显示剩余4条评论

21

在你最疯狂的梦里也不可能。

#!/usr/bin/python

counter = 100          # An integer assignment
miles   = 1000.0       # A floating point
name    = "John"       # A string

print counter
print miles
print name

counter = "Mary had a little lamb"

print counter
当你运行时,你会看到:
python p1.py
100
1000.0
John
Mary had a little lamb

如果一个编程语言可以轻松地将变量的内容从整数转换为字符串,那么无论如何你都不能认为它是“类型安全”的。

在专业软件开发的实际世界中,“类型安全”是指编译器会捕捉到愚蠢的错误。是的,在C/C++中,你可以采取非常规措施来规避类型安全性。你可以声明像这样的内容:

union BAD_UNION
{
   long number;
   char str[4];
} data;

但是程序员必须付出额外的努力才能做到这一点。我们不需要在Python中走额外的英寸来扭曲计数器变量。

C/C++中的程序员可以通过转换做一些讨厌的事情,但他们必须有意地这样做,而不是无意间。

最容易让你犯错的地方是类转换。当您声明一个带有基类参数的函数/方法,然后传递指向派生类的指针时,您并不总是得到所需的方法和变量,因为方法/函数期望基本类型。如果在您的派生类中覆盖了任何内容,则必须在该方法/函数中加以考虑。

在现实世界中,“类型安全”的语言有助于保护程序员免于无意中做出愚蠢的事情。它还保护人类免受致命伤害。

考虑胰岛素泵或输液泵。它们会将限量的延长生命的化学物质按照所需的速率/间隔注入人体。

现在考虑一下,当存在一个逻辑路径时,泵步进控制逻辑试图将字符串“insulin”解释为要管理的整数量时会发生什么。结果将不会是好的。最有可能是致命的。


10
因为还没有人说过,所以值得指出的是Python是一种强类型语言,不应与动态类型混淆。Python将类型检查推迟到最后可能的时刻,并通常会导致抛出异常。这解释了Mureinik提到的行为。话虽如此,Python也经常进行自动转换。这意味着它将尝试将int转换为float进行算术运算,例如。
您可以通过检查输入的类型来手动强制执行程序中的类型安全。由于所有内容都是对象,因此您始终可以创建从基类派生的类,并使用isinstance函数验证类型(当然是在运行时)。Python 3添加了类型提示,但这并不强制执行。如果您想使用它,mypy已将静态类型检查添加到语言中,但这并不保证类型安全。

将其推迟到最后一刻意味着: - Dan Marinescu
  1. 大多数情况下,最后时刻是由客户经历的(在已部署的产品中,出现了特殊情况/问题)。
- Dan Marinescu
使用动态元数据查询(isinstance)明确进行单元测试无法覆盖任何用户定义的数据类型,这些数据类型在语法上部分符合某个执行上下文,原因在于:由于不能在测试用例和调用isinstance实际发生时使用不存在的数据类型。你会想:“什么?!”好吧,试想一下:今天你手动添加了所有类型的isinstance检查到成千上万的测试用例中。但是,如果(将来)有人添加新的符合要求的数据类型而不调整所有测试用例,则它们将无效。 - Dan Marinescu
mypy 不是Python的核心功能。 - Dan Marinescu
应该有一个选项来强制执行提示,但像往常一样,“仁慈的独裁者”(未退休)并没有这么做。这也解释了Python 2与Python 3的彻底失败。 - Dan Marinescu
“最后一刻”有时被社区称为“鸭子类型”,通常被认为是一种特性。与通过类型强制执行类型不同,当在运行时尝试调用未实现它的类型的方法或运算符时,会抛出异常。这样,接口(请参见abc模块)不需要在编写程序之前设计和实现,并且可以扩展或“猴子补丁”遗留代码,以添加仅所需的功能,使数据适合算法。 - VoNWooDSoN

8
维基百科的文章将类型安全与内存安全联系在一起,这意味着不能将同一内存区域作为整数和字符串等多种类型进行访问。因此,Python是类型安全的。您无法隐式地更改对象的类型。

但是你可以明确地更改它,在同一范围内,这让我想起了快速基本语言 :-) - Dan Marinescu

6
在Python中,如果您在错误的上下文中使用了错误类型的变量,则会出现运行时错误。例如:
>>> 'a' + 1

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: cannot concatenate 'str' and 'int' objects

由于这种检查只发生在运行时而不是在运行程序之前,因此Python不是一种类型安全的语言(尽管PEP-484除外)。


3
这仍然是定义良好的行为,不违反类型系统规则,是吗? - user8664060
7
据我所知,似乎没有要求类型检查必须在运行时发生。事实上,正如维基百科文章所述:“类型强制可以是静态的,在编译时捕获潜在错误,也可以是动态的,在运行时将类型信息与值关联起来,并根据需要查询它们以检测即将发生的错误,或者两者兼而有之。” - juanpa.arrivillaga
Python不需要编译。 - whackamadoodle3000
@whackamadoodle3000:所有广泛使用的Python实现都是编译器。 - Daniel
1
而“发生在运行时”的最好例子是简单的:不是你或QA会捕捉到它,而是你的客户。很可爱吧? - Dan Marinescu
显示剩余2条评论

0
假设您有一个名为sum的函数,它接受两个参数。如果参数是未类型化的(可以是任何东西),那么...嗯...这对于任何在实际大型系统上工作的严肃软件工程师来说都是不可接受的。原因如下:
  1. 天真的答案可能是“编译器是你的朋友”。尽管已经有65年历史了,但这是真实的。嘿,这不仅仅是关于静态类型!IDE(集成开发环境)使用编译器服务来完成许多事情,对于普通程序员来说,这些看起来像魔法...(代码自动补全、设计时间(编辑)辅助等)
  2. 更现实的原因在于,这对于没有扎实计算机科学和软件工程背景的开发人员来说完全是未知的。可扩展性有三个方面:a. 设计/编写和部署,b. 运行时和c. 维护时间,基于重构。你认为哪一个最昂贵?在任何真实的严肃系统中都会明显地反复出现的是第三个(c)。为了满足(c),你需要做到安全。为了进行任何安全的重构,你需要进行单元测试和覆盖测试(这样你就可以估计你的单元测试套件覆盖的级别)-记住,当某些东西没有自动测试时,它将会在运行时(周期后期,在客户现场等)出现问题-因此,为了拥有一个体面的产品,你需要有体面的单元测试和测试覆盖率。

现在,让我们来谈谈我们的智力挑战函数(sum)。如果sum(a,b)没有指定a和b的类型,则无法进行良好的单元测试。像断言sum(1,1)是2这样的测试是错误的,因为它只涵盖了假定的整数参数。在现实生活中,当a和b是类型雌雄同体时,就没有办法针对函数sum编写真正的单元测试!甚至有些框架假装从上述残缺的测试用例中推导出测试覆盖结果。这是另一个谎言。

这就是我要说的全部内容!感谢您的阅读,我发布这篇文章的唯一原因可能是让您思考这个问题,也许(也许……)有一天会从事软件工程……


0

一般来说,大规模/复杂系统需要类型检查,首先是在编译时(静态)和运行时(动态)。这不是学术界的问题,而是一个简单、常识性的经验法则,就像“编译器是你的朋友”一样。除了运行时性能影响之外,还有其他重要的影响,如下所示:

可扩展性的三个方面是:

  1. 构建时间(在时间和预算内设计和制造安全系统的能力)
  2. 运行时(显而易见)
  3. 维护时间(以安全的方式维护(修复错误)和扩展现有系统,通常通过重构实现)

唯一安全的重构方法是对所有内容进行充分测试(使用测试驱动开发或至少单元测试和至少良好的覆盖率测试,这不是质量保证,而是开发/研发)。未覆盖的部分将会出错,这样的系统更像垃圾而不是工程艺术品。

现在假设我们有一个简单的函数sum,返回两个数字的和。可以想象对这个函数进行单元测试,基于已知的参数和返回类型。我们不是在谈论函数模板,它们归结为简单的例子。请编写一个简单的单元测试,测试同样名为sum的函数,其中参数和返回类型可以是任何类型,包括整数、浮点数、字符串和/或任何其他用户定义类型,只要这些类型重载/实现了加号运算符即可。你如何编写这样一个简单的测试用例?!?为了涵盖每种可能的情况,测试用例需要多复杂?
复杂性意味着成本。没有适当的单元测试和测试覆盖率,没有安全的方法进行任何重构,因此产品会变得难以维护,虽然这不会立即显现,但长期来看会很明显,因为在盲目进行任何重构就像酒后驾车且没有保险一样危险。
自己琢磨吧! :-)

0
我们的代码出现了一个大错误。这个错误是因为我们有这个东西:
   if sys.errno:
        my_favorite_files.append(sys.errno)

应该使用这个:

    if args.errno:
        my_favorite_files.append(sys.errno)

将任何东西强制转换为布尔值,因为它使if语句更容易的做法是我不希望在一种类型安全的语言中找到的。


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