从职业玩家角度看,Python 的开发效率真的比 Java高吗?

Python 的快主要体现在「当我有一个想法时,能够在最短时间内实现它的逻辑,并进行快速调整」。

开发不是单纯的堆积代码,很多时候,开发的过程是这样的:

为了解决问题 A,我想到了一个方案,通过 abc 的步骤来解决这个问题

用代码实现了 abc,但是发现无法解决几个 corner case,于是流程变成了 abcd

验证新的流程,如果还有问题,那么继续进行调整,直到解决所有可见的问题

「写出最后部署在产品上的代码」这部分,在一些复杂问题的解决中只占据一小部分时间。有时候你先用 Python 写出来一个正确的版本,再翻译成 C++、 Java(往往是出于性能的需求),开发效率并不比一开始用 Java 写要来的低。在 Python 里修改流程,可能只是几句话的事,但是换成 Java就得先定义几个新类型新接口,再进行具体实现。

因此,如果某一项开发工作预期会进行较多的尝试,那么先用 Python 去实现正确的逻辑是最佳选择之一。很多框架把「需要各种尝试」的步骤做成 Python 接口,然后把通用的计算流程用 C、C++实现,就是这个原因。

至于 IDE 和静态分析,相比于「实现正确的逻辑」,只是细枝末节而已。

统计了一下两个主要负责的项目的行数,只计.py文件,不计空行,计注释和docstring

一个是开源的VLCP,源代码约45000行,单元测试约3500行,集成测试1411行,开发三年左右了,最近升到了Python 3.5+

另一个是公司内部使用的项目,代码约23000行,集成测试7000行,单元测试700行左右,Python 3.6+,这个项目包括web前端、Python后端等多个组件,CI相关的shell脚本有一千行左右

这两个项目个人认为都是很成功的,也不难维护,以后一个为例,小的新需求一般2小时到一天就能上线,大的功能版本有时候要开发一个月,后端是两到三人的小团队。

总是看到说Python不能开发大型项目的说法,对这个问题是这样理解的:

从结论上来说,觉得Python开发项目不好维护,可能有两个方面的原因:1. 不熟悉Python的特性,沿用了Java等静态语言的开发模式和思路 2. 设计水平还不到家,走了弯路

尤其是,我们后面会提到,如果设计水平不够,或者开发时没有严守设计,动态语言代码劣化得会比静态语言快得多。但反过来,如果设计优秀,开发时严守设计,则动态语言维护起来可能反而更加省心。

实际上,Python开发的中型、大型项目很多,包括各类底层库、框架、大型系统等等。Python的系统库自然也是用Python开发的。Python的生态系统非常优秀,其实也是Python工程能力出众的表现。

再比如说有人举Google的例子,Guido亲自编写的项目后来用Java重写了,我们来看一下官方的说法(来自Gerrit官方网站):

Google Mondrian

Google developed Mondrian, a Perforce based code review tool to facilitate peer-review of changes prior to submission to the central code repository. Mondrian is not open source, as it is tied to the use of Perforce and to many Google-only services, such as Bigtable. Google employees have often describedhow useful Mondrian and its peer-review process isto their day-to-day work.

……

The Rietveld fork

Gerrit Code Review started as a simple set of patches to Rietveld, and was originally built to service AOSP. This quickly turned into a fork as we added access control features thatGuido van Rossum did not want to see complicating the Rietveld code base. As the functionality and code were starting to become drastically different, a different name was needed. Gerrit calls back to the original namesake of Rietveld, Gerrit Rietveld, a Dutch architect.

Gerrit 2.x rewrite

Gerrit 2.x is a complete rewrite of the Gerrit fork, changing the implementation from Python on Google App Engine, to Java on a J2EE servlet container and a SQL database.

以及一个第三方的资料(引用自Gerrit: Google-style code review meets git)

Mondrianhas been a huge success inside Google, Pearce said. "Almost every engineer uses this as their daily thing." But Mondrian is heavily dependent on Google's internal infrastructure, including the in-house Bigtable non-relational table store and the proprietary Perforce revision control system. Google is a huge Perforce shop, and has built its own highly-customized IT infrastructure, including Perforce-dependent tools.

