面向忙碌的-Java-开发者的-Python-教程-全-

面向忙碌的 Java 开发者的 Python 教程(全)

原文:Python for the Busy Java Developer

协议:CC BY-NC-SA 4.0

一、语言

让我们从了解 Python 与 Java 的不同之处开始我们的 Python 之旅。在下一章深入研究 Python 的语法之前,我将帮助你设置 Python。

Python 是什么?

Python 是一种“开放源代码、通用编程语言,它是动态的、强类型的、面向对象的、函数式的、内存管理的,并且使用起来很有趣。”一句话用了这么多形容词!让我们一次打开一个。

Python 是在开源、BSD 风格的许可下发布的,称为 Python 软件基金会许可协议。这是一个非常宽松的许可,允许在如何使用 Python 方面有很大的灵活性。Python 的开发是由一个庞大而多样化的志愿者社区公开完成的。

Python 是通用的,因为您可以使用它来构建各种应用程序,从简单的脚本和命令行工具到桌面和 web 应用程序、网络服务器、科学应用程序等等。

我们知道 Java 是一种静态类型语言;也就是说,类型是在编译时检查和强制执行的。相比之下,Python 是动态的,这意味着只在运行时检查类型。但是 Python 也是强类型的,就像 Java 一样。您只能执行目标类型支持的操作。

另一种思考方式是,在 Java 中,变量和对象都有与之关联的类型;而在 Python 中,只有对象有类型,而没有它们所绑定的变量。在 Java 中,当我们声明

MyType obj = new MyType()

变量obj被声明为MyType类型,然后新实例化的MyType类型的对象被赋给它。相比之下,在 Python 中,相同的声明如下所示

obj = MyType()

忽略缺少的new关键字(Python 没有),obj只是一个绑定到右边对象的名字,恰好是MyType类型。我们甚至可以将obj重新分配到下一行——obj = MyOtherType()——这不成问题。在 Java 中,这种重新分配将无法编译 1 ,而在 Python 中,程序将会运行,只有当我们试图通过obj执行一个与当时分配给它的类型不兼容的操作时,程序才会在运行时失败。

Python 是面向对象的,支持 Java 所有的标准 OOP 特性,比如使用类创建类型、封装状态、继承、多态等等。它甚至超越了 Java,支持诸如多重继承、操作符重载、元编程等特性。

Python 还支持丰富的函数式编程特性和习惯用法。在 Python 中,函数是一级对象,可以像其他对象一样创建、操作和传递。虽然 Python 对函数式编程的强调可能不像 Clojure 那样集中,但它确实为函数式程序员提供了比 Java 更多的东西。 2

这两种语言之间的另一个相似之处是手动内存管理,因为没有手动内存管理。语言运行时负责正确分配和释放内存,将程序员从手工管理内存的苦差事和错误中解救出来。话虽如此,JVM 垃圾收集器的性能要比 Python GC 好得多。根据您正在构建的应用程序的类型,这可能会成为一个问题。

最后,也是最重要的,Python 很有趣,使用起来很愉快。这是一个强有力的声明,但是我希望当你读完这本书的时候,你会同意我和其他数百万 Python 程序员的观点!

历史

Python 是荷兰程序员吉多·范·罗苏姆的发明。20 世纪 80 年代末,当他对 ABC 语言感到失望时,他开始从事这项工作,经过几年的私人开发,他于 1994 年发布了 Python 的第一个版本。这实际上使 Python 比 Java 更古老,Java 的第一个版本是在整整两年后的 1996 年发布的!两种语言的比较如表 1-1 所示。

表 1-1

Historical comparison of Java and Python

| 爪哇 | 计算机编程语言 | | :-- | :-- | | 詹姆斯·高斯林 | 圭多·凡 rossum | | 来自 C++/Oak | 来自 ABC | | 1.0-1996 年 1 月 | 1.0-1994 年 1 月 | | 2017 年 9 月 9 日 | 2016 年 12 月 3.6 日 | | 日本合成橡胶 | 精力 | | 商业 | 社区 |

Note

只要有意义,我将使用这种表格格式来比较 Python 和 Java。

从那以后,随着 Python 2.0 在 2000 年的发布,这种语言一直在改进和发展。在撰写本文时,2.x 版本是部署最广泛的版本。

在 3.0 版本中,语言设计者决定打破向后兼容性,以便清除一些累积的语言缺陷。尽管从语言的角度来看这很好,但是对于从 2.x 升级到 3.x 的人来说,这是一个很大的障碍。至少可以说,今天的 Java 语言会好得多,但是过渡期会很困难。这就是 Python 用户社区正在经历的转变。

Note

由于 2.x 仍然是使用最广泛的 Python 版本,本书将涵盖 Python 2.x 的特性和语法,并不时指出与 3.x 的差异。

从一开始,Python 的开发就是在开放的环境中进行的,有一群志愿者为这种语言和核心库做出贡献。任何语言的改变都是通过一个叫做 PEP (Python 增强提案)的过程提出和讨论的,Guido 对决定结果有最后的发言权。由于他在 Python 开发中的管理和持续参与,Guido 被亲切地称为“仁慈的终身独裁者”他还定期撰写 Python 历史博客 3 ,记录各种语言特性的演变。

装置

这本书充满了示例代码,最好的学习方法是亲自尝试这些示例。为此,您显然需要在您的系统上安装 Python。但是更简单的方法是检查您是否已经可以访问安装了 Python 的系统!几乎所有运行 Linux 的系统都应该预装 Python。Mac OS X 的最新版本也预装了 Python。只需在这两个系统中的任何一个上打开一个命令 shell,然后输入python。如果您得到一个 Python shell 提示,您就一切就绪了!安装的 Python 版本可能有点过时,但应该足够开始使用了。

Tip

作为一个轻量级的替代方案,你可以尝试一个在线的 Python 环境,比如 http://repl.it/ 。这本书里的例子都很简单,可以在那里工作。

工具

Python 源代码被组织在扩展名为.py的文件中。python可执行文件解释源代码,并将其翻译成存储在.pyc文件中的特定于 Python 语言的字节码。然后,这个字节码由 Python 虚拟机执行,该虚拟机也由同一个python可执行文件调用。虽然这听起来像两步,但实际上,这只是字节码动态生成的一步。

这与 Java 形成对比(见表 1-2 ),在 Java 中,源代码的解析和编译以及编译后的字节码的实际执行分别由javacjava负责。在 Python 中,python可执行文件处理这两个步骤。事实上,.pyc文件实际上只是保存翻译后的字节码的中间缓存。它们对于执行并不是绝对必要的。如果你删除了.pyc文件,它们会在你下次运行.py文件时重新生成。

表 1-2

Comparison of Tools

| 爪哇 | 计算机编程语言 | | :-- | :-- | | 。爪哇岛 | 。巴拉圭 | | 。班级 | 。力兴 | | Java.exe+javac.exe | python.exe | | IntelliJ IDEA | PyCharm | | Eclipse JDT | 派德夫 | | Java 9 JShell | 取代 |

有多种 ide 可用于编写 Python 代码。基于 Eclipse 框架的 PyDev 和基于 IntelliJ IDEA 框架的 PyCharm 是两个比较流行的选择。虽然拥有一个 IDE 很好,但使用纯文本编辑器(如 Vim 4 或 Sublime Text)编写 Python 代码是完全可行的。

Python 的一个有趣特性是 REPL,这是 Java 所没有的,它是 Read Eval Print Loop 的缩写。在这里,快速演示会很有用。如果您可以访问 Python 安装(遵循本章“安装”一节中的说明),继续运行一个python shell,如下所示:

antrix@dungeon:~$ python
Python 2.7.5+ (default, Feb 27 2014, 19:39:55)
[GCC 4.8.1] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>>

当您以这种方式运行python可执行文件时,它会以交互模式启动。前几行包含版本和底层操作系统等信息。之后,您会看到>>>提示。这是您与 Python 进行所有交互的地方。python shell 正在运行一个循环,它将读取您在这个提示符下键入的所有内容,评估它所读取的内容,然后打印结果。因此得名,REPL。

让我们试一试:

>>> 10 + 10
20
>>>

我们在提示符下键入10 + 10,然后按下回车键。Python REPL 读取该值,对其进行评估,并打印结果。然后它又回到提示符下,等待我们的下一次输入。让我们试试下面的变量赋值:

>>> x = 10
>>>

