编者注:这是一篇来自Java社区的文章,作者的观点是从Java开发者的角度进行讲解的。但是....为了部落的荣耀,人生苦短,我用Python!
从哲学的角度来看,Python和Java是全然相反的。它放弃了静态类型和刚性结构,并以松散的沙盒结构取而代之,在这样的沙盒里你基本上可以做任何你想做的事情。也许Python这门语言关注的是你能做什么,而Java则更关注你可以做些什么。
尽管如此,两门语言都从C语言中汲取了大量灵感。它们都是使用块、循环、函数、赋值、中缀算术的命令式语言。二者都大量使用了类、对象、继承和多态。二者对异常的处理都非常优秀。两门语言都能自动进行内存管理。它们甚至都需要编译成字节码然后在虚拟机上运行,尽管Python的编译过程对用户是隐式的。Python有部分功能也借鉴了Java,例如Python标准库中的logging和unittest模块分别受到Java的log4j和JUnit的启发。
基于以上的技术重叠,我觉得Java开发者理应对Python产生家人一般的感觉。所以我给你们带来了Python的一些简单介绍。有机会的话,我会给大家讲一下是什么让Python和Java有所不同,以及这些不同吸引我的原因。至少,你有可能会把一些有意思的想法带回到Java的生态系统中。
语法
那么,就从hello world 开始吧:
呃,好吧,看起来好像没什么启发性。好吧,接下来是一个函数,用来统计文件中出现频次最高的十个单词。这里我是用了Python标准库中的Counter类取了个巧,没办法它实在太好用了。
Python使用空格作为分隔。人们对此颇有怨言。我之前甚至将其视为异端。但是十多年后的今天,这看上去竟如此正常以至于再让我用回花括号我还得花一段时间来适应。如果你是因为这个而迟迟没有开始,那么我不保证能说服你,但我强烈推介你暂时忽略它;在实际工作中,这真的不会带来太大的问题,反而会减少代码中的一些干扰。除此之外,Python开发者从来不用在诸如这个花括号应该放在哪里之类的问题产生争论。
除了这些美学上的区别外,其他看起来都很熟悉。我们得到一些数值,做了赋值操作,调用了模块。Python的import语句使用起来有一点不同,但是很明显它的作用是“使后面这些东西变得可用”。除了一些标点符号外,Python的for循环和Java的for循环非常像。函数使用def分隔而不是Java中的class,但是它运行的过程并无太大差异:函数可以使用参数调用,然后返回值(即使上述例子中的函数并没有返回值。)
只有两个地方有比较明显的差别。第一个是with代码块,这和Java 7 的“try-with-resources”很像—这保证了区块中的代码运行结束后文件的正常关闭,即使代码块中抛出了异常。另外一个是f”…”语法,这是相当新的一个特性,它允许把表达式直接写入字符串中。
大概就是这样了!你已经读了一些Python代码。至少,它看上去并不像一门完全来自外星的语言了吧。
动态类型
从上面的列子我们可以看到,Python代码没有太多的类型声明。变量声明的时候没有,函数的参数和返回值没有,对象上也没有。任何事在任何时间可以是任何类型。我还没有介绍过类的定义,所以这里就有一个:
甚至x,y都没有声明为属性;它们的存在是因为构造器中创建了它们。没有什么限制我传入整数,我也可以传入浮点数,小数或者分数等。
如果你只用过静态语言,这听起来简直要乱成一团。类型是温暖的懒惰的以及令人满意的。类型保证了…好吧,也许不一定能保证代码能顺利工作(尽管有人会不同意),但总是有好处的。当你连正确的类型都没搞清楚的时候你怎么可能顺利地运行代码。
稍等片刻- Java也不一定能保证这些呀。毕竟,所有的对象都可以是null,对吧?这样看来,实际上几乎从来没有一个类型正确的对象。
你可能会把动态类型看做对null问题的完全屈服。如果我们必须去处理它,我们也可以试着去拥抱它,然后让它来为我们工作—通过把所有问题都放在运行过程中解决。类型错误在Python中变成很普通的逻辑错误,你可以用相同的方式处理。
(另一种相反的方法,请查看Rust,这是一门没有null值或者异常的语言。我仍然更愿意使用Python,尽管我对Rust从不说谎的类型系统赞赏有加。)
在我的magnitude模块中,self.x的值是整型、浮点型还是其他任何类型的值都不重要。它只要支持**操作,然后返回支持+操作的值就够了。(Python支持运算符重载,所以它可能是任何类型。)正常的方法调用同样适用:只要在实践中能运行,那么任何类型都是可以接受的。
这意味着Python不需要泛型;一切都是按照泛型工作的。Python也不需要接口;一切都是多态的。没有向下转型,没有向上转型,在类型系统中也没有逃生舱口。当它们可以和任何可迭代对象在一起运行正常时,API也不需要必须是列表。
许多常见模式将变得更加简单。你可以创建一个封装和代理而不用去修改你的消费者代码。你可以使用组合来替代集成扩展第三方类,而不用做任何特殊的工作来保证多态。灵活的API不需要将每个类作为接口重复复制;一切都早已经是隐式的接口了。
动态类型哲学
使用静态类型,无论谁编写一些代码来选择类型,编译器都要确定其能运行。而使用动态类型,无论谁使用某些代码来选择类型,都会在运行过程中尝试一下。这就是前文讲到的两门语言对立哲学在实际中的体现:类型系统关注的是你可以做什么,而不是你可能要做什么。
这样使用动态类型被称为“鸭子类型”,这是基于思想“如果它走起路来像一只鸭子,叫起来也像一只鸭子,那么它就是一只鸭子”。简单来说,意思就是如果所有你想做的只是像鸭子一样叫,那么不用像静态语言那样限制你的代码必须接受一只鸭子,你可以接受任何给你的东西然后想办法让它像鸭子一样呱呱叫就行。如果它能做到,那么它就和鸭子一样好用了。(如果它做不到,你可能会得到一个类型错误,但这也不是多大的问题。)
同时要注意的是,Python仍然是强类型的。这个术语可能有点含糊,总体上来说它的意思就是值在运行过程中始终拥有他们的类型。典型的例子就是Python不会允许你把一个字符串和一个数字相加,而像JavaScript这样的弱类型语言将会隐式地把一个类型转换成另外一种,,这里使用的优先级规则和你想到的可能有所不同。
不像大多数的动态语言,Python在运行之前就可以捕捉错误信息。例如,从一个不存在的变量中读取信息会产生一个异常,包括从字典(类似Java中的Map)中读取一个不存在的键。在JavaScript、Lua和类似的语言中,上述操作会返回null值(对Map中不存在的键取值,Java也会返回null!)如果想在键不存在时返回默认值,Python的字典类提供了更加明确的方法调用。
这样的操作当然是有所取舍的,到底值不值得要因人而异、因项目而异。对我来说,至少非常适合用来设计一个更加可靠的系统,在我看到它运行之后;而静态语言则需要预先进行设计。静态类型使大量尝试不同想法变得更加困难。你确实少了一些静态检查保证,但在我的实践中,大多数的类型错误都会立刻被捕捉到...因为我写完代码的第一件事情就是运行一下。另外一些会在我们的测试中被捕捉到—这个测试你可以用任何语言来写,当然Python写起来要相对容易一些。
混合范式
Python和Java都是命令式和面向对象的;它们都通过执行命令来运行,同时都遵循一切皆对象的理念。
在最近的发行版中,Java新增了一些函数类的特性,我认为这是一个好事。Python也有一定比例的函数式特性,但是二者使用的方法还是有些不同。Python提供了内置的函数如map和reduce,但它们实际上并不是设计用来把一系列小的函数串联起来的。
相反,Python把挺多东西混合起来了。我不太清楚Python使用的方法的通用名称。我觉得它应该是把函数链的概念拆分成两部分了:序列运行,使函数本身更加强大。
序列
序列和迭代在Python中扮演了非常重要的角色。序列是最重要的数据结构之一,所以操作序列的工作都很容易获取。我认为Python对于函数式编程的实现如下:Python首先使得使用命令式代码来操作序列非常容易,而不是使得结合许多小函数然后应用于序列非常容易。
回到本文开始的地方,我曾写下这么一行代码:
for循环很常见,但这行代码中同时对两个变量进行迭代。真正发生的事most_common这个列表中的每个元素都返回一个元祖,一组有序唯一值。元组可以通过解包赋值给一组变量,这就是for循环中发生的事情。Python中的元组通常作为返回一组值,但它们有时在ad-hoc数据结构中也很有用。而在Java中,你需要一个完整的类以及很多行的赋值操作。
所有可迭代对象都支持解包。解包也支持嵌套结构,所以a, (b,c) = … 这种形式的可以顺利解包。对不定长的序列来说,* leftovers 元素可以出现在任何位置,并且将根据需要获取尽可能多的元素。也许你真的比较喜欢LISP?
Python同样也有创建列表的简单表达—被称为列表生成式—这比map之类的函数式方法要更常见。类似的方法也支持创建字典和元组。整个循环都被压缩到你真正感兴趣的一个表达式上。
标准库还在itertools模块中包含了一些有趣的迭代,组合器。
最后,Python使用生成器来生成命令代码的延迟序列。包含yield关键字的函数被调用时,不会立即执行,而是产生一个生成器对象。当生成器进行迭代时,代码运行到yield关键字处即停止;返回的值将作为下一次迭代的值。
由于生成器运行是有延迟的,它可以用来生成无限的序列或者在中途中断。它们可以用来生成大量的大型对象,而不需要为了保持存活而一次性消耗大量内存。它们同样被用来作为链式风格的函数式编程的一般替代。你可以编写熟悉的命令行代码,而不用考虑maps 和filters的组合。
在Java中实现一个完全任意延迟的迭代器,你可能需要在迭代器中增加一些代码来跟踪其状态。除了最简单的情况之外,这将变得有些棘手。Python也有迭代的接口,所以你仍然可以使用这种方式来实现,但生成器非常好用以至于大多数常用的迭代都是用它实现的。
另外,由于生成器可以自动停止,所以它们在另外一些场景下也是非常有用的。通过手动调用生成器(而不是用一次for循环来实现一次性全部迭代),你可以先运行一个函数,使其在某处停止,然后在函数运行过程中执行其他代码。Python依靠这些特性新增支持了异步I/O(不使用线程的非阻塞网络),即使现在它已经有专用的async 和 await 语法。
函数
Python的函数看上去似曾相似。你可以使用参数调用它。参数传递的方式也和Java一模一样 – Python既不是引用也不是隐式复制。Pyhton甚至也有描述符,和Java的注释类似,但是Python的是一种语法而且在运行过程中是可读的。
Java有使用args…的可变参数函数;相应的Python对应的函数使用*args。(支持 *leftovers语法的解包操作正是受函数这种语法的启发。)但Python在这基础上还提升了一些技巧。任何参数都可以设置默认值,使其成为可选参数。另外,参数也可以设置名称-之前我曾经这样做过Point(x=3, y=4)。*args语法用来在函数调用时接收一个列表作为参数;**kwargs用来接受或者传递字典类型的带名称参数。参数可以是“仅关键字keyword-only”的,所以参数传递必须通过名字,这对可选的布尔参数来说非常友好。
当然,Python没有函数重载的概念,不过大多数你需要它的时候都可以用鸭子类型和可选参数来取代。
这是现阶段Python最强大的功能之一。就像动态语言允许你使用封装或者代理透明的替换一个对象一样,* args和**kwargs允许任何函数被透明封装起来。
不好意思,这看上去有点难以理解。不过别太担心它是怎么工作的;需要知道的就是函数foo被一个新函数所代替了,它将所有的参数都转发给foo。不管是foo还是调用都和之前完全一样。
我还无法想象这个功能有多么强大。它可以用在日志、调试、资源管理、缓存、访问控制,验证等等。它在串联其他元编程特性上表现的非常不错,换句话说,它让你结构化思考而不只是单单的编程。
对象和动态运行时
运行时是一种在幕后驱动语言核心部分的东西,它可以在运行时被执行。C和C++之类的语言没有动态运行时;它们源码的结构被“烘焙”进编译器输出,之后就没有能修改的机会了。另一方面,Java确实有动态运行时,Java甚至还有一整套用来进行自省(反射)。
当然,Python也有自省(反射)。Python有许多简单的通过自省构建的函数可以用于监控、修改对象的属性,这对调试和偶发性的异常来说非常有用。
Python还将这个功能进行了升级。因为一切都在运行时完成,Python使用了很多扩展来自定义其语义。你无法修改语法,所以代码看上去仍然很像Python,但你可以分解出一些结构,这在死板的语言来说是非常困难的。
举一个极端点的例子,pytest使用Python的assert语句很智能的完成了很多事情。通常,assert x == 1 这样的代码当判定为假时,只会简单的抛出一个属性异常,这让你对异常产生的位置和时间毫无概念。这也正是Python内置的unittest模块(和JUnit以及其他语言的单元测试库类似)提供了一堆诸如assertEquals之类的函数的原因。不幸的是,这也让测试在第一眼看上去显得更加复杂难懂。但在pytest中,assert x == 1是可用的。如果结果为false,pytest将会告诉你x是什么…或者两个列表哪里出现偏离,或者两个集合有什么不同,以及其他事情。基于进行的比较和操作数的类型,所有的这一切都是自动产生的。
那pytest又是怎么运行的呢?这些你并不需要知道。你也不需要搞清楚使用pytest来写测试-这总是让人很开心的。
这正是动态运行时的优势所在。你个人来说可能不会使用这些特性。但你将从使用这些功能的库中获益良多而不需要去弄明白它们是如何工作的。甚至Python本身也通过使用自己的扩展拥有了很多特性-这些不需要改变语法和解释器。
对象
我最喜欢的例子是属性的获取。在Java中,一个point类可能会有getX()和setX()方法而不是直接操作x属性。原因是在不破坏接口的前提下,你可以修改x的读或写。在Python中,你不用担心前面的这些问题,因为必要时你能拦截属性的存取。
有趣的@property语法是一个装饰器,和Java的注解看上去很像,但是可以更直接的修改函数和类。
读取point.x现在调用了一个函数并以函数的返回值作为输出。这对调用代码来说是完全透明的-和读取其他属性并没太大区别-但对象可以根据自己的需要干预和处理它。不像Java,读取属性是类API的一部分,你可以自由得进行自定义。(注意这个例子使得x是只读的,因为我并没有指定如何对其进行修改。可写属性的语法看上去可能有点滑稽,这里暂时不管它如何工作。但你可以规定只有__init__可以赋值给point.x。)
同样的特性也存在于C#之类的静态语言中,所以这可能并不会让人们多么印象深刻。关于Python真正有意思的部分是属性本身并没什么特别。它这是一种使用纯Python语言几行代码就能写下来的内置类型。它能运行主要还是因为Python类可以自定义它的属性访问,包括一般的和暗属性的。封装、代理和组合是很容易实现的;你可以将所有访问调用转发到底层对象,而不必知道它有什么方法。
同样的钩子属性可用于惰性加载属性或者自动持有弱引用的属性,对调用代码来说完全透明,全部使用纯Python就能实现。
你可能已经注意到至今为止我的代码都没有public或者private修饰符。事实上,Python并没有这些概念。按照惯例,用一个下划线开头标识私有,或者更确切地说—“不打算作为稳定公开API的一部分”。但这其实并没有语法上的意义,Pyhton本身并不会阻止任何人查看或者修改这样的属性(调用方法)。同样,也没有final、static、const这些概念。
这又是同样的哲学在起作用:核心Python通常不会阻碍你做任何事情。当我们需要的时候,它将会非常有用。我已经通过启动时调用或重写、甚至重新定义私有方法来修改第三方包的BUG。这样我就不需要重新做一个项目的本地分支,可以省不少事情。而且一旦官方修复了BUG,我可以很容易得删掉自己的补丁代码。
同样,你可以很容易的修改依赖于外部状态的测试代码,例如当前时间。如果重构不行,你可以在测试期间使用模拟函数代替time.time()。库函数就是模块的属性(和Java的包类似),Python的模块和其他任何对象一样,所以它们也能以同样的方式来监测和修改。
类
Java的类由Class对象支持,但二者并不是通用的。对一个Foo类来说,其Class对象是Foo.class。我不认为Foo可以被它自己所使用,因为它命名了一个类型,Java在处理类型和值的时候有一些微妙的区别。
Python中的类是一个对象,是类型的一个实例(即类本身是一个对象,也是其自身的一个实例,这样想的话还是挺有趣。)这样的话类就可以被当成任何值来对待:可以当成参数传递,保存在更大的结构中,检查和操作。把类作为字典的值有时候特别有用。由于类是其本身实例化后的对象(Python没有new关键字),在很多场合类可以和一些函数通用。一些常用的模式类似工厂模式是如此简单以至于都快要消失了。
最近我好几次把函数和普通代码放在任何类外的顶层。这也是允许的。但其含义可能会有些微妙。在Python中,甚至class和def这样的表达式都只是运行时执行的普通代码。Python代码从上往下执行,class和def也不例外。他们只不过是创建类和函数这两种对象的语法罢了。
这才是最酷的部分。由于类是对象,他们的类型是type,因此你可以子类化然后修改它运行的方式。然后你可以使用你子类的实例来创建类。
第一次接触这些可能会有点奇怪。但是再一次说明,你不需要弄清它是怎么运行的,直接利用它就可以了。例如,Pyhton没有enum块,但它有enum模块::
Class语句创建了一个对象,这意味着它调用了某处的构造函数,而这个创建函数可以用来修改类创建的方式。这里,Enum创建了一个固定数量的类的集合而不是类属性。所有这些都是用纯Python代码和普通Python语法实现的。
所有的库都是基于以上思路构建的。你厌倦了单调的使用self.foo = foo 这样的方式在构造函数中为每个属性复制了么?然后纯手工定义比较、哈希、克隆和开发者可读的列表?Java可能需要编译器支持,可能来自于Amber项目。Python则足够灵活,所以在社区中使用attrs库解决了以上问题。
或者以SQLAlchemy为例,这是使用Python写的一个功能强大的数据库封装。它包含了一个灵感来自于Java的Hibernate库的ROM,但你可以直接在类里编写映射,而不需要在配置文件里定义表结构或者通过其他冗长的注解来实现。
这和Enum都基于同样的思路,但SQLAlchemy也使用了property类似的钩子,所以你可以很自然的修改列的值。
最后,类本身可以在运行过程中创建。这有点好处,但thriftpy创建了整个module,其中都是基于Thrift定义文件的类。在Java中,你需要代码生成,这增加了一个全新的编译步骤有可能会引发不同步。
所有的这些例子都依赖于Pyhton已有的语法,但又赋予其新的含义。所有这些也都不是Java或者其他语言所不能实现的,但Python减少了结构上的重复,这也使得代码更容易读写,并减少bug的产生。
回顾
Pyhton有很多和Java类似的基础概念,但把它们运用在完全不同的方向,同时也新增了很多思路。Java更关注稳定和可靠性,而Python则更关注表达性和灵活性。它们是命令式编程两种完全不同的方向。
对你来说我不太确定Python将来会不会在Java现在还领先的领域取而代之。例如,Python可能会在任何速度方面的测试中失败(但是可以了解下PyPy,一种即时编译的Python)。Java原生支持线程,而Python社区大都回避了这些问题。规模较大且复杂程度较高的软件更喜欢声明类型并提供合理性检查的静态语言(但可以看一下 mypy,Python的一个静态类型检查器。)
但也许Python将会在Java暂时不太擅长的领域继续发光发热。很多软件并不需要特别快或者并行执行,而更关注其他方面的效率。我发现使用Pyhton可以很快很容易的启动一个项目。没有单独的编译步骤,Python的写完马上运行的方式要快很多。Python的代码更短,这经常意味着更易读。尝试不同的架构成本也更低。有时候尝试一些比较愚蠢的主意会比较有趣,使用库实现goto功能。
我希望你可以试试Python。使用Python非常有趣,我觉得你也会有同感。只要你不把它当成隐藏了所有类型的Java。
最坏的情况,Python还有Pyjnius模块,它会让你这样使用Python。
译者:mrwoody
转自: https://blog.csdn.net/jerry_1126/article/details/74012174
英文原文:http://www.sitepoint.com/python-for-java-people/