……

Shawn Pearce, whopreviously reimplemented git in Javaas JGit, and is now at Google, took on the project; the result is Gerrit Code Review, now used to track public proposed changes to Android. Android's applications are written in Java, so writing the new tool in that language should make it more accessible to would-be contributors among Android developers.

也就是说:

Guido用Python开发了这个项目,并且取得了巨大的成功

后来这个项目被一个显然是Java专家的人接手了

大量的业务逻辑发生了变更,后来整个用Java重写了一遍

这个我认为显然不是Python的问题,而是项目leader的个人差异的问题,后来接手的人并不认可Guido的思路,重新按自己的思路发展了这个项目,导致了一些难以解决的设计冲突,最终决定重写将它理顺。如果Python开发大型项目有问题,为什么这个项目在Guido在的时候就是好好的,从Google内部迁移到Google App Engine的时候也没有出什么问题?将这个一个结果归到语言的身上,显然是看Guido走了之后的甩锅行为。

对于我个人来说,我最近几年的所有的项目都是用Python开发的,开源的有VLCP,不开源的有公司的若干内部系统,在上线之后的迭代中,从没有产生过动态语言难以重构的感想。事实上,因为我很少开发Java,我本身也很少使用IDE的refactoring功能,以前编写C++的时候也没有用过,有些代码是直接用没有装额外插件的vim(只有语法高亮)编写的。

对于一个项目来说,能用的代码很多,正确的代码往往只有一种或者少数几种。

什么是正确的代码?

代码的逻辑结构精确反映了业务的结构。代码的逻辑层级精确反映了业务在不同层次上的抽象。

业务中的不变性成为框架性的基础;业务中灵活多变的部分成为顶层代码、DSL、配置或者插件,从而能够方便地修改

代码有非常好的一致性,相同的业务对象用相同的类型表示,同类的业务对象用相同的接口表示,相同的业务逻辑用同一段代码实现,相似的业务逻辑用相同的抽象模型+不同的参数实现,独立的业务逻辑用独立的接口和模块实现。

业务模型中的保证和约束与代码中的假定完全一致,比如说,永远不假定某个网络请求一定会成功,永远不传入一个哪怕只是理论上可能超出允许范围的值。

可以看出,正确代码的定义和语言没有什么关系,它完全取决于代码与业务的结构关系,取决于代码是否与业务的抽象同构。因此,要写出正确的代码,首先必须要能正确地理解业务,做出对业务的优秀的抽象模型,然后基于模型做出代码模块、类和接口的设计,关键在于建模,而建模本质上来说是个数学问题。许多人可能推崇用编程语言思考,比如Thinking in Java,Thinking in C++,在我看来,其实应该首先Thinking in Mathematics,然后思考相应的数学模型如何用编程语言最佳地表示。国内常见的产品经理不懂技术,架构设计不懂业务的情况,对于做出正确的设计来说是非常糟糕的。

实际上,用任何常见的语言都能写出正确的代码,但这些正确的代码的设计不尽相同,有时候会跟语言特性有关,并不是Java所有的语言特性Python都支持,那么Java中正确的代码并不是逐字翻译过来就是Python中正确的代码,那只能叫做能用的代码。原因在于虽然数学建模是相似的,但编程语言的表意方式不同,有时候需要将相应的模型转化为不同的编程概念。

对于正确的代码来说,使用动态类型语言和静态类型语言开发效率是接近的,静态类型语言的某些限制会轻微地成为写出和维护正确代码的障碍。

注意我用限制而不是功能来描述静态类型。