在这种情况下,我们没有看到任何输出,因为我们输入的只是一个语句,而不是表达式。但是它确实修改了python外壳的状态。如果我们再次查询x,我们会发现:

>>> x = 10
>>> x
10
>>>

让我们调用其中一个名为help的内置函数。

>>> help(x)

Help on int object:
class int(object)
|  int(x=0) -> int or long
|  int(x, base=10) -> int or long
|
|  Convert a number or string to an integer, or return 0 if no arguments are given
:q

>>>

在任何对象上调用help都会引出一个分页视图,实际上就是对象类的 Javadoc。要退出帮助视图,只需在:提示符下键入q,你将回到>>>提示符下。

对象的完整文档视图可能非常冗长。如果你只想快速浏览一个对象支持哪些属性,使用dir函数。

>>> dir(x)

['__abs__', '__add__', '__and__', '__class__', '__cmp__', '__coerce__', '__delattr__', '__div__', '__divmod__', '__doc__', '__float__', '__floordiv__', '__format__', '__getattribute__', '__getnewargs__', '__hash__', '__hex__', '__index__', '__init__', '__int__', '__invert__', '__long__', '__lshift__', '__mod__', '__mul__', '__neg__', '__new__', '__nonzero__', '__oct__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdiv__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'imag', 'numerator', 'real'] 

>>>

暂时忽略时髦的双下划线,dir(x)返回的实际上是对象上所有可用属性的目录。您可以使用.(点)语法来访问它们中的任何一个。

>>> x.numerator
10
>>> x.denominator
1
>>> x.conjugate
<built-in method conjugate of int object at 0x9e9a274>
>>> x.conjugate()
10

您也可以使用不带任何参数的dir()函数来获取内置列表。

>>> dir()

['__builtins__', '__doc__', '__name__', '__package__']

>>> dir(__builtins__)

['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BufferError', 'BytesWarning', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'None', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'ReferenceError'ror', 'RuntimeError', 'RuntimeWarning', 'StandardError', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__debug__', '__doc__', '__import__', '__name__', '__package__', 'abs', 'all', 'any', 'apply', 'basestring', 'bin', 'bool', 'buffer', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'cmp', 'coerce', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'execfile', 'exit', 'file', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'intern', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'long', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'raw_input', 'reduce', 'reload', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'unichr', 'unicode', 'vars', 'xrange', 'zip']

>>>

这给出了内置的、不需要从其他包中导入的函数和其他对象的列表。这类似于java.lang包中定义的所有内容在 Java 中随处可用,而不必显式导入。

Tip

在 Python 交互式 shell 中进行探索性开发时,dirhelp函数非常有用。

在我们结束这一部分之前,我还想展示最后一件东西。让我们创建一个名为hello.py的新文件,内容如下:

print "Hello There!"
x = 10 + 10
print "The value of x is", x

现在执行python,将该文件作为参数传入:

antrix@dungeon:~$ python hello.py
Hello There!
The value of x is 20
antrix@dungeon:~$

这更符合传统的开发过程:在一个文件中编写代码,然后执行该文件。这也演示了python可执行文件如何在一个进程中结合javacjava的角色。

有了 Python 的简短演示,我们就可以开始探索这种语言的语法了。

摘要

在本章中,我们了解到 Python 不仅仅是一种脚本语言,还是一种有着悠久历史的通用编程语言。然后我们熟悉了python可执行文件,它是javajavac可执行文件的 Python 对应物。

我们还研究了 Python REPL 环境,这是一种交互式测试 Python 的好方法。如果您还没有安装 REPL,我强烈建议您现在就安装,因为下一章我们将深入研究该语言语法的本质细节时会大量用到它!

Footnotes 1

除非MyOtherType恰好是MyType的子类。

2

即使在 Java 8 中引入了 lambdas 之后。

3

http://python-history.blogspot.com/

4

是的,Emacs 也很好。

二、语法

这一章是这本书的核心。这是对 Python 语言特性的深入探究。我用简短的代码片段来解释它们,您可以很容易地自己尝试。

我们首先介绍基本的数据类型和内置的集合,比如字典和集合,特别强调列表。然后,我们将深入函数,发现它们作为一流语言特性的威力。

转到类,我们将发现 Python 作为一种面向对象的语言是多么灵活,尤其是与 Java 相比。然后我们将探索协议,它将语言的语法扩展到您自己的类型。

最后,我们将讨论作为组织 Python 代码的一种方式的模块和包的概念。

如你所见,这将是一个很长的章节。所以拿些咖啡,让我们开始吧!

你好世界

>>> print "Hello World"
Hello World
>>>

嗯,那很简单!继续前进…

Tip

在 Python 3 中,print关键字已经被替换为print()函数。

基本结构

这是一小段 Python 代码,我不需要告诉你它是做什么的,对吧?大多数 Python 代码都是这样的:非常易读,几乎像伪代码一样。

>>> numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]                     ①

>>> odd_numbers = []                                          ②

>>>

>>>
# What are the odds?                                      ③

>>> for num in numbers:                                       ④

...   if num % 2 != 0:                                        ⑤

...     odd_numbers.append(num)                               ⑥

...

>>> print "the odd numbers are:", odd_numbers                 ⑦

the odd numbers are: [1, 3, 5, 7, 9]
>>>

您一定注意到了一些事情,比如缺少分号作为语句分隔符。让我们一次一行地研究这段代码,看看与 Java 相比还有什么新的和不同的地方。

  1. 在第一行中,我们声明了一个numbers列表。列表是 Python 中内置的数据结构之一,还有元组、字典和集合。注意,我们没有声明任何类型,也没有使用任何new关键字来分配列表。
  2. 接下来,我们声明另一个名为odd_numbers的列表,它被初始化为空。
  3. 继续往下,我们会发现一个以#标记开始的注释。注释延伸到行尾,就像在 Java 中使用//标记一样。
  4. 这里,我们遇到了一个for循环,这应该会让您想起 Java 的 foreach 循环的一个更英语的版本。像这个for循环或下一行的if条件中的块范围,使用缩进而不是花括号({..})来表示。仅仅使用空白缩进来定义块可能听起来很奇怪,甚至容易出错!但只要给它一个机会,你会发现它很快成为第二天性。请注意,Python REPL 使用省略号(...)来指示块范围,您不需要键入省略号。接下来的三行以省略号开始,这是这个for循环的范围。
  5. 这一行是一个if语句,除了缺少括号之外,它与 Java 非常相似。forif条件表达式的括号是可选的。只有当它们增加清晰度时才包括它们。除了这里显示的forif构造,Python 还有elifwhile等等。
  6. 这里,我们将当前循环号添加到odd_numbers列表中。和 Python 中的几乎所有东西一样,list 是一个支持多种操作的对象,包括append操作。
  7. 最后,我们用结果来安慰。不用再输入明显更冗长的System.out.println

Caution

千万不要在 Python 源代码中混用制表符和空格。虽然可以使用制表符或空格来表示缩进,但是在同一个源文件中混合使用这两种字符可能会导致解析错误。我的建议是:不要使用制表符,坚持使用空格。将文本编辑器设置为每个制表符插入四个空格字符。

基本类型

Python 中的一些基本数据类型是数字、字符串和集合。

民数记

数字有以下几种。

| 类型 | 示例值 | | :-- | :-- | | `int` | `1000` | | `long` | `1000L` | | `float` | `1000.12` | | `complex` | `1000 + 12j` |

虽然intlong是不同的数据类型,但是实际上,你只需要在声明文字值的时候担心它们;也就是说,字面上的长整型需要用一个L后缀来声明。在算术运算过程中,Python 会根据需要自动将int值转换为long值。这也防止了与溢出相关的错误。

Note

在 Python 3 中,intlong没有区别;只有一种任意长度的整数类型。

用线串

和 Java 一样,Python 中的字符串是不可变的。字符串值可以用单引号或双引号括起来。为了区分普通的 ASCII 字符串和 Unicode 字符串,Python 使用前缀u来表示后者。Unicode 字符串提供了与各种字符集的编码/解码相关的附加操作。

| 类型 | 示例值 | | :-- | :-- | | `str` | `'apple'` | | `unicode` | `u'äþþĺė'` | | `str` | `r'C:\temp'` |

第三种类型的字符串是由前缀r表示的原始字符串。这只是指示 Python 解析器不要对字符串应用任何反斜杠转义规则。这里有一个简单的例子来说明区别。

  1. \t被解释为制表符,导致制表符被打印。
  2. 用一个额外的反斜杠对\t进行转义是有帮助的,但是会使字符串更难阅读。
  3. 现在,\t保持原样,因为前缀r用于将字符串标记为原始字符串。
