来自翻译布兰登·罗德斯(Brandon Rhodes)是一个非常谦虚的人,他在Twitter上表现为“以报告或散文形式向社区偿还贷款的Python程序员”。 这些“报告和论文”的数量令人印象深刻,布兰登曾经或曾经贡献的免费项目也是如此。 布兰登出版了两本书,正在写第三本书。
我经常在对Habré的评论中发现对动态语言,动态类型,广义编程和其他范例的基本误解或拒绝。 我正在发布Brandon的一份报告的授权(缩写)翻译(抄本),以期能帮助静态语言范例中的程序员更好地理解动态语言,尤其是Python。
按照我的惯例,我要求您在PM中通知我我的错误和错别字。
我的报告标题中的“边际案例”是什么意思? 当您遍历一系列选项直到达到极致值时,就会出现极限情况。 例如,一个n面多边形。 如果n = 3,则为三角形,n = 4为四边形,n = 5为五边形,依此类推。当n接近无穷大时,边变得越来越小,多边形的轮廓变得像圆形。 因此,圆是正多边形的极限情况。 当某个想法达到极限时,就会发生这种情况。
我想谈谈Python是C ++的极端情况。 如果您将C ++中的所有好主意都整理一遍,以得出合乎逻辑的结论,那么我相信您会自然而然地使用Python,因为一系列多边形会变成一个圆。
非核心资产
在90年代,我开始对Python感兴趣:在我生命中的这段时期,我摆脱了所谓的“非核心资产”。 许多事情开始让我感到无聊。 例如,中断。 还记得吗,一旦在许多计算机板上都存在与跳线的这种接触? 并且您在手册上设置了这些跳线,以便视频卡收到更高优先级的中断,从而您的游戏运行更快? 因此,我厌倦了在停止使用跳线调整计算机性能的同时使用malloc()
和free()
分配和释放内存。 那是1997年左右。
我的意思是,当我们研究一个过程时,我们通常会努力对其进行完全控制,以掌握所有可能的控制杆和按钮。 然后,有些人仍然对这种控制的可能性着迷。 但是我的特点是,一旦我习惯了管理层并了解了什么,我立即开始寻找机会放弃一些权力,将操纵杆和按钮转移到某些机器上,以便为我分配中断。
因此,在90年代后期,我一直在寻找一种编程语言,该语言可以使我专注于主题领域和任务建模,而不用担心数据存储在计算机内存的哪个区域。 如何在不重复著名脚本语言的罪过的情况下简化C ++?
例如,我无法使用Perl,您知道为什么吗? 这个美元符号! 他立即清楚地表明Perl的创建者不了解编程语言是如何工作的。 您使用Bash中的美元将变量名与字符串的其余部分分开,因为Bash程序由字面意义上的命令及其参数组成。 但是,当您了解了这些编程语言后,即在字符串之间将字符串放在称为引号的小字符对之间,而不是整个程序文本中,将$
视为可视垃圾。 美元符号无用,丑陋,必须走! 如果要设计用于认真编程的语言,则不应使用特殊字符来表示变量。
句法
语法呢? 以C为基础! 效果很好。 让分配用等号表示。 并非所有语言都接受此指定,但是一种或另一种方式已经习惯了许多。 但是,我们不要使赋值成为表达式。 我们语言的使用者不仅是专业的程序员,而且还是学童,科学家或数据科学家(如果您不知道这些类别的用户中写得最差的代码,那么我会暗示他们不是学童)。 我们不会给用户机会在意想不到的地方更改变量的状态,而是使赋值成为操作符。
如果等号已经用于赋值,那么应该用什么来表示相等呢? 当然,双重赋值就像在C语言中一样! 许多人已经习惯了。 我们还将从C借用所有算术和按位运算的表示法,因为这些表示法有效,并且许多人都对它们感到满意。
当然,我们可以改善一些东西。 当您在程序文本中看到百分号时,您会怎么想? 当然,关于字符串插值! 尽管%
主要是模块捕获运算符,但对于字符串它只是未定义。 如果是这样,那为什么不重用它呢?
用反斜杠控制序列的数字和字符串文字-所有这些在C语言中看起来都一样。
执行流控制? if
break
并continue
。 当然,我们可以通过选择旧数据来迭代数据结构和值范围,从而增加一些乐趣。 这将在稍后的C ++ 11中提出,但是在Python中, for
运算符最初封装了所有用于计算大小,遍历链接,递增计数器等的操作,换句话说,做了为用户提供数据结构元素所需的一切。 什么类型的结构? 没关系,只要将其传递给for
,它就会弄清楚。
我们还将从C ++中借用异常,但是就资源消耗而言,我们将使它们变得如此便宜,以至于它们不仅可用于处理错误,还可用于控制执行流程。 通过添加切片,我们将使索引更加有趣-不仅可以索引顺序数据结构的各个元素,还可以索引它们的范围。
哦,是的! 我们将修复C语言中的原始设计缺陷-添加一个悬挂的逗号!
这个故事始于Pascal,这是一种可怕的语言,其中使用分号作为表达式定界符 。 这意味着用户必须在分块中每个表达式的末尾( 最后一个除外)放置分号。 因此,每当您在Pascal中更改程序中表达式的顺序时,如果不确定确保从最后一行删除分号并将其添加到过去的最后一行,都有冒语法错误的风险。
If (n = 0) then begin writeln('N is now zero'); func := 1 end
当Kernigan和Ritchie将C中的分号定义为表达式的终止符而不是定界符时,他们做了正确的事情,当程序中的每一行(包括最后一行)以相同的结尾并可以自由互换时,就产生了奇妙的对称性。 不幸的是,将来,它们的和谐感发生了变化,并且它们使逗号成为静态初始值设定项中的分隔符 。 当表达式适合一行时,这看起来很好:
int a[] = {4, 5, 6};
但是,当您的初始值设定项变得更长并且垂直排列时,您将得到与Pascal中相同的令人不适的不对称性:
int a[] = { 4, 5, 6 };
在开发的早期阶段,Python使得数据结构中的悬挂逗号完全可选,无论该结构的元素是水平还是垂直排列。 顺便说一下,这对于代码自动生成非常方便:您无需将最后一个元素视为特殊情况。
后来,C99和C ++ 11标准也纠正了最初的误解,使您可以在初始化程序的最后一个文字之后添加逗号。
命名空间
我们还需要用编程语言实现诸如名称空间或名称空间之类的东西。 这是该语言的重要组成部分,可以使我们免于名称冲突之类的错误。 我们将比C ++更加轻松:我们将为每个模块(文件)创建一个名称空间,并使用文件名指定它们,而不是让用户能够随意命名名称空间。 例如,如果您创建模块foo.py
,则将为其分配名称空间foo
。
要使用这种简化的名称空间模型,用户仅需要一个运算符。
创建my_package
目录,在其中放置my_module.py
文件,然后在文件中声明类:
class C(object): READ = 1 WRITE = 2
然后访问类的属性将如下所示:
import my_package.my_module my_package.my_module.C.READ
不用担心,我们不会强迫用户每次打印全名。 我们将给他机会使用几个版本的import
语句来改变名称空间的“接近度”:
import my_package.my_module my_package.my_module.C.READ from my_package import my_module my_module.C.READ from my_package.my_module import C C.READ
因此,在不同软件包中给出的相同名称永远不会冲突:
import json j = json.load(file) import pickle p = pickle.load(file)
每个模块都有自己的名称空间这一事实也意味着我们不需要static
修饰符。 但是,我们回想起static
执行的一个函数-封装内部变量。 为了向同事显示给定名称(变量,类或模块)不是公共名称,我们以下划线开头,例如_ignore_this
。 这也可能是IDE不能在自动完成中使用此名称的信号。
函数重载
我们不会用我们的语言实现函数重载。 过载机制太复杂。 相反,我们将使用可选参数以及可以从调用中省略的默认值,以及将命名参数“跳过”具有有效默认值的可选参数,并仅设置与默认值不同的那些值。 重要的是,缺少重载将使我们不必从重载函数集中确定哪个函数刚刚被调用,调用管理器如何工作:该函数在该模块中始终是一个,很容易按名称查找。
系统API
我们将为用户提供对许多系统API(包括套接字)的完全访问权限。 我不明白为什么脚本语言的作者总是提供自己独特的方式来打开套接字。 但是,他们从未意识到完整的Unix Socket API。 他们实现了他们理解的5-6个功能,并抛弃了其他所有功能。 与它们不同,Python具有用于与操作系统交互的标准模块,这些模块实现了每个标准系统调用。 这意味着您可以立即打开史蒂文斯的书并开始编写代码。 并且您所有的套接字,进程和派生将完全按照其说明工作。 是的,Guido或早期的Python贡献者可能就这样做了,因为他们懒得编写系统库的实现,也懒得再次向用户解释套接字的工作方式。 但是结果是,它们取得了出色的效果:您可以将在C和C ++中获得的所有UNIX知识转移到Python环境中。
因此,我们决定了从C ++中“借用”哪些功能来创建我们的简单脚本语言。 现在我们需要决定要修复的问题。
未定义的行为
未知的行为,未定义的行为,由实现定义的行为...这些都是学童,科学家和数据科学家将使用的语言的坏主意。 与不便相比,允许这样做的性能提升通常可以忽略不计。 相反,我们将宣布任何语法正确的程序在任何平台上都会产生相同的结果。 我们将使用诸如“ Python从左到右评估所有表达式”之类的短语来描述语言标准,而不是尝试根据处理器,操作系统或月相来对计算进行重新排序。 如果用户确定计算顺序很重要,则他有权正确地重写代码:最后,由用户负责。
运营重点
您必须遇到类似的错误:表达式
oflags & 0x80 == nflags & 0x80
总是返回0,因为C中的比较优先于按位运算。 换句话说,此表达式的计算结果为
oflags & (0x80 == nflags) & 0x80
哦,那个C!
我们将用简单的脚本语言消除潜在的此类错误原因,将比较运算的优先级置于算术和位操作之后,以便更直观地计算示例中的表达式:
(oflags & 0x80) == (nflags & 0x80)
其他改进
代码的可读性对我们很重要。 如果C语言的算术运算甚至是学校算术运算对于用户来说也是熟悉的,那么逻辑运算与按位运算之间的混淆无疑是错误的根源。 我们将用和替换双“&”号, or
用or
替换双竖线,以便我们的语言看起来更像是人类的语音,而不是“计算机”字符的纠葛。
我们将把简化计算的可能性留给逻辑运算符( https://en.wikipedia.org/wiki/Short-circuit_evaluation ),但也使他们能够返回任何类型的最终值,而不仅仅是布尔值。 然后像
s = error.message or 'Error'
在此示例中,如果该变量非空,则将其设置为error.message
,否则为字符串'Error'。
我们将C的概念扩展为0等于false,而不是整数。 例如,在空行和容器上。
我们将破坏整数溢出。 我们的语言在实现上将是一致的并且易于使用,因此我们的用户将不需要记住可疑的接近20亿的特殊值,此后,整个值加一,突然改变符号。 我们实现了这样的整数,它们将表现得像整数,直到耗尽所有可用内存为止。
严格与弱打字
脚本语言设计中的另一个重要问题是:键入的严格性。 许多观众都熟悉JavaScript? 如果从字符串“ 4”中减去数字3,会发生什么情况?
js> '4' - 3 1
太好了! 如果将数字3添加到字符串'4'?
js> '4' + 3 "43"
这称为松散(或弱)键入。 当编程语言认为程序员无法通过重复转换类型返回任何(甚至显然毫无意义的)表达式的结果时,程序员会谴责它,这就像自卑感。 问题是弱类型语言会自动生成的类型转换很少会产生有意义的结果。 让我们尝试一些更复杂的转换:
js> [] + [] "" js> [] + {} "[object Object]"
我们希望加法运算是可交换的,但是如果在后一种情况下更改术语会怎样?
js> {} + [] 0
JavaScript并不是唯一的问题。 在类似情况下的Perl也会尝试至少返回以下内容:
perl> "3" + 1 4
awk会做类似的事情:
$ echo | awk '{print "3" + 1}' 4
脚本语言的创建者传统上认为松散键入很方便 。 他们弄错了:松散的打字太糟糕了 ! 它违反了本地性原则。 如果代码中有错误,则编程语言应将其告知用户,从而在尽可能接近代码中有问题的位置处引起异常。 但是在所有这些无休止地强制转换类型的语言中,直到解决了某些问题,控件通常才结束,并且我们得到了结果,从中可以判断出程序中某个地方出了问题。 我们必须逐行调试整个程序,才能找到此错误。
松散的类型还会降低代码的可读性,因为即使我们在程序中正确使用了隐式类型转换,对于另一位程序员而言,这也是意外发生的。
在Python中,与在C ++中一样,此类表达式将返回错误。
>>> '4' - 3 TypeError >>> '4' + 3 TypeError
因为类型转换(如果确实有必要)很容易明确地编写:
>>> int('4') + 3 7 >>> '4' + str(3) '43'
这段代码易于阅读和维护,可以清楚地知道程序中到底发生了什么,从而导致结果。 这是因为Python程序员认为显式要比隐式好,并且不应忽略该错误。
Python是一种强类型语言,它唯一的隐式类型转换发生在对整数进行算术运算期间,其结果必须表示为小数。 也许在程序中也不允许这样做,但是在这种情况下,太多的用户将不得不立即解释整数和浮点数之间的差异,这会使他们在Python中的第一步变得复杂。
续:“ Python是C ++的终极案例。 第2/2部分 。“