举个例子来说,一个接口有三个成员函数,某个过程A使用其中前两个,而某个过程B使用其中后两个。实际上我们可以发现,实际过程A需要的并不是这个接口,而是只包含前两个成员函数的某个“子接口”,类似地,过程B需要的也是另一个不同的子接口。但是像Java这样的语言因为接口必须声明,所以通常为了方便,将它们合成一个接口来使用。但未来,真的不会出现只有前两个成员函数、而没有第三个成员函数的类型吗?这就是类型与抽象捆绑在一起导致的限制。相对来说,Go将类型与接口彻底分开,就是一个很有用的设计,虽然接口仍然要明确定义出来才能使用,不如动态语言方便,但因为提供了类型检查,算是一个权衡过的方案。而对动态语言来说,用到的就是接口限制的一部分,没有用到的就不是,也就是duck type,这个duck type是跟着代码实现一起变化的,优势就在于实现变更时,duck type随着实现一起变更,而不用单独维护;甚至,同一个参数可以在不同条件下依赖不同的成员函数,例如某个接口可以自动检测传入的参数是哪一个接口版本,从而自动实现兼容老版本的代码。

对于正确的代码来说,业务模型已经理清,需要的只是将业务模型映射到接口,然后分别实现每个接口。任何编程语言都可以轻易做到这个。对于动态语言来说,因为不需要写出类型,也可以充分利用动态特性,编码量会小一些,但静态语言也可以靠自动补全来弥补这个劣势。动态语言没有静态类型检查,但如果业务逻辑清晰,接口定义明确,类型检查的作用并不是不可或缺的:比如,某个接口传入的参数是用户名,一个熟悉业务逻辑的人一定会知道用户名的类型是字符串;调用这个接口的人也一定会把某个用户名传进去,如果他传了用户ID,那肯定是编码的时候很不认真才会犯这种错误。这种错误也不是光靠类型系统就能纠正的,如果用户有UUID,UUID也是字符串,类型检查就发现不了问题了。

在业务逻辑发生变化(需求变更)的时候,动态语言在维护正确的代码时有一定优势

有几方面的原因:

通过充分利用动态特性和duck type,能更容易将新的业务逻辑融合到老的框架中,而尽量不变更老的业务代码。

比如说,在上层代码不变的情况下,将底层实现迁移到新的库或者组件,比如从ZooKeeper迁移到etcd,在这个过程中,许多由上层业务逻辑保存的对象的类型都会发生改变,如果以前没有做好依赖倒置的设计,现在可能就需要重新修改许多代码。反过来,如我们以前说过的,动态语言的duck type天生就是一种接口依赖,只要保存的对象和以前有相同的接口,就完全不需要修改代码,我们只需要在新的etcd适配层上,实现和以前zookeeper接口完全相同的功能即可。

业务逻辑发生整体调整的时候,有时需要将某一段实现从一个模块移动到另一个模块中,在静态语言中一般通过IDE的refactoring功能进行。而在动态语言中,直接将类和函数复制到另一个文件中就可以了。

有人可能会问,那调用方怎么办,调用老的位置的模块的代码不就不能用了吗?但实际上,如果是因为功能迁移导致的实现移动,老的代码仍然调用这个接口本来就是有问题的,因为业务上这段逻辑应该已经发生变化了,要么这时候不应该调用了,要么这时候应该调用的是一个新接口,然后从新接口中再来调用这个迁移过的功能。如果只是无脑将调用方都改到新的地址,可能会导致代码的调用层次与业务逻辑不同构,让正确的代码降格为能用的代码。

不过,因为静态语言有IDE加持,可以充分利用refactoring的功能,大部分问题也都是可以弥补的,但必须要看到:对于动态语言来说,在维护正确的代码的时候,这些工作本身就是不必须的。

开发出正确的代码是很困难的

实际上,由于优秀的架构师很少,大部分架构师根本不合格,许多公司并没有正确的代码,只有能用的代码,这些项目也有一些特征:

设计里充满了各种因为主观喜好而添加进去的设计模式和接口,完全对应不上实际的业务;实际的业务中本来清晰的逻辑,被拆到了各种乱七八糟的地方,通过奇奇怪怪的hack完成

找不出一个能用简洁、一致的语言,清晰描述出业务逻辑模型的人。有的人只知道自己做的一点点东西,有的人能列举出一堆业务场景,但是形成不了模型

代码依赖很多实际业务中并不一定成立的假定(客户一定不会同时访问XXX和XXX,输入的XXX都是XXX),许多假定已经被证明是无效的,然后通过调用者的代码逻辑hack过去