>>> print 'c:\temp\dir'        ①
c:      emp\dir

>>> print 'c:\\temp\dir'       ②
c:\temp\dir

>>> print r'c:\temp\dir'       ③
c:\temp\dir
>>>

可以想象,原始字符串在表示文件系统路径或正则表达式时非常有用。

Note

在 Python 3 中,默认情况下所有字符串都是unicode。没有编码的字符串被视为没有任何文本语义的字节。

收集

内置的 Python 集合有四种类型。

| 集合类型 | Java 等价物 | 示例值 | | :-- | :-- | :-- | | `list` | `java.util.ArrayList` | `['apple', 'ball', 'ball']` | | `tuple` | `java.util.ArrayList` | `('apple', 'ball', 'ball')` | | `dict` | `java.util.HashMap` | `{'fruit': 'apple', 'toy': 'ball'}` | | `set` | `java.util.HashSet` | `{'apple', 'ball'}` |

这些集合类型中的每一种都提供了一些有用的操作,比如排序、子序列等等。另一个关键属性是,所有这些数据类型都是异构的,可以承载不同数据类型的值。想Collection<Object>而不是Collection<T>

Tip

虽然tuplelist可能看起来相似,但区别在于tuple是不可变的。

将这些基本集合构建到语言语法中非常有用。它使得许多日常代码非常简洁,而没有导入集合 API 及其相关行李的开销。

列表的乐趣

列表是 Python 中最重要的数据结构,当我说掌握列表是掌握 Python 的关键时,我只是稍微夸大了一下。虽然之前我说过他们很像java.util.ArrayList,但他们真的不仅仅是这样。但是首先,让我们看一个简短的例子,演示它们作为基本数组的用法。

  1. 首先要注意的是,numbers列表不是同构的,可以存放不同类型的值,可以是数字、字符串、其他对象甚至其他列表!
  2. 单个元素的访问使用众所周知的数组索引语法:L[index] .
  3. Python 允许为索引传入负值,在这种情况下,它会将列表的长度添加到索引中,并返回相应的元素。
>>> numbers = [0, 1, 2, 'three', 4, 5, 6, 7, 8, 9]    ①

>>> numbers[0]                                        ②

0

>>> numbers[-1]                                       ③

9

除了单个元素访问,Python 列表与众不同的地方在于从列表中提取元素范围的能力。这是使用 slice 语法完成的。事情是这样的。

  1. 切片语法—L[start:stop]—将列表的子序列作为新列表返回。
  2. start索引是可选的,当省略时,默认为0
  3. stop索引也是可选的,默认为列表的长度。
  4. 根据该代码示例第 3 行描述的规则,负的stop索引从末尾开始计数。
  5. 完整的切片语法实际上是L[start:stop:step],其中省略步骤时,默认为1。在这里,我们将它设置为2,它跳过列表中的所有其他元素。
  6. 另一个示例显示了startstop的默认值。
  7. 负的step反转迭代的方向。
>>> numbers[0:4]               ①

[0, 1, 2, 'three']

>>> numbers[:4]                ②

[0, 1, 2, 'three']

>>> numbers[4:]                    ③

[4, 5, 6, 7, 8, 9]

>>> numbers[2:-2]                  ④

[2, 'three', 4, 5, 6, 7]

>>> numbers[0:9:2]                 ⑤

[0, 2, 4, 6, 8]

>>> numbers[::2]                   ⑥

[0, 2, 4, 6, 8]

>>> numbers[::-1]                  ⑦

[9, 8, 7, 6, 5, 4, 'three', 2, 1, 0]

切片表示法返回的副本是浅层副本。我可以在下面的例子中演示这一点:

  1. 创建浅层副本
  2. 这两个列表在逻辑上是相等的。这类似于在 Java 中使用equals()进行比较。
  3. 但是这两个列表不是同一个对象。这类似于在 Java 中使用==进行比较。
  4. 我们可以通过使用id()内置函数检查对象引用来确认。这类似于 Java 中的默认hashCode()
>>> copy = numbers[:]                ①
>>> copy == numbers                  ②
True
>>> copy is numbers                  ③
False
>>> id(copy), id(numbers)            ④
(3065471404L, 3075271788L)

现在让我们把目光转向在列表上执行操作。我们想对列表做的第一件事就是遍历它的元素。Python 中的列表迭代有几种不同的变体,要么使用普通的 foreach 风格语法,要么用enumeraterangelen内置函数对其进行扩充。

>>> numbers = [10, 20, 30]

>>> for number in numbers:

...   print number

...

10
20
30

>>> for index, number in enumerate(numbers):

...   print index, number

...

0 10
1 20
2 30

>>> for  index in range(len(numbers)):

...   print index

...

0
1
2

接下来,让我们看看如何变异列表。

>>> toys = ['bat', 'ball', 'truck']

>>> if 'bat' in toys:

...   print 'Found bat!'

...

Found bat!

>>> toys.append('doll')

>>> print toys
['bat', 'ball', 'truck', 'doll']

>>> toys.remove('ball')
>>> print toys
['bat', 'truck', 'doll']

>>> toys.sort()
>>> print toys
['bat', 'doll', 'truck']

列表也可以用作简单的堆栈和队列。

>>> stack = []
>>> stack.append("event")   # Push
>>> event = stack.pop()       # Pop
>>>
>>> queue = []
>>> queue.append("event")  # Push
>>> event = queue.pop(0)    # Pop from beginning

列表提供了更多的操作,比如extendinsertreverse。但是现在让我们来看看列表最有趣的特征之一:理解。

考虑下面的代码,它计算前几个整数的阶乘:

>>> import math

>>> numbers = range(5)

>>> numbers
[0, 1, 2, 3, 4]

>>> factorials = []

>>> for num in numbers:

...   factorials.append(math.factorial(num))

...

>>> factorials
[1, 1, 2, 6, 24]

使用内置的map函数,前面的程序循环可以被一个功能性的一行程序代替,如下所示:

>>> factorials = map(math.factorial, range(5))

Python 使用语法定义了列表理解:new_list = [function(item) for item in L]。我们可以使用以下语法重写阶乘循环,如下所示:

>>> factorials = [math.factorial(num) for num in range(5)]

Tip

列表理解是最重要的语言特性之一。任何时候你看到或想到一个map(fn, iter),用[fn(x) for x in iter]来表达会更好。

这是另一个在理解中引入条件的变体:

>>> factorials_of_odds = [math.factorial(num) for num in range(10) if num % 2 != 0]

如果被迭代的列表/对象很大(甚至是无界的),那么可以使用一种叫做生成器表达式的列表理解语法的变体。在下面的代码片段中,factorials_of_odds是在迭代时缓慢计算的。

>>> factorials_of_odds = (math.factorial(num) for num in xrange(10**10) if num % 2 != 0)

从语法上来说,列表理解和生成器表达式之间的唯一区别是,前者用方括号括起来,而后者用圆括号括起来。

Aside

在生成器表达式示例中,我使用了函数xrange(10**10)**是指数运算符;也就是说,10**10就是10000000000。通常的range函数,当用10**10作为参数调用时,必须分配并在内存中保存一个 100 亿个元素的列表。xrange没有预先分配这么大的列表,而是返回一个迭代器,只有在迭代时,才会产生多达 100 亿个元素,一次一个。

在对列表进行了冗长的介绍之后,让我们把注意力转向过程化编程的核心构件之一:函数。

功能

Python 中的函数比 Java 中的灵活得多。

  • 它们是一级对象,可以独立存在,不需要包装在类中。它们可以在运行时创建,赋给变量,作为参数传递给其他函数,并作为其他函数的值返回。
  • 除了简单的位置参数列表,Python 函数还支持命名参数、varargs 和基于关键字的 varargs。
  • Python 还支持 lambda 表达式形式的匿名函数,这是 2014 年作为 Java SE 8 版本的一部分添加到 Java 中的一个功能。

Python 中的函数定义如下:

def a_function(arg1, arg2="default", *args, **kwargs):
    """This is a short piece of documentation for this function.

       It can span multiple lines.

    """

    print "arg1:", arg1     # arg1 is a mandatory parameter

    print "arg2:", arg2     # arg2 is an optional parameter with a default value

    print "args:", args     # args is a tuple of positional parameters

    print "kwargs:", kwargs # kwargs is a dictionary of keyword parameters

