编者按:在Java文章频道里,我们大部分人应该对该语言都非常的了解,而且在该生态圈内至少已经呆了好几年了。这让我们有常规和专业的知识,但是也同时也让我们一些井蛙之见。 在Outside-In Java系列文章中,一些非Java开发人员会给我们讲讲他们对于我们这个生态圈的看法。
从哲学的角度来讲,Python几乎是与Java截然相反。它抛弃了静态类型和刚性结构,而是使用了一个松散的沙盒,在这里面你可以自由的做任何你想做的事情。也许Python是关于你能够做什么,而Java则是关于你可以做什么。
然而,两种语言都从C语言里吸取了大量的灵感。他们都是命令式语言,拥有块、循环、方法、赋值以及中缀算术(infix math)。两者都大量使用了类、对象、继承以及多态性。两者的功能异常都相当优秀。 两者都能自动管理内存。它们都是编译成可以运行在某种虚拟机上的字节码,尽管Python是透明的进行编译。 Python甚至从Java汲取了一些营养:比如基本库的 logging 和 unittest 模块分别是受到了log4j 和JUnit的启发。
鉴于以上的技术重叠,我认为Java开发人员在使用Python时理应感到宾至如归。 所以我来给你一些简单的Python介绍。 我会可以告诉你什么使得Python与Java不同,以及为什么我觉得这些差异有吸引力。 至少,您可能会发现一些有趣的想法使您回到Java生态系统。
(如果你想要一个Python教程,Python文档是一个很好的选择,而且这是从Python 3的角度编写的,Python 2的使用还是很常见的,它与Python 3有一些语法上的差异。
语法
我们先把语法弄明白。下面是 hello world入门程序:
- print("Hello, world!")
嗯, 并不是很有启发性。 好吧,再来看一个函数,看看如何在一个文件中找到最常见的10个单词。在这里我取巧使用了标准库的 Counter 类型,但是它就是这么的好用。
- from collections import Counter
- def count_words(path):
- words = Counter()
- with open(path) as f:
- for line in f:
- for word in line.strip().split():
- words[word] += 1
- for word, count in words.most_common(10):
- print(f"{word} x{count}")
Python由空格分隔。人们经常对此有强烈的意见。当我***次看到它的时候,我 甚至认为这是异端邪说。现在,十多年过去了,这种写法似乎自然到我很难再回到大括号式的写法。如果你因此逃避,我甚至怀疑我可以说服你,不过我劝你至少暂时忽略一下它;实际上它并没有造成任何严重的问题,反而消除了一大堆的干扰。此外,Python开发人员从来不必争论{应该放在哪里。
除了审美上的差异之外,其他方面应该看起来很熟悉。我们有一些数字,一些赋值和一些方法调用。import 语句的工作方式有些不同,但它具有相同的“使这些内容可用”的一般含义。 Python的for循环与Java的for-each循环非常相似,只是少了点标点符号。函数本身使用def而不是类型进行分隔,但它正是按照你期望的方式工作:您可以使用参数调用它,然后返回一个值(尽管某些函数不返回值)。
只有两件事情是很不寻常的。 一个是 with 块,非常类似于Java 7的“try-with-resources” – 它保证文件在块的结尾处关闭,即使会抛出一个异常。 另一个是f“…”语法,这是一个相当新的功能,允许将表达式直接插入到字符串中。
就是这样! 你已经读了一些Python的内容。 至少,它不是来自一个完全不同的星球的语言。
动态类型
看这个例子可能很明显,但是Python代码里没有太多的类型声明。 变量声明上没有,参数或返回类型上没有,对象上也没有。 任何值在任何时候都可以是任何类型的。 我还没有显示一个类定义,所以这里只是一个简单的定义。
- class Point:
- def __init__(self, x, y):
- self.x = x
- self.y = y
- def magnitude(self):
- return (self.x ** 2 + self.y ** 2) ** 0.5
- point = Point(3, 4)
- print(point.x) # 3
- print(point.magnitude()) # 5.0
尽管x和y有并没有定义为属性,它们的存在是因为构造器中创建了它们。没有谁强制我必须传个整型参数,我也可以传小数和分数。
如果你以前只用过静态语言,这可能看起来一片混乱。类型是温暖的懒惰的以及令人满意的。他们保证···(好吧,或许不会)代码实际能工作(虽然有人不同意)。但是当你都不知道什么是正确类型的时候,你又怎么能依靠代码呢?
但是等等 – Java也没有这样的保证! 毕竟任何对象可能都是null,对吧? 实际上几乎从来没有一个正确类型的对象。
您可能会将动态类型视为对null问题的彻底放弃。 如果我们必须处理它,我们不妨拥抱它,并让它为我们工作 – 通过将一切 延迟到运行时。 类型错误变成正常的逻辑错误,您可以以相同的方式处理它们。
(对于相反的方法,请参阅 Rust,它没有空值 – 或者异常,我还是宁愿写Python,但是我很欣赏Rust的类型系统并不总是对我说谎。)
在我的magnitude方法中,self.x是int或float或任何其他类型,这都不重要。 它只需要支持**运算符并返回支持+运算符的内容。 (Python支持操作符重载,所以这可能是任何内容。)同样适用于普通方法调用:任何类型都可以接受,只要它实际上可以工作。
这意味着Python不需要泛型; 一切都是按照泛型工作的。 不需要接口; 一切都已经是多态的。 没有向下转型(downcasts),没有向上转型(upcasts),在类型系统中没有逃生舱口(escape hatches)。 当它们可以和任意Iterable工作良好时,运行时API不需要List。
许多常见的模式变得更加容易。 您可以创建包装器对象和代理,而无需更改消费代码。 您可以使用组合而不是继承来扩展第三方类型,而不需要为保留多态做任何特殊的操作。 灵活的API不需要将每个类作为接口复制; 一切都已经作为一个隐式接口了。
动态类型哲学
使用静态类型,无论谁编写 某些代码来选择这些类型,编译器都会检查它们是否正常工作。 使用动态类型,无论谁使用 某些代码来选择这些类型,运行时都会尝试一下。 这就是对立的哲学在起作用:类型系统着重于你可以 做什么,而不是你可能 做什么。
这样使用动态类型有时被称为 “鸭子类型”(duck typing),这是基于这种思想: “如果它走起来像鸭子,而且叫起来也像鸭子,那么它就是一个鸭子。” 换言之,就是说,如果你想要的是能像鸭子一样呱呱叫的东西的话,你不用强制你的代码必须接收一个鸭子对象,相反的你可以接收任何给你的东西,并且让它能够呱呱叫即可。如果它可以达成你的目标的话,那么它就跟鸭子一样好用。(否则如果它无法如你所愿,会抛出AttributeError的错误,但是这也没什么大不了的。)
同时也要注意Python是强类型的。这个词有点模糊,它通常意味着变量的值在运行时会一直保持其类型不变。一个典型的例子是,Python不会让你把一个字符串赋值给一个数字类型的变量,而像弱类型语言,如JavaScript 则可以将一种类型静默的转换成另一种,这里使用了优先级的规则,跟你的预期会有所不同。
与大多数动态语言不同的是,Python 在运行之前就可以捕获错误信息。例如读取一个不存在的变量会产生一个异常,包括从一个字典对象(如 Map)中读取一个不存在的键时也会报错。在 JavaScript 、Lua 和类似语言中,上面两种情况是返回 null 值。(Java 也是返回 null 值)。如果想在值不存在时返回默认值,字典类提供了更加明确的方法调用。
这里绝对是一个权衡的结果,是否值得取决于不同的项目和人。对我来说,至少非常适合用来设计一个更加可靠的系统,以为我可以看到它实际执行的情况。但是静态类型的语言都期待有一个预先的设计。静态类型很难尝试各种不同的想法,更难发挥。
你确实拥有了更少的静态检查保证,但根据我的经验,大多数的类型错误可以正确被捕获……因为我写完代码的***件事就是尝试去运行它!其它任何错误应该可以被你的测试所捕获—测试应该在所有语言中都有,并且python语言写测试相对来说更容易。
一个混合的范例
Python 和 Java 都是命令式和对象导向的:它们的工作方式是执行指令,它们把每件事物塑造为对象。
在最近的发行版本中,Java增加了一些函数式语言特征,我认为这是一件好事。Python也有函数式语言特征,但是实现的方式不太一样。它提供了一些内置的函数如map和reduce,但是它并不是基于串联许多小函数的思想来设计的。
相反,Python混合了其它东西。我不知道Python采用的方法的通用名称。我认为它把“函数链”的思想分为两个:作用于序列上和使函数自身更加有用。
序列
序列和迭代在Python有重要的地位。序列是最基础的数据结构,作为于之上的工具非常有价值。我认为Python对于函数式编程的实现如下:Python首先使得使用命令式代码来操作序列非常容易,而不是使得结合许多小函数然后应用于序列非常容易。
回到本文开始的地方,我曾写下这一行代码:
- for word, count in words.most_common(10):
for循环对我们来说很熟悉,但是这行代码一次迭代了两个变量。 实际发生的是,列表most_common中的每个元素返回一个元组,它们是一组按顺序区分的值。 真正发生的是元组可以通过将它们分配给元组变量名来解包。 元组通常用于在Python中返回多个值,但它们偶尔也可用于特殊结构。 在Java中,您需要一个完整的类和几行分配值的代码。
任何可以迭代的东西同样可以解包。 解包支持任意嵌套,所以a,(b,c)= …按照它看起来的样子解包。 对于未知长度的序列,一个*leftovers元素可以出现在任何地方,并且将根据需要获取尽可能多的元素。 也许你真的会喜欢LISP?
- values = [5, 7, 9]
- head, *tail = values
- print(head) # 5
- print(tail) # (7, 9)
Python还具有通过简单表达式创建列表的语法 – 所谓的“列表解析” – 这比函数方法如map更为常见。 存在类似的语法用于创建分组和集合。 整个循环可以减少到一个单一的表达式,只突出你真正感兴趣的部分。
- values = [3, 4, 5]
- values2 = [val * 2 for val in values if val != 4]
- print(values2) # [6, 10]
标准库还在itertools模块中包含了许多有趣的迭代,组合器和用法。
***,Python具有用于生成命令行代码的延迟序列的生成器。 包含yield关键字的函数在被调用时不立即执行; 而是返回一个生成器对象。 当生成器迭代结束时,该函数运行直到它遇到一个yield,此时它暂停; 生成的值将成为下一个迭代值。
- def odd_numbers():
- n = 1
- while True:
- yield n
- n += 2
- for x in odd_numbers():
- print(x)
- if x > 4:
- break
- # 1
- # 3
- # 5
因为生成器延迟运行,它们可以产生***序列或在中途中断。 他们可以产生大量的大型对象,不会因为让它们全部存活而消耗一大堆内存。 它们也可以作为“链式”函数编程风格的一般替代。 您可以编写熟悉的命令行代码,而不是组合map和filter。
- # This is the pathlib.Path API from the standard library
- def iter_child_filenames(dirpath):
- for child in dirpath.iterdir():
- if child.is_file():
- yield child.name
要在Java中表示完全任意的惰性迭代器,您需要编写一个手动跟踪其状态的Iterator。除了最简单的情况之外,这一切都会变得相当棘手。 Python也有一个迭代接口,所以您仍然可以使用这种方法,但是生成器非常容易使用,以至于大多数自定义迭代都是用它们编写的。
而且因为生成器可以自己暂停,所以它们在其他一些上下文中是有用的。 通过手动调用生成器(而不是仅用一个for循环来一次性全部迭代),就可以在一段时间内运行一个功能,让它在某一点停止,并在恢复该函数之前运行其他代码。 Python充分利用这一点来添加对异步I/O(不使用线程的非阻塞网络)的支持,尽管现在它具有专用的async 和await 语法。
函数
乍一看,Python的函数看上去非常面熟。你可以使用参数来调用它们。传递风格与Java完全相同—Python既没有引用也没有隐式复制。 Python甚至有“docstrings”,类似于Javadoc注释,但它是内置的语法并且在运行时可见。
- def foo(a, b, c):
- """Print out the arguments. Not a very useful function, really."""
- print("I got", a, b, c)
- foo(1, 2, 3) # I got 1 2 3
Java具有args …语法的可变函数; Python使用* args可以实现类似的功能。 (用于解包的*leftovers语法灵感来源于函数语法。)但是,Python还有一些技巧。任何参数都可以有一个默认值,使其成为可选项。任何参数也可以通过名称 给出 – 我之前使用Point(x = 3,y = 4)演示了这点。在调用 任何函数时,可以使用* args语法,它将传递一个序列,就像它是单独的参数一样,并且有一个等价的** kwargs将命名参数作为一个dict接受或传递。一个参数可以作为“仅关键字(keyword-only)”,所以它必须 通过名称传递,这对于可选的布尔值是非常好的。
Python当然没有 函数重载,但是你使用它实现的功能大部分都可以被鸭子类型(duck typing)和可选参数替换。
这是现阶段Python***大的功能之一。 与动态类型一样,您可以通过包装器或代理透明地替换对象,* args和** kwargs允许任何函数 被透明地包装。
- def log_calls(old_function):
- def new_function(*args, **kwargs):
- print("i'm being called!", args, kwargs)
- return old_function(*args, **kwargs)
- return new_function
- @log_calls
- def foo(a, b, c=3):
- print(f"a = {a}, b = {b}, c = {c}")
- foo(1, b=2)
- # i'm being called! (1,) {'b': 2}
- # a = 1, b = 2, c = 3
这有点难以理解,对不起。 不用担心它究竟是如何工作的; 要点是,foo被一个new_function替换,它将所有的参数转发到foo。 foo和调用者都不需要知道哪些事情有哪些不同。
我不能低估它有多么强大。 它可用于记录,调试,管理资源,缓存,访问控制,验证等。 它与其他元编程功能工作良好,同样地,它可以让您分解结构,而不仅仅是代码。
对象和动态运行时
动态运行时是一种在背后驱动语言核心部分的东西 — 它可以在运行时被执行。像 C 或者 C++ 这类的语言绝不会具有动态运行时;它们源码的结构被“烘焙”成编译输出,并且后续没有明显的方法来改变它的行为。从另一方面,Java的确具有动态运行时!它甚至带有一整个专门用于反射的包。
Python 当然也有反射。很多简单的函数通过反射被构建,用来联机检查或修改对象的属性,这对调试以及偶尔的欺骗非常有用。
但 Python 对此更深入一点。 因为一切都在运行时完成,Python 暴露了很多扩展点来自定义它的语义。 你不能改变语法,代码依然看起来像 Python,但你可以分解结构 — 这在一种更死板的语言是非常难以做到的。
举个极端的例子,看下 pytest, 它聪明的处理了 Python 的 assert 声明。 通常, assert x == 1 为 false 时只会简单的抛出一个 AssertionError 异常,导致你不知道错误是什么或者哪里出错了。这就是为什么 Python 内置的单元测试模块 — 像 JUnit 和很多其他的测试工具 — 提供很多专门的工具函数,比如 assertEquals。不幸的是,这些工具函数使得测试初看到时,更加复杂更难以读懂。但在 pytest 中, assert x == 1 是好用的。如果失败,pytest 将告诉你 x 是… 或者两个列表哪里有分歧,或者两个集合哪些元素不同, 或者其他的。所有的这些都是基于比较完成和运算对象的类型自动发生的。
pytest是如何工作的呢? 你确实不想知道。你不需要知道如何用pytest写测试 — 这使人很开心。
这就是动态运行时的真正优势所在。 就自己而言,可能没有使用这些功能。但是你能从这些库中收获巨大的好处,你使用这些库并不需要关心它们如何运行。 甚至 Python 本身依靠使用自己的扩展点实现了很多额外的特性 — 这些不需要改变语法或者解释器。
对象
属性(在C#中一般翻译成特性)存取是我喜欢举的一个简单例子。在 Java 中,一个 Point 类可能选择用 getX() 和 setX() 方法而不是一个简单直白的 x 属性。原因是如果你需要改变 x 的读或写,你不需要破坏接口。在 Python 中,你不需要担心前面的这些, 因为必要时你能解释属性存取。
- class Point:
- def __init__(self, x, y):
- self._x = x
- self._y = y
- @property
- def x(self):
- return self._x
- # ... same for y ...
- point = Point(3, 4)
- print(point.x) # 3
有趣的 @property 语法是一种修饰,看来就像 Java 的注解,但它可以更直接地修改函数或类。这是完全透明的调用代码——和读其它属性没什么区别——但是对象可以根据自己的需要干预或处理它。与 Java 不同,属性访问是类 API 的一部分,可以自由的定义。(注意这个示例让 x 成为只读的,因为我没有指定写方法!可写属性的语法有点滑稽,这里暂时不管它如何工作。但你可以具体规定只有奇数可以赋值给 point.x。)
这个特性也存在于其它静态语言中,比如 C#,所以这并不是什么大不了的东西。关于 Python 真正有趣的部分是,属性并没什么特别。它是一个正常的内建类型,一段纯粹而且不满一屏的 Python 程序。它的工作原理是 Python 类可以自定义其属性访问,包括一般的和按属性的。包装、代理和组合很容易实现:你可以将所有访问调用转发到底层对象,而不必知道它有什么方法。
相同的钩子属性可用于懒加载属性或者自动持有弱引用的属性——这对调用代码完全透明,通过纯 Python 就能实现。
你可能已经注意到我的代码没有使用 public 或 private 修饰符。事实上,Python 中不存在这个概念。按照惯例,用一个下划线开头表示“私有”——或者更准备地说,“不打算成为稳定公开 API 的一部分”。但这并没有语言上的意义,Phthon 本身不会阻止侦查或修改这样的属性(如果是方法的话,则是调用)。同样,也没有final 、static或const。
这是同样的工作原理:核心 Python 通常不会妨碍你做任何事情。如果你需要,它会非常有用。我已经通过启动时调用或重写,甚至重新定义私有方法来修补第三方库的 BUG。这样我不需要重新做一个项目的本地分支,可以省不少事。而且一量官方修复了 BUG,可很容易就能删掉自己的补丁代码。
同样,你可以轻松地编写依赖外部状态的测试代码——比如当前时间。如果重构不可行,你可以在测试时把 time.time() 替换为模拟函数。库函数只是模块模块的属性(就像 Java 的包),而且 Python 模块是种对象,和其它对象一样,所以它们可以以同样的方式侦查到或被修改。
类
Java 的类由 Class 对象支持,但二者并不有完全互换。比如 Foo 类的 Class 对象是 Foo.class。我不认为 Foo 可以被它自己使用,因为它命名了一个类型,Java 在处理类型和值的时候会有一些微妙的区别。
Python 中,类是对象,是类型的实例(它本身就是对象,是它自己的实例,想起来很有趣。)类可以当作其它值一样使用:作为参数、保存一某个更大的数据结构中、检查、操作。有时候把类作为字典的键特别有用。而且因为类是实例化的,可以简单地调用它们——Python 没有 new 关键字 —— 很多情况下 new 和简单的函数调用可以互换。这样一来,一些常见的模式,比如工厂模式,就太简单了,几乎不需要。
- # 等等,Vehicle 是一个类还是一个工厂函数?谁管呢!
- # 就算它在类或工厂函数之间互换,也不会破坏这段代码。
- car = Vehicle(wheels=4, doors=4)
最近我好几次把函数甚至常规代码放在顶层,不在任何类里面。这样做不会有问题,但是其含义有点微妙。Python 中甚至 class 和 def 都是常规代码,在运行的时候执行。Python 文件从上往下执行,class 和 def 并不特别。它们只是有特殊的语法,用来创建特定类型的对象:类和函数。
以下是真正酷的部分。类是对象,它们的类型是type,因此你可以子类化并改变它的运行。然后,你可以生成你的子类的实例。
***次接触仔细想想会感觉有点奇怪。但是再想下,你获益于不需要知道它是如何运行的。比如,Python没有enum块,但它确实有 enum module:
- class Animal(Enum):
- cat = 0
- dog = 1
- mouse = 2
- snake = 3
- print(Animal.cat) # <Animal.cat: 0>
- print(Animal.cat.value) # 0
- print(Animal(2)) # <Animal.mouse: 2>
- print(Animal['dog']) # <Animal.dog: 1>
class 语句创建了一个对象,这意味着它在某处调用了构造函数,而且可以重写该构造函数来改变类的构造方式。这里Enum创建了一个固定的实例集,而不是类属性。所有这些都是用普通的 Python 代码的常规的 Python 语法实现的。
实体库也是基于这个思路来构建的。你讨厌在构造函数中单调的干 self.foo = foo 这种事情吗?然后纯手工定义相等性比较、哈希和克隆和开发可读的列表?Java 需要编译器支持,这个支持可能来自 Amber 项目。Python 非常灵活,所以社区中有 attrs 库解决了这个问题。
- import attr
- @attr.s
- class Point:
- x = attr.ib()
- y = attr.ib()
- p = Point(3, 4)
- q = Point(x=3, y=4)
- p == q # True, which it wouldn't have been before!
- print(p) # Point(x=3, y=4)
或者采用 SQLAlchemy 这个功能强大的 Python 数据库封装库。它包含一个灵感来自 Hibernate 的 ORM,但不需要在配置文件里定义表结构或者通过其他冗长的注解,你可直接在类里编写数据库映射代码:
- class Order(Table):
- id = Column(Integer, primary_key=True)
- order_number = Column(Integer, index=True)
- status = Column(Enum('pending', 'complete'), default='pending')
- ...
它的基本思想类同Enum,但SQLAlchemy也使用和property同样的钩子,自然而然地,你可以修改栏位值。
- order.order_number = 5
- session.commit()
***,类本身可以在运行中被创建。 这有点好处,可是 thriftpy 创建的整个 module, 里面全是基于 Thrift 定义文件的类。 在Java中,你得需要代码生成,这就增加了全新的编译步骤,从而导致不同步。
所有这些示例依赖于Python现存的语法,但也在里面吸收了新的含义。它能做的,没有你在Java或者其他语言中不能做的。但它删减了结构性的重复 — 正是这些使得编码更易写、易读,以及产生更少的bug。
结语
Python 有很多和Java相同的基本概念,但处理方向非常不同,它加入了一些全新的思想。Java关注于稳定性和可靠性,而Python关注于可表达性和灵活性。它是一种完全不同的思想方式来思考命令式编程。
我有理由相信Python将让你在Java所擅长的领域替换Java。Python可能不会赢在速度竞赛上,比如(Pypy,一种即时编译的Python)。Java拥有对线程的原生支持,而Python界大都回避了这些问题。规模庞大复杂又有很多死角的软件更喜欢静态类型提供的合理性检查(比如mypy,一种Python静态类型检查器)。
也许某些 Java 并不擅长的领域刚好是 Python 的强项。例如大量的软件对性能的要求并不高,也不需要并行执行,我们只需要考虑具体实现业务的问题。我发现使用 Python 开始一个项目非常简单而且快速。没有单独的编译过程,编码然后运行的速度非常快。代码很简短,这也意味着代码更容易被理解。尝试不同的架构方法看起来更加容易。而且有时候尝试一些愚蠢的想法是如此的有趣,就好像 使用库实现 goto 功能。
我希望你能试试 Python ,自从我用了它之后,已经从中获得很多乐趣,我相信你也一样。至少不用刻意的去拒绝它。
最坏的情况,还有个 Pyjnius 可以让你这样做:
- from jnius import autoclass
- System = autoclass('java.lang.System')
- System.out.println('Hello, world!')