三天两头出业务逻辑bug,修修补补,谁也没法保证代码运行的效果和需求一致,甚至拿出一个输入,有可能每个人理解它“应该”的输出都是不一样的

经常要在推倒重写和把代码修改得更丑之间艰难抉择

代码里有一大堆谁也说不上是什么用,就是不敢删的东西

如果有的人一直都在维护这样的代码,那他就会以为动不动就重构是理所当然的事情,就无法理解为什么动态语言也可以用来开发项目。

要开发出正确的代码,以下的条件都必须具备:

要有一个精确的业务模型和分层级的业务抽象

要有清晰的业务模型映射到代码实现的方案,并在组员中达成一致

必须有精通前两点的人为代码质量把关,严格拒绝所有与设计不符的代码

发现设计错误的时候,要勇于改正,哪怕会因此变动大量代码

为了让第四点有底气,必须有自动化的测试系统来提高开发测试的效率,保障代码正确率和测试覆盖率,这一点对于动态语言尤为重要。

实际上,即使是静态语言,如果你只在上线之前才简单手工测一下,你根本就是在撞大运。没有时间根本不是借口,无非就是懒惰而已。

动态语言较容易改对,但更容易改错

我们前面分析了动态语言在修改维护上实际上有一定的优势,但是反过来,如果没有一个优秀、强大的leader来为代码质量把关,动态语言劣化起来会比静态语言快得多,这一般来自于一些缺乏自律的程序员:

动态语言普遍没有访问级别检查(private,protected),一个缺乏自律的程序员为了实现逻辑,会轻易选择调用没有开放的内部接口和成员,而不是仔细设计外部接口,从而严重破坏设计

动态语言改变接口很容易,一个缺乏自律的程序员会在没有经过批准的情况下,擅自为设计好的接口添加额外的参数,以不符合设计的方式实现某些逻辑,从而严重破坏设计

动态语言一般不需要明确定义类的成员字段,可以随意为对象添加字段。一个缺乏自律的程序员会擅自通过自己新添加的字段来传递某些数据,而不通过接口参数,或者将不属于某个对象的数据保存在对象上,从而严重破坏设计

动态语言一般可以通过一些高级的动态特性实现特殊的效果。一个缺乏自律的程序员可能会在没有必要的情况下,仅仅因为个人的兴趣而擅自使用这些动态特性实现一些破坏可读性和接口假设的特性,从而严重破坏设计

为了图方便,一个缺乏自律的程序员可能会将新增加的逻辑添加在比较顺手的地方,而不是与设计层级相符的地方,从而严重破坏设计。这一点静态语言也难以避免。

我们可以注意到,这些全部都是主观原因、故意操作,完全可以通过自律和code review来避免。如果这样的破坏没有得到遏制,代码就会迅速劣化,从正确的代码变成能用的代码,从能用的代码变成难用的代码,从难用的代码变成不可用的代码。

能用的代码,如果不积极改造为正确的代码,往往会被迫进一步劣化,因为设计错误,某些需求无法实现,就会做出更奇怪、错得更离谱的设计,这一点在动态语言上也尤为明显。

静态类型语言代码也会劣化,但由于语言本身的限制,一般劣化得比较慢而不明显,但如果仔细研究那些能用的代码,通常早就已经千疮百孔了,这些劣化往往是下一次需求变动时候引起重大困难的原因。所以某种意义上来说,静态语言更适合维护那些能用的代码,减缓他们劣化的速度。

总结

无论是静态语言还是动态语言,项目是否可维护,都主要取决于业务模型、设计、代码是否正确

动态语言在维护正确代码时略有优势,但静态语言经过IDE加持之后差别不大

如果没有强有力的leader控制局面,动态语言进火葬场的速度的确非常快,但不是动态语言的错

多说一句Java,我认为Data Class、getter、setter这些真的是特别糟糕,原生的Array和Map又不给力,开发效率上要提上来,可能还是需要有一套行之有效的开发规范和开发工具的,说实话,我是有点用不来这玩意。Go,除了没有异常恶心点,其他还可以。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180911A1SYYG00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券