函数定义以def关键字开始(Hello,Scala 和 Groovy!)后跟括号中的参数列表。同样,没有花括号,只有缩进定义了函数体的范围。

Note

现在,请忽略这个函数的参数声明中argskwargs前面奇怪的星号前缀。这是一种特殊的语法,我将在下一节描述。

三重引号内的文档称为 docstring,类似于 Javadoc。调用help(a_function)显示这个文档字符串。

>>> help(a_function)
Help on function a_function in module __main__:

a_function(arg1, arg2="default", *args, **kwargs)
    This is a short piece of documentation for this function.
    It can span multiple lines.
(END)

我们不声明参数的类型,而是依赖 duck 类型;也就是说,只要参数变量具有函数期望操作的属性,我们就不关心它的真实类型。

Aside

Wikipedia 对 duck typing 有一个很好的、简洁的解释:“一种类型,其中对象的方法和属性决定有效的语义,而不是它从特定类的继承或显式接口的实现。”这个概念的名字是指鸭子测试,归功于詹姆斯·惠特科姆·莱利,其措辞如下:“当我看到一只像鸭子一样走路、像鸭子一样游泳、像鸭子一样嘎嘎叫的鸟时,我就把它叫做鸭子。”

在 duck typing 中,程序员只关心确保对象在给定的上下文中按照要求运行,而不是确保它们是特定的类型。例如,在一种非 duck 类型的语言中,你可以创建一个函数,要求传递给它的对象是类型Duck的,以确保该函数可以使用该对象的walkquack方法。在 duck-typed 语言中,函数可以接受任何类型的对象,并简单地调用它的walkquack方法,如果没有定义它们,就会产生运行时错误。

让我们看看用不同的参数值调用a_function(如前所述)时的行为。

  1. 仅提供arg1;其他参数被初始化为默认值。
  2. 提供了位置参数。
  3. 这就像 Java 的 varargs。所有没有在参数列表中明确声明的位置变量都被填充到args元组中。
  4. 这演示了关键字或命名参数的用法。
  5. 当参数名显式时,顺序并不重要。
>>> a_function(10)                            ①

arg1: 10
arg2: default
args: ()
kwargs: {}

>>> a_function(10, "ten")                                ②

arg1: 10
arg2: ten
args: ()
kwargs: {}

>>> a_function(10, 20, 30, 40)                           ③

arg1: 10
arg2: 20
args: (30, 40)
kwargs: {}

>>> a_function(10, "twenty", arg3=30, arg4="forty")      ④

arg1: 10
arg2: twenty
args: ()
kwargs: {'arg3': 30, 'arg4': 'forty'}

>>> a_function(arg2="twenty", arg1=10, arg3=30, arg4="forty")                                                 ⑤

arg1: 10
arg2: twenty
args: ()
kwargs: {'arg3': 30, 'arg4': 'forty'}

函数和元组

Python 函数还有一个锦囊妙计:支持多个返回值!

def multi_return():
    # These are automatically wrapped up

    # and returned in one tuple

    return 10, 20, 'thirty'

>>> values = multi_return()
>>> print values
(10, 20, 'thirty')

当一个函数返回多个逗号分隔的值时,Python 会自动将它们包装成一个元组数据结构,并将该元组返回给调用者。这是一个称为自动元组打包的特性。您可以通过自己将返回值包装在一个元组中来使这种包装更加明确,但这不是必需的,也不鼓励这样做。

真正有趣的部分出现在这个特性与它的对应物自动元组解包相结合的时候。以下是它的工作原理:

  1. 在这里,numbers只是一个普通的元组。
  2. 赋值右边的元组被解包到左边的变量中。
  3. multi_retur n 返回的元组被解包到左边的变量中。
>>> numbers = (1, 2, 3)              ①

>>> print numbers
(1, 2, 3)

>>> a, b, c = (1, 2, 3)              ②

>>> print a, b, c
1 2 3

>>> a, b, c = multi_return()    ③

>>> print a, b, c
10 20 thirty

首先,Python 将来自multi_return的多个返回值打包成一个元组。然后,它透明地解包返回的元组,并将包含的值赋给赋值左边的相应变量。

为此,左边的变量数量必须与被调用函数返回的元素数量相匹配;否则,将引发一个错误。

>>> a, b = multi_return()
ValueError: too many values to unpack

现在你知道了元组打包和解包是如何工作的,让我们重温一下我们在上一节中遇到的*args**kwargs中奇怪的星号。前面的单星号是 Python 符号,用于解包元组值,而前面的双星号用于解包 dict 值。这里有一个例子来说明这一点:

def ternary(a, b, c):
    print a, b, c

>>> ternary(1, 2, 3)
1 2 3

>>> args = (1, 2, 3)

>>> ternary(args)
TypeError: ternary() takes exactly 3 arguments (1 given)

>>> ternary(*args)  # Unpacks the args tuple before function call

1 2 3

>>> kwargs = {'a': 1, 'b': 2, 'c': 3}

>>> ternary(kwargs)
TypeError: ternary() takes exactly 3 arguments (1 given)

>>> ternary(**kwargs) # unpacks the dictionary before function call

1 2 3

函数内部的函数

现在您已经熟悉了基本的函数定义语法,让我们来看一个更高级的例子。考虑以下函数:

def make_function(parity):                                    ①

    """Returns a function that filters out `odd` or `even`

       numbers depending on the provided `parity`.

    """

    if parity == 'even':
        matches_parity = lambda x: x % 2 == 0                 ②

    elif parity == 'odd':
        matches_parity = lambda x: x % 2 != 0
    else:
        raise AttributeError("Unknown Parity: " + parity)     ③

    def get_by_parity(numbers):                               ④

        filtered = [num for num in numbers if matches_parity(num)]
        return filtered

    return get_by_parity                                      ⑤

#

这里有很多东西需要消化!我们一行一行来。

  1. 这里,我们从 docstring 开始定义一个名为make_function的函数。
  2. 接下来,我们使用lambda关键字来定义一个单行的匿名函数,并将其分配给matches_parity。分配给matches_parity的λ函数取决于parity函数参数的值。
  3. 如果parameter参数值既不是odd也不是even,我们会引发内置的AttributeError异常。
  4. 我们现在在封闭函数体内定义一个get_by_parity函数。您会注意到这里使用了matches_parity的值。这是一个终结。这类似于在 Java 中从匿名类声明的封闭范围中捕获 final 字段。事实上,Java 8 中的 lambda 功能比 Java 匿名类更接近于这个 Python 特性。
  5. 最后,我们从make_function返回get_by_parity函数对象。

Python 中的函数是类型function的一级对象。它们可以被传递和赋给变量,就像任何其他对象一样。在这种情况下,当有人调用make_function时,它返回另一个函数,该函数的定义取决于传递给make_function的参数。让我们通过一个简单的例子来看看这是如何工作的。

  1. 我们用odd作为parity参数值来调用make_function,它返回给我们一个我们分配给get_odds变量的函数。
  2. 现在,出于所有实际目的,get_odds只是另一个函数。我们通过传入一个数字列表来调用它(range(10)返回一个 0 列表)..10)然后出来一个奇数的过滤列表。
  3. 我们可以对even奇偶校验重复这个练习,并验证make_function是否按预期工作。
>>> get_odds = make_function('odd')            ①

>>> print get_odds(range(10))                  ②

[1, 3, 5, 7, 9]

>>> get_evens = make_function('even')          ③

>>> print get_evens(range(10))
[0, 2, 4, 6, 8]

Tip

“作为一级对象的函数”是一个需要理解的强有力的想法,并且需要改变你的程序结构。从 Java 背景到 Python,您必须学会抵制将一切都建模为类的冲动。毕竟,不是所有的东西都是名词,有些东西最好用动词来描述! 1

使用函数和 Python 内置的数据结构,如列表和字典,可以完成很多工作。这样做,你会发现你的程序往往变得更简单、更容易理解。

班级

Python 中的一切都是对象,正如你所料,创建对象的方法是从类开始。考虑下面简单的Person类的定义。

class Person(object):
    def __init__(self, first, last):
        self.first = first
        self.last = last

    def full_name(self):
        return "%s %s" % (self.first, self.last)

    def __str__(self):
        return "Person: " + self.full_name()

与 Java 一样,object位于类层次结构的根,但与 Java 不同,它需要在 Python 中显式指定(尽管在 Python 3 中没有)。 2 继承声明不使用像 extends 这样的特殊关键字。相反,父类名被括在声明类名后面的括号中。

__init__方法是初始化器方法,类似于 Java 类构造函数。还有一个名为__new__的构造函数方法,但是除非你正在进行元编程,比如编写工厂类等等,否则你不会使用它。在__init__中,所有在 Python 中被称为属性的实例字段都被初始化。请注意,我们不必预先声明该类的所有属性。

所有实例方法的第一个参数是self。在 Java 中是隐式的,但在 Python 中是显式的。注意字面上的名字self不是强制性的;你可以随便给它起什么名字。如果你把它命名为current,那么full_name将被定义为:

# `current` instead of the conventional `self`

def full_name(current):
    return "%s %s" % (current.first, current.last)

Aside

我在full_name方法定义中偷偷加入了一个字符串插值的例子。Python 的字符串插值作用于元组参数,类似于 Java 的String.format(s, arg...)。还有另一种处理命名参数并接受字典参数的变体:

>>>
"The name is %(last)s, %(first)s %(last)s" % {'first': 'James', 'last': 'Bond'}

'The name is Bond, James Bond'

__init__方法名中使用的双下划线符号是声明特殊方法的 Python 约定。__str__是另一种特殊的方法。它的行为与 Java 中的toString()方法完全一样。当我们谈到协议时,我将解释这些方法的特殊之处。

下面是这个Person类的一些用法示例。

  1. 对象创建就像在 Java 中一样,只是不需要使用new关键字。
  2. print相当于System.out.println(),它将调用参数的__str__方法,就像后者调用toString()一样。
  3. 该类的字段在 Python 中称为属性,使用带点的语法进行访问。
  4. 方法也是使用带点的语法来访问的。虽然self在方法定义期间是显式的,但是当在对象上调用该方法时,它是隐式传递的。
  5. 但是您可以通过从类中调用方法并传入实例来使其显式化。Java 里做不到!
>>> person = Person('Clark', 'Kent')  ①

>>> print person                      ②

Person: Clark Kent

>>> print person.first                ③

Clark

>>> print person.full_name()          ④

Clark Kent

>>> print Person.full_name(person)    ⑤

Clark Kent

你会注意到我们没有声明我们的字段是私有的还是公共的。我们只是访问它们,就好像它们是公共的一样。其实 Python 根本没有可见性的概念!一切都是公开的。如果您希望向您的类的用户表明某个特定的属性或方法是一个内部实现细节,那么约定是在属性/方法名称前面加上一个下划线,这样使用代码的人就会知道要小心行事。

遗产

Python 支持单一继承,也支持多重继承;也就是说,继承模型比 Java 更接近 C++。对于多重继承,当在类层次结构中的多个位置声明方法时,总是存在如何解决方法的问题。在 Python 中,方法解析顺序通常是深度优先。可以检查类属性__mro__,以检查用于该类的实际方法解析顺序。

这里有一个SuperHero类,它扩展了我们之前定义的Person类。我们在SuperHero类中添加了一个新属性nick和一个新方法nick_name

class SuperHero(Person):
    def __init__(self, first, last, nick):
        super(SuperHero, self).__init__(first, last)
        self.nick = nick

    def nick_name(self):
        return "I am %s" % self.nick

像在 Java 中一样工作,但是再次强调,你需要明确它应该从哪个类开始向上爬。让我们看看SuperHero在几个例子中的表现。

  1. 内置的type()函数给出任何对象的类型。一个class对象的类型是type。您在类名中看到的__main__只是 Python 放置对象的默认名称空间。您将在“组织代码”一节中了解更多关于名称空间的内容。
  2. isinstance()内置函数是 Java 的instanceof操作符和Class.isInstance()方法的 Python 对应物。
  3. 类似地,obj.__class__属性就像 Java 的obj.class字段。
>>> p = SuperHero("Clark", "Kent", "Superman")

>>> p.nick_name()
I am Superman

>>> p.full_name()
'Clark Kent'

>>> type(p)                              ①

<class '__main__.SuperHero'>

>>> type(p) is SuperHero
True

>>> type(type(p))
<type 'type'>

>>> isinstance(p, SuperHero)             ②

True

>>> isinstance(p, Person)
True

>>> issubclass(p.__class__, Person)      ③

True

多态性

让我们来看一个用来演示多态行为的典型例子——形状。

class Square(object):
    def draw(self, canvas):
        ...

class Circle(object):
    def draw(self, canvas):
        ...

给定这两个SquareCircle类,您内部的 Java 开发人员可能已经在考虑提取一个定义draw(canvas)方法的Shape类或接口。忍住冲动!因为 Python 是动态的,所以下面的代码不需要显式的Shape类也能很好地工作:

shapes = [Square(), Circle()]

for shape in shapes:
    shape.draw(canvas)

拥有一个定义draw(canvas)的公共Shape基类并没有真正的优势,因为没有静态类型检查来强制执行。如果shapes列表中的对象没有实现draw(canvas),你会在运行时发现这一点。简而言之,对共享行为使用继承,而不是多态性。

变得有活力!

到目前为止,我们所看到的 Python 中的类是相当平淡的。在 Java 中没有什么是你做不到的。是时候让它变得有趣了!请考虑以下几点:

  1. 我们从SuperHero类的一个实例开始。
  2. 接下来,我们定义一个名为get_last_first()的新顶级函数。
  3. 然后,我们将get_last_first()函数的引用分配给 Person 类的一个名为last_first的新属性。
  4. 由于上一步,所有的Person类的实例,包括派生类的实例,现在都有了一个新的方法。
>>> p = SuperHero("Clark", "Kent", "Superman")     ①

>>> def get_last_first(self):                      ②

...   return "
%s
, %s
" % (self.last, self.first)

...

>>> Person.last_first = get_last_first               ③

>>> print p.last_first()                           ④

Kent, Clark

总而言之,我们在这里所做的是将一个新函数作为实例方法绑定到Person类。一旦绑定,该方法对所有的Person类实例都可用,包括那些已经创建的实例!

这种技术也可以用于为现有方法定义新的实现。这样做通常被称为 monkey patching,并且在 Python 社区中通常不被允许,因为它很容易导致令人惊讶和意想不到的行为。

既然我们已经看到了如何在事后向类中添加行为,我们能不能反过来移除行为呢?当然可以!

>>> print p.last
Kent
>>> del p.last
>>> print p.last
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  AttributeError: 'SuperHero' object has no attribute 'last'

>>> del Person.full_name
>>> p.full_name()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  AttributeError: 'SuperHero' object has no attribute 'full_name'

>>>

因为 Python 是动态类型的,所以访问不存在的字段和方法会在运行时导致异常。反过来,我们可以在运行时定义新的属性!

class Person(object):
   ...

   def __getattr__(self, item):
       # This special method is called when normal attribute lookup fails

       if item is 'hyphenated_name':
           return lambda x: "%s-%s" % (x.first, x.last)
       else raise AttributeError(item)

>>> p = Person('Clark', 'Kent')
>>> p.hyphenated_name()

'Clark-Kent'

想象一下,为了在 Java 中达到同样的效果,您需要做多少字节码重写的工作!举个例子,想想 Java 模拟库被迫经历的代码体操。在 Python 中,使用__getattr____setattr__来实现模拟是微不足道的。

协议

协议类似于 Java 接口,因为它们定义了一个或多个提供特定行为的方法。然而,与 Java 接口不同,协议没有在源代码中明确定义。最接近的对等物是 Java 中的equals()hashcode()toString()方法。这些方法不是任何显式接口的一部分。然而,我们有一个隐含的约定 3 这些方法将在某些情况下被调用。Python 协议也是如此。

Python 语言定义了几种不同的协议,比如序列、数字、容器等等。这些协议通过语言语法中的特殊语法支持来体现自己。几乎 Python 中的每种语言语法都是根据协议实现的,因此,通过实现相关的协议,可以使它们与您自己的类型一起工作。

让我用一个协议,容器协议作为例子来进一步解释这一点。考虑下面的OrderRepository类定义,它提供对数据库支持的Order对象集合的访问。

Caution

请不要用这个例子作为生产代码的基础!它很容易受到 SQL 注入攻击。对于关系数据库访问,考虑使用第三章中讨论的 SQLAlchemy 库。

 class OrderRepository(object):
    ...
    def __contains__(self, key):
        return 1 == db.query("select count(1) from Orders where id='%s'" % key)

    def __getitem__(self, key):
        return Order(db.query("select * from Orders where id='%s'" % key))

    def __setitem__(self, key, value):
        d = value.as_dict()
        update_params = ", ".join( ["%s = '%s'" % x for x in d.iteritems()] )
        db.update("update Orders set %s where id='%s'" % (update_params, key)

我省略了完整的类定义,只展示了容器协议中的三个方法。既然现在可以说OrderRepository实现了容器协议,它允许我们以如下方式使用它:

  1. 因为我们已经为OrderRepository实现了__contains__方法,所以我们现在可以使用if x in y语法对其进行操作。幕后发生的事情是 Python 正在将那个if语句翻译成if orders.__contains__("orderId123").
  2. 类似地,__getitem__方法使用订单 id 解锁字典,将键查找转换为orders.__getitem__("orderId123")
  3. 最后,类似字典的赋值通过__setitem__方法调用工作。
>>> orders = OrderRepository(db)

>>> if "orderId123" in orders:           ①

...     order = orders["orderId123"]     ②

...     order.status = "shipped"

...     orders["orderId123"] = order     ③

>>>

您可以认为这是操作符重载或语法糖,只要符合您的心理模型就行!

表 2-1 列出了 Python 支持的一些其他协议以及它们支持的语法。

表 2-1

Protocols in Python

| 草案 | 方法 | 支持语法 | | :-- | :-- | :-- | | 顺序 | 在`__getitem__`中支持切片`,`等。 | `seq[1:2]` | | 迭代程序 | `__iter__` `, next` | `for x in collection:` | | 比较 | `__eq__``, __gt__``, __lt__` | `x == y``, x > y``, x < y` | | 数字的 | `__add__``, __sub__``, __and__` | `x + y``, x - y``, x & y` | | 字符串状 | `__str__``, __unicode__` | `print x` | | 属性访问 | `__getattr__` `, __setattr__` | `obj.attr` | | 上下文管理器 | `__enter__` `, __exit__` | `with open('out.txt') as f: f.read()` |

组织代码

Python 中源代码组织的最基本单位是模块,它只是一个.py文件,里面有 Python 代码。该代码可以是函数、类、语句或它们的任意组合。几个模块可以一起收集到一个包中。Python 包就像 Java 包一样,只有一点不同:包对应的目录必须包含名为__init__.py的初始化器文件。这个文件可以是空的,也可以选择包含一些在首次导入包时执行的引导代码。

假设我们有一个代码库,如下所示:

 .
 |-- cart.py
 |-- db
 |   |-- __init__.py
 |   |-- mysql.py
 |   +-- postgresql.py
 +-- model
     |-- __init__.py
     +-- order.py

给定这个目录列表,我们可以看到有两个包:dbmodel。共有四个模块:cartmysqlpostgresqlorder

导入代码

使用import语句将一个文件(或 Python 术语中的模块)中定义的代码导入到另一个文件中。import语句的工作方式非常类似于它在 Java 中的工作方式:它将声明从目标模块带入当前的名称空间。

import语句有两种语法变体。第一个是熟悉的 Java 风格,import ...,而第二个遵循from ... import ...模式。

假设我们在order模块中定义了一个名为SellOrder的类;也就是在order.py文件里面:

$ cat model/order.py

class SellOrder(object):
    ...
    ...

在我们的主应用程序cart.py中,有几种不同的方法可以导入和使用这个类。

import model

sell_order = model.order.SellOrder()

在这个例子中,我们使用import <package|module>语法将目标包— model 导入到当前名称空间中,然后使用点标记法到达我们的SellOrder类。我们可以使用相同的语法来导入特定的模块,而不是包含的包:

import model.order

sell_order = order.SellOrder()

这里我们直接导入了order模块。请注意import语法在 Java 和 Python 中的工作方式之间的区别。在 Java 中,我们总是从我们的包层次结构中导入一个类。在 Python 中,import ...语句只能用于导入包或模块。如果你想访问一个类或函数的定义,你必须通过包含它的模块来引用它。或者使用备用语法,from <package|module> import <item>:

from model.order import SellOrder

sell_order = SellOrder()

这里,我们使用直接将SellOrder导入当前名称空间的from <package|module> import <item>语法变体。这种类型的import可以用于从源模块中导入任何顶级项目,无论是函数、类还是变量定义。

Python 提供了比 Java 导入更多的增强:使用as关键字重命名导入的能力。

from model.order import TYPES as ORDER_TYPES
from db import TYPES as DATABASE_TYPES

print ORDER_TYPES

# ['buy', 'sell']

print DATABASE_TYPES

# ['mysql', 'postgresql']

可以想象,当试图避免名称空间冲突而不必像在 Java 中那样使用完整的包/模块层次结构来消除歧义时,这个特性非常方便。

Tip

在 Java 中,每个.java文件必须只包含顶级的类或接口声明。此外,在典型的使用中,每个这样的文件只定义了一个公共类或接口。Python 模块没有这样的限制。创建只有函数或类或者两者混合的模块是非常好的。不要限制自己每个模块只有一个类或函数定义。这不仅是不必要的,而且被认为是一种不好的做法。相反,努力将概念相似的结构收集到一个模块中。

main()方法

既然我们已经将应用程序的源代码组织到多个文件中,您一定想知道,什么定义了应用程序的入口点?作为 Java 程序员,public static void main是永远烙进我们大脑的!Python 中的对等词是什么?

Python 中没有应用程序入口点的正式概念。而是 Python 执行一个文件的时候;例如,当您运行python foo.py时,执行从文件的开头开始,所有在顶层定义的语句都按顺序执行,直到我们到达文件的结尾。考虑一个名为odds.py的文件,它包含以下代码:

# odds.py

def get_odds(numbers):
    odds = [n for n in numbers if n % 2 != 0]
    return odds

odds_until = 10

numbers = range(odds_until)

print get_odds(numbers)

当通过从您最喜欢的操作系统 shell 运行python odds.py来执行该文件时,Python 解释器从顶部开始,向下运行该文件,直到找到get_odds函数的定义。它通过将该函数的名称添加到当前名称空间来记录该函数。它通过在为每个名称空间维护的查找表中创建一个条目来做到这一点。一旦将get_odds添加到当前名称空间的查找表中,Python 就会跳过函数声明的其余部分,因为函数体中的语句不在顶层。

沿着文件往下走,Python 遇到了对变量odds_until的声明,并执行该语句,将值10赋给它。再次,在当前名称空间的查找表中为odds_until变量创建一个条目。

在下一行,它遇到了一个赋值语句,该语句包含一个名为range的函数。Python 在当前名称空间中查找这个函数,在这个名称空间中它找不到它。然后,它在内置名称空间中查找它,并在那里找到了它。回想一下,内置名称空间相当于java.lang.*——这里定义的东西不必显式导入。找到range函数后,它调用该函数将返回值赋给numbers。正如您现在所猜测的,在当前名称空间中为numbers创建了另一个条目。

继续前进,我们到达文件的最后一行,这里有一个以numbers为参数的对get_odds的调用。由于这两个名字在当前名称空间中都有条目,Python 可以毫不费力地用数字列表调用get_odds。只有在这个时间点,才会解析和执行get_odds函数体。然后返回值被提供给print,它将返回值写到控制台,如下所示:

$ python odds.py
[1, 3, 5, 7, 9]
$

了解了 Python 如何执行脚本后,我们可以尝试并模拟一个 main 方法,如下所示:

# odds.py

def get_odds(numbers):
    odds = [n for n in numbers if n % 2 != 0]
    return odds

def main():
    odds_until = 10
    numbers = range(odds_until)
    print get_odds(numbers)

main()

我们在这里所做的就是将所有的顶级语句打包成一个函数,我们方便地给这个函数命名为main!我们在文件的末尾调用这个函数,实际上使main成为我们应用程序的入口点。

让我们通过关注main的参数来完成类似 Java 的main方法的实现;也就是public static void main(String[] args)中的args。在 Java 中,启动时传递给应用程序的所有命令行参数都将被填充到args数组中。在 Python 中,使用内置的sys标准库模块可以获得这些信息。这个模块定义了sys.argv,它是启动时传递给 Python 脚本的命令行参数列表。列表中的第一个值sys.argv[0]是脚本本身的名称。该列表中的其余项目是命令行参数(如果有的话)。

让我们修改我们的odds.py脚本来获取数字,在此之前,我们应该打印奇数作为命令行参数,我们使用sys模块来检索它。

# odds.py

def get_odds(numbers):
    odds = [n for n in numbers if n % 2 != 0]
    return odds

def main(args):
    try:
        odds_until = int(args[1])
    except:
        print "Usage: %s <number>" % sys.argv[0]
        sys.exit(1)

    numbers = range(odds_until)
    print get_odds(numbers)

import sys
main(sys.argv)

在这个修改过的odds.py中,我们使用命令行参数列表作为参数来调用main函数。在main中,我们使用第一个命令行参数初始化odds_until变量。如果由于任何原因失败,我们会在退出之前打印一条关于如何使用脚本的有用消息,并显示一个1错误代码。下面是这个修改后的示例在实践中的工作方式:

$ python odds.py
Usage: odds.py <number>
$ python odds.py abc
Usage: odds.py <number>
$ python odds.py 15
[1, 3, 5, 7, 9, 11, 13]
$

最后,我们有了一个像 Java 一样工作的主函数!它甚至看起来像 Java 中的 main 方法;一旦所有类型相关的声明都被删除,def main(args)或多或少与public static void main(String[] args)相同。

然而,这里有一个问题我想谈谈。想象一下,我们发现我们的get_odds函数非常有用,以至于我们想在项目代码库中的其他地方将它用作一个实用函数。既然我们刚刚讨论了模块,那么显而易见的方法就是将odds.py作为一个模块,并在我们发现使用这个实用函数的地方导入这个模块。例如,在demo.py:

# demo.py

import odds

print odds.get_odds(range(10))

当我们运行这个demo.py脚本时,我们期望它导入odds模块,然后使用其中定义的get_odds打印奇数直到十。相反,事情是这样的:

$ python demo.py
Usage: demo.py <number>
$

太奇怪了。为什么我们会收到这条信息?我们将10作为参数传递给demo.py中的get_odds。它期待的又是哪一只<number>?事实上,即使消息说“用法:demo.py”,这个用法消息看起来非常像我们在odds.py中定义的那个。

以下是实际发生的情况。当 Python 导入一个模块时,它实际上执行该模块,就像该模块作为脚本运行一样!在执行demo.py的过程中,当 Python 遇到import odds语句时,它首先尝试定位odds.py文件。找到它后,Python 执行整个文件,正如我在前面的讨论中所描述的那样。具体来说,它执行下面这段odds.py中的顶层代码:

import sys

main(sys.argv)

由于在执行demo.py期间没有提供命令行参数,因此sys.argv[1]的值缺失。这在main()中引发了一个异常,导致使用消息被打印出来。此外,由于在这种情况下执行的实际命令是python demo.pysys.argv[0]的值是demo.py而不是odds.py。这解释了输出消息。

要将odds用作一个模块,我们必须从其中移除所有顶级语句。其实这是很重要的一点!

Caution

除非你有一个非常好的理由,否则不要在你的模块中定义任何副作用——导致顶级语句。如果您这样做了,每当您的模块被导入时,这些语句就会被执行,从而导致各种各样的麻烦。

下面是一个修改过的odds.py,去掉了所有的顶级代码:

# odds.py

def get_odds(numbers):
    odds = [n for n in numbers if n % 2 != 0]
    return odds

现在,运行demo.py脚本产生预期的输出。

$ python demo.py
[1, 3, 5, 7, 9]

做出这一改变后,虽然我们获得了将odds用作模块的能力,但我们失去了将它作为脚本运行的能力。如果我们能两者兼得不是很好吗?即使在 Java 领域,仅仅因为一个类定义了一个main方法并不意味着它不能被导入并在其他地方作为一个香草类使用!

为了实现这种模块/脚本二元性,我们必须稍微深入一点名称空间的概念。

在这一章中,我已经多次提到名称空间这个术语,但没有更详细地定义它。在计算机科学术语中,我们将名称空间理解为独立的上下文或名称容器。名称空间允许我们将逻辑事物组合在一起,并允许在不引起冲突的情况下重用名称。

在程序执行期间,每当导入一个模块时,Python 都会为它创建一个新的名称空间。模块的名称用作命名空间的名称。因此,像odds.get_odds(...)这样的一段代码所做的就是调用odds名称空间中的get_odds函数。如果省略了名称空间限定符,那么在当前名称空间中查找对象,否则在内置名称空间中查找。

在运行时,您可以通过引用特殊的__name__变量来访问封装代码的名称空间。这个变量总是绑定到当前的名称空间。让我们通过修改我们的demo.pyodds.py脚本来看一个__name__的例子。

# odds.py

print "In odds, __name__ is", __name__

def get_odds(numbers):
    odds = [n for n in numbers if n % 2 != 0]
    return odds

# demo.py

import

odds

print "In demo, __name__ is", __name__

print odds.get_odds(range(10))

现在,当我们运行演示脚本时,我们会看到以下输出:

$ python demo.py
In odds, __name__ is odds
In demo, __name__ is __main__
[1, 3, 5, 7, 9]

正如我们刚刚讨论的,在导入时,odds模块被绑定到一个同名的名称空间;也就是odds。因此,在odds.py内的代码上下文中,__name__变量的值是odds。然而,对于demo.py ,中的代码,我们看到__name__的值被奇怪地命名为__main__。这是 Python 分配给应用程序主上下文的一个特殊名称空间。换句话说,应用程序的入口点(通常是 Python 执行的脚本)被赋予了名称空间__main__

我们可以将这些知识用于实现脚本/模块二元性。

这又是我们的odds.py文件,其形式可以作为 Python 脚本直接执行,但不能作为模块导入。

# odds.py

def get_odds(numbers):
    odds = [n for n in numbers if n % 2 != 0]
    return odds

def main(args):
    try:
        odds_until = int(args[1])
    except:
        print "Usage: %s <number>" % sys.argv[0]
        sys.exit(1)

    numbers = range(odds_until)
    print get_odds(numbers)

import sys

main(sys.argv)

我们将把顶层代码放在当前名称空间的检查后面:

# odds.py

def get_odds(numbers):
    odds = [n for n in numbers if n % 2 != 0]
    return odds

def main(args):
    try:
        odds_until = int(args[1])
    except:
        print "Usage: %s <number>" % sys.argv[0]
        sys.exit(1)

    numbers = range(odds_until)
    print get_odds(numbers)

if __name__ == '__main__':
    import sys

    main(sys.argv)

如您所见,我们采用了导致顶层代码的副作用,即调用main()函数,并将其置于检查当前名称空间是否为__main__的条件之后。当odds.py作为脚本运行时;也就是说,它是应用程序的入口点,__name__的值将是__main__。因此,我们将进入条件块并运行这段代码。另一方面,当odds.py作为模块导入时,__name__的值将是odds而不是__main__。因此,跳过条件后面的代码块。

随着您阅读越来越多的 Python 代码,您会一直遇到if __name__ == '__main__'构造。这是 Python 程序中用来获得 main 方法效果的标准习惯用法。Python 的创造者吉多·范·罗苏姆写了一篇很好的博文,讲述如何编写地道的 Python main()函数 4 ,这篇博文将这一想法推进了一步。

摘要

我们在这一章中涉及了相当多的内容。我们从 Python 提供的基本语法和内置类型开始。然后,我们继续学习函数和类的构建模块。然后我们熟悉了 Python 的隐式接口;也就是协议。最后,我们学习了如何将源代码组织成模块和包。

虽然这本书在这里结束了我们对 Python 语言语法的讨论,但是 Python 还有更多内容!我们没有讨论的一些高级主题包括 decorators、properties、generators、context managers 和 I/O。为了帮助你学习,我在本书末尾的“参考资料”部分收集了一些有用的资源。

Footnotes 1

参见史蒂夫-耶格。博格斯波特。名词王国的处决。html

2

在 Python 2 中,你可以跳过将 object 指定为基类,但是它会有一些含义,如新样式类 - https://www.python.org/doc/newstyle/ 中所解释的

3

Java 语言规范中记录的约定,但仍然是约定;不是源代码级别的协定。

4

http://www.artima.com/weblogs/viewpost.jsp?thread=4829

三、生态系统

作为软件开发人员,我们知道在评估一种编程语言时,我们不仅需要考虑核心语言本身,还需要考虑该语言的库和工具的生态系统。这个生态系统的丰富性通常决定了你用这种语言编写软件时的生产力。

在这一章中,我描述了 Python 生态系统,并分享了一些比较流行的工具和框架,帮助你开始 Python 开发。

丰富的生态系统

在 Java 世界中,我们知道 Java 不仅仅是语言本身。有 JVM,Java 语言运行时允许在各种硬件和操作系统目标上部署应用程序。有 Java SE 和 EE 标准库,它们提供了许多现成的有用功能。当然,有非常丰富的第三方库和框架可供选择。正是这种生态系统的力量使得 Java 成为一个很好的构建平台。

Python 也是如此!

Python 也可以部署在不同的硬件目标上,比如 x86、ARM 和 MIPS 在 Linux、Mac OS X、Windows 和 Solaris 等多种操作系统上。它甚至可以部署在其他软件运行时上,如。NET CLR(IronPython1)或者 Java 虚拟机(Jython 2 )。

Python 有一个很棒的标准库,提供了很多现成的功能。“包含电池”经常被用来描述标准库的丰富性。

除了标准库,第三方库和框架也同样丰富,从数字和科学计算包,到 NLP,再到网络、GUI 和 web 框架。你很难找到一个还没有 Python 库的领域。

为了帮助你开始,我在表 3-1 和 3-2 中整理了几个列表,帮助你挑选出你已经熟悉的 Java 工具和库的 Python 对应物。这些列表并不详尽,但我相信它们代表了当今最流行的选择。

流行工具

在 Python 世界中,第三方库的安装通常是使用名为pip的工具来完成的。它可以从标准化的requirements.txt文件中读取您的项目所需的第三方包依赖项,然后根据需要安装/升级包。如果需要,它甚至可以从一个名为 PyPI 的中央存储库下载包,PyPI 是 Python 包索引的缩写。这是 Python 世界中相当于 Maven 的中央存储库。

表 3-1

Popular Build and Execution Tools

| 爪哇 | 计算机编程语言 | | :-- | :-- | | build.xml/pom.xml | requirements.txt | | 专家 | 点 | | Maven 中央存储库 | 好吧 | | 类路径 | 皮顿路径 | | 热点 | CPython 先生 |

第三方包带来了它们在运行时如何定位的问题。Java 的解决方案是类路径,Python 的对等物是PYTHONPATH。从概念上讲,它们是相似的,因为两者都指定了一个位置列表,语言运行库应该在该列表中搜索要导入的包。就实现方式而言,它们略有不同。PYTHONPATH设置为环境变量;例如,PYTHONPATH=/path/to/foo:/path/to/bar,一个类似于典型系统 shell 的PATH变量的语法。Python 提供的另一个灵活性是,这个库搜索路径可以在运行时修改,只需操作内置系统模块公开的sys.path属性。正如您可能已经猜到的,sys.path是从为PYTHONPATH环境变量设置的值初始化的。

我之前提到过 Python 语言有多种运行时实现。规范的实现是 CPython,这样命名是因为语言运行时的核心是用 c 实现的。它是使用最广泛的实现,被视为 Python 语言的参考。可选的实现包括 Jython,它是运行在 JVM 上的 PythonIronPython 是运行在。NET 公共语言运行库;PyPy 是即将推出的 Python 实现,它提供了许多改进,如运行时 JIT 编译器、安全沙箱和基于绿色线程的并发性。

作为一名 Java 开发人员,您可能希望更多地了解 Jython。因为它本质上是被翻译成 Java 字节码的 Python 代码,并且运行在 Java 虚拟机中,所以它允许 Python 和 Java 之间的简单互操作。例如,您可以用 Python 编写高级业务规则,并调用 Java 规则引擎来执行这些规则。

流行的框架

因为 Python 是一种动态语言,所以有一类编程错误会在运行时暴露出来,而不是像静态类型语言那样在编译时被捕获。因此,为您的 Python 程序编写自动化测试甚至比在 Java 中更重要。Python 标准库附带了一个创造性地命名为 unittest 的单元测试框架,这应该是一个良好的开端。如果您发现 unittest 缺少一些您喜欢的特性,或者使用起来有点麻烦,那么 pytest 是一个很好的第三方替代品。

表 3-2

Popular Development and Testing Frameworks

| 爪哇 | 计算机编程语言 | | :-- | :-- | | 单元测试 | 单元测试/pytest | | 莫基托 | 单元测试,mock | | FindBugs/检查样式 | 派林特 | | 文档 | 狮身人面像 | | 摇摆 | PyQT/PyGTK | | Spring Boot | 决哥/弗拉斯克 | | 冬眠 | sqllcemy(SQL 语法) | | 速度/百里香叶 | 金耶 2 | | 小型应用程序 | WSGI(消歧义) | | Tomcat/Jetty | Apache/uWSGI |

现代开发人员保持代码高质量的另一个工具是源代码的自动化静态分析。Python 的 findbugs 和 checkstyle 等价物是 pylint。它可以强制执行统一的代码格式,检测基本的编程错误,并复制代码块。如您所料,您可以将它集成到您的 IDE 以及 CI 构建服务器中。

如果您正在构建一个桌面 GUI,那么 PyQT 和 PyGTK 库是流行的选择。这是分别围绕流行的 Qt 和 GTK 库的 Python 包装器。这两个框架都是跨平台的,就像 Java Swing 一样。

虽然桌面 GUI 应用程序仍有其一席之地,但说今天构建的大多数新应用程序都是 web 应用程序并不夸张。Python 非常适合这个任务。事实上,你可能会惊讶地发现,许多互联网上最受欢迎的网站,如 YouTube 和 Reddit,都是用 Python 编写的。

Spring Boot 是 Java 世界中全栈 web 开发的流行选择。我所说的全栈指的是一个框架,它处理从前端用户认证到后端数据库连接的所有事情,以及介于两者之间的所有事情。最流行的 Python 全栈 web 开发框架是 Django。如果您刚刚开始使用 Python 进行 web 开发,我强烈建议您从 Django 开始。你会发现这是一个非常容易学习和高效的 web 开发环境。

另一方面,如果你是那种发现全栈框架限制太多而无法使用的开发人员,那么你可以混合和匹配同类最佳的库并构建你自己的栈。在数据库访问方面,SQLAlchemy 是最好的选择。它提供了一个像 JDBC 一样的低级数据库接口,一个像 Spring JDBCTemplate 一样的中级接口,为编写原始 SQL 查询提供了方便,最后,它还提供了一个像 Hibernate 一样的高级 ORM 层。根据您的需求,您可以选择您需要的抽象层次。

web 开发的另一个方面是使用模板引擎为 HTTP 请求(通常是 HTML 页面)生成响应。这是一种你可以在 Java 世界中的 Velocity 或百里香图书馆中找到的工作。Jinja2 是 Python 应用程序的首选模板库。它有一个令人愉快的语法,许多功能,而且相当快。

Python 网站通常使用 Apache web 服务器或更小的特定于 Python 的 web 服务器(如 uWSGI)来部署。这些 web 服务器依赖于称为 web 服务器网关接口(WSGI)的 Python web 开发标准。可以把它看作是 Python 世界中 Java servlets 的等价物。

摘要

由于其悠久的历史和受欢迎的程度,Python 已经获得了丰富的库、工具和框架的生态系统来帮助软件开发。我们在本章中讨论了几个流行的,应该可以帮助你开始你的第一个 Python 项目。

Footnotes 1

http://ironpython.net/

2

http://www.jython.org/

四、Python 之禅

我们终于看完了这本简短的书,我希望,正如开始时所承诺的,你已经对 Python 语言和生态系统有了足够的了解,可以开始编写自己的 Python 代码了。

当您开始编写 Python 代码时,我强烈建议您也花些时间阅读由资深 Python 程序员编写的现有 Python 代码。阅读由有经验的 Python 爱好者编写的代码,会让你对被认为是好的、惯用的 Python 代码有一种欣赏。因为最终,不仅仅是语言的纯机制,而是社区对简单、可读的代码的普遍欣赏,使得 Python 的使用如此愉快。

没有什么比蒂姆·彼得的《Python 之禅》更能提炼出这种 Python 思维模式了 1

antrix@cellar:~$ python
>>> import this

The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one -- and preferably only one -- obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.

Namespaces 

are one honking great idea -- let's do more of those!
>>>

Footnotes 1

http://www.wefearchange.org/2010/06/import-this-and-zen-of-python.html

五、参考

posted @ 2024-08-09 17:45  绝不原创的飞龙  阅读(11)  评论(0编辑  收藏  举报