第 8 章:模块和包

到目前为止,我们一直在交互式控制台和简单脚本的级别上查看代码。这对于小型示例来说效果很好,但当您的程序变得更大时,有必要将程序分解成更小的单元。在 Python 中,这些单元在大型程序中的基本构建块是模块。

导入以供重用

将代码分解成模块有助于组织大型代码库。模块可用于逻辑地分离属于一起的代码,使程序更容易理解。模块有助于创建可以在共享某些功能的不同应用程序中导入和使用的库。Jython 的标准库附带大量模块,这些模块可以立即在您的程序中使用。

导入基础

以下讨论将使用一个名为 breakfast.py 的愚蠢示例文件

class Spam(object):

    def order(self, number):
        print "spam " * number

def order_eggs():
    print " and eggs!"

s = Spam()
s.order(3)
order_eggs()

我们将从几个定义开始。命名空间是唯一标识符的逻辑分组。换句话说,命名空间是在您的程序中从给定代码段可以访问的那组名称。例如,如果您打开 Jython 提示符并键入 dir(),则将显示解释器命名空间中的名称。

>>> dir()
['__doc__', '__name__']

解释器命名空间包含 __doc__ 和 __name__。__doc__ 属性包含顶层文档字符串,在本例中为空。我们将在稍后介绍 __name__ 属性。首先,我们需要谈谈 Jython 模块。Jython 中的模块是一个包含 Python 定义和语句的文件,这些定义和语句反过来定义了一个命名空间。模块名称与文件名相同,只是去掉了后缀 .py,因此在我们当前的示例中,Python 文件“breakfast.py”定义了模块“breakfast”。

现在我们可以谈谈 __name__ 属性。当直接运行模块时,例如“jython breakfast.py”,__name__ 将包含 ‘__main__’。如果导入模块,__name__ 将包含模块的名称,因此“import breakfast”会导致 breakfast 模块包含一个名为“breakfast”的 __name__。

>>> __doc__
>>> __name__
'__main__'
让我们看看当我们导入 breakfast 时会发生什么::
>>> import breakfast
spam spam spam
 and eggs!
>>> dir()
['__doc__', '__name__', 'breakfast']

在导入后检查 doc() 显示早餐已被添加到顶级命名空间。请注意,导入操作实际上执行了 breakfast.py 中的代码。大多数情况下,我们不希望模块在导入时以这种方式执行。为了避免这种情况,但允许代码在直接调用时执行,我们通常检查 __name__ 属性。

class Spam(object):

    def order(self, number):
        print "spam " * number

def order_eggs():
    print " and eggs!"

if __name__ == '__main__':
    s = Spam()
    s.order(3)
    order_eggs()
现在,如果我们导入早餐,我们将不会得到输出::
>>> import breakfast

这是因为在这种情况下,__name__ 属性将包含 'breakfast',即模块的名称。如果我们从命令行调用 breakfast.py,例如“jython breakfast.py”,那么我们将再次获得输出,因为 breakfast 将作为 __main__ 执行。

在 Java 等语言中,import 语句严格来说是一个编译器指令,必须出现在源文件的最前面。在 Jython 中,import 语句是一个表达式,可以出现在源文件的任何位置,甚至可以有条件地执行。

例如,一个常见的习惯用法是尝试导入可能不存在的东西,并在 try 块中,在 except 块中导入一个已知存在的模块。

>>> try:
...     from blah import foo
... except ImportError:
...     def foo():
...         return "hello from backup foo"
...
>>> foo()
'hello from backup foo'
>>>

如果名为 blah 的模块存在,foo 的定义将从那里获取。由于不存在这样的模块,因此 foo 在 except 块中定义,当我们调用 foo 时,将返回 'hello from backup foo' 字符串。

我应该指出,dir() 实际上并没有打印出解释器顶层的整个命名空间。有很多名称被省略,因为 dir() 输出将不太有用。可以导入特殊的 __builtin__ 模块来查看其余部分。

>>> import __builtin__
>>> dir(__builtin__)
['ArithmeticError', 'AssertionError', 'AttributeError', ...

不幸的是,Jython 必须处理“包”的两个截然不同的定义。在 Python 世界中,Python 包是一个包含 __init__.py 文件的目录。该目录通常包含一些据说包含在包中的 Python 模块。__init__.py 文件在导入任何包含的模块之前执行。

在 Java 世界中,Java 包使用嵌套目录将 Java 类组织到命名空间中。Java 包不需要 __init__.py 文件。此外,与 Python 包不同,Java 包在每个 Java 文件中使用位于顶部的 package 指令显式引用。

chapter7/
    searchdir.py
    search/
        __init__.py
        walker.py
        scanner.py

该示例包含一个包:search,它是一个包,因为它是一个包含特殊 __init__.py 文件的目录。在这种情况下,__init__.py 是空的,因此仅用作 search 是一个包的标记。如果 __init__.py 包含代码,它将在导入其任何包含的模块之前执行。请注意,目录 chapter7 本身不是一个包,因为它不包含 __init__.py。示例程序中有三个模块:searchdir、search.input 和 search.scanner。该程序的代码可以在 XXX 下载。

searchdir.py ~~~~~~~~~~~~

import search.scanner as scanner
import sys

help = """
Usage: search.py directory terms...
"""

args = sys.argv

if args == None or len(args) < 2:
    print help
    exit()

dir = args[1]
terms = args[2:]
scan = scanner.scan(dir, terms)
scan.display()

scanner.py ———-

from search.walker import DirectoryWalker
from javax.swing import JFrame, JTable, WindowConstants

class ScanResults(object):
    def __init__(self):
        self.results = []

    def add(self, file, line):
        self.results.append((file, line))

    def display(self):
        colnames = ['file', 'line']
        table = JTable(self.results, colnames)
        frame = JFrame("%i Results" % len(self.results))
        frame.getContentPane().add(table)
        frame.size = 400, 300
        frame.defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE
        frame.visible = True

    def scan(dir, terms):
        results = ScanResults()
        for filename in DirectoryWalker(dir):
            for line in open(filename):
                for term in terms:
                    if term in line:
                        results.add(filename,line)
        return results

walker.py ———-

import os

class DirectoryWalker:
    # A forward iterator that traverses a directory tree. Adapted from an
    # example in the eff-bot library guide: os-path-walk-example-3.py

    def __init__(self, directory):
        self.stack = [directory]
        self.files = []
        self.index = 0

    def __getitem__(self, index):
        while 1:
            try:
                file = self.files[self.index]
                self.index = self.index + 1
            except IndexError:
                # pop next directory from stack
                self.directory = self.stack.pop()
                self.files = os.listdir(self.directory)
                self.index = 0
            else:
                # got a filename
                fullname = os.path.join(self.directory, file)
                if (os.path.isdir(fullname) and not
                    os.path.islink(fullname)):
                        self.stack.append(fullname)
                else:
                    return fullname

如果您在自己的目录中运行 searchdir.py,如下所示

尝试示例代码 —————————

$ jython scanner.py . terms

您将获得一个名为“5 Results”的 Swing 表格(如果匹配 .class 文件,则可能更多)。让我们检查一下此程序中使用的 import 语句。searchdir 模块包含两个 import 语句:

import search.scanner as scanner
import sys

第一个导入模块“search.scannar”并重命名模块为“scannar”。第二个导入内置模块“sys”并将名称保留为“sys”。“search.scannar”模块有两个 import 语句

from search.walker import DirectoryWalker
from javax.swing import JFrame, JTable, WindowConstants

第一个从“search.walker”模块导入 DirectoryWalker。请注意,即使 search.walker 与 search.scanner 位于同一个包中,我们也必须这样做。最后一个导入很有趣,因为它从 java 包 javax.swing 导入 java 类,例如 JFrame。Jython 使这种导入看起来与其他导入相同。这个简单的示例展示了如何从不同的模块和包导入代码来模块化您的程序。

导入语句类型

import 语句有多种形式,允许更精细地控制导入如何将命名值引入当前模块。

基本导入语句 ———————–

import module
from module import submodule
from . import submodule

我将依次讨论每种 import 语句形式,从

import module

这种最基本的导入类型直接导入一个模块。与 Java 不同,这种形式的导入绑定最左边的模块名称,因此,如果您导入一个嵌套模块,例如

import javax.swing.JFrame

您需要在代码中将其引用为“javax.swing.JFrame”。在 Java 中,这将导入“JFrame”。

从导入语句 ———————-

from module import name

这种形式的导入允许您导入嵌套在其他模块中的模块、类或函数。这使您可以实现典型的 Java 导入所带来的结果。要获取 Jython 代码中的 JFrame,您需要发出

from javax.swing import JFrame

您还可以使用 from 样式的导入,使用“*”将模块中的所有名称直接导入到当前模块中。这种形式的导入在 Python 社区中是不鼓励的,并且在从 Java 包导入时尤其麻烦(在某些情况下它不起作用,有关详细信息,请参见第 10 章),因此您应该避免使用它。它看起来像这样

from module import *

相对导入语句

Python 2.5 中引入了一种新的导入类型,即显式相对导入。这些导入语句使用点来指示您将从当前模块嵌套后退多远,一个点表示当前模块。

from . import module
from .. import module
from .module import submodule
from ..module import submodule

尽管这种导入方式刚刚引入,但其使用是不鼓励的。显式相对导入是对隐式相对导入需求的反应。如果您查看 search.scanner 包,您将看到导入语句

from search.walker import DirectoryWalker

因为 search.walker 与 search.scanner 位于同一个包中,所以导入语句可以是

from walker import DirectoryWalker

一些程序员喜欢使用这样的相对导入,以便导入能够在模块重构后继续存在,但这些相对导入可能会因为可能出现名称冲突而容易出错。新语法提供了一种显式使用相对导入的方法,尽管它们仍然不鼓励使用。上面的导入语句将看起来像这样

from .walker import DirectoryWalker

别名导入语句

以上任何导入都可以添加“as”子句来更改导入模块,但赋予它一个新名称。

import module as alias
from module import submodule as alias
from . import submodule as alias

这在您的导入中为您提供了极大的灵活性,因此要回到 Jframe 示例,您可以发出

import javax.swing.JFrame as Foo

并使用对 Foo() 的调用实例化一个 JFrame 对象,这会让大多数从 Java 转到 Jython 的开发人员感到惊讶。

隐藏模块名称

通常,当导入模块时,模块中的所有名称都可用于导入模块。有两种方法可以将这些名称隐藏在导入模块之外。从以下划线 (_) 开头的任何名称开始,这是 Python 用于将名称标记为私有的约定,这是第一种方法。隐藏模块名称的第二种方法是定义一个名为 __all__ 的列表,该列表应仅包含您希望模块公开的名称。例如,以下是 Jython 的 os 模块顶部的 __all__ 的值

__all__ = ["altsep", "curdir", "pardir", "sep", "pathsep",
           "linesep", "defpath", "name", "path",
           "SEEK_SET", "SEEK_CUR", "SEEK_END"]

请注意,您可以在模块内部添加 __all__ 以扩展该模块的公开名称。实际上,Jython 中的 os 模块就是这样做的,它根据 Jython 运行的操作系统有条件地公开名称。

模块搜索路径、编译和加载

编译

尽管人们普遍认为 Jython 是“解释型,而不是编译型”,但实际上所有 Jython 代码在执行之前都会转换为 Java 字节码。这些字节码并不总是保存到磁盘,但是当您看到 Jython 执行任何代码时,即使是在 eval 或 exec 中,您也可以确定字节码正在被馈送到 JVM。我知道的唯一例外是实验性的 pycimport 模块,我将在下面关于 sys.meta_path 的部分中描述它,它解释 CPython 字节码而不是生成 Java 字节码。

模块搜索路径和加载

在 Jython 中理解模块搜索和加载的过程比在 CPython 或 Java 中更复杂,因为 Jython 可以搜索 Java 的 CLASSPATH 和 Python 的路径。我们将从查看 Python 的路径和 sys.path 开始。当您发出导入时,sys.path 定义了 Jython 将用于搜索您尝试导入的名称的路径。sys.path 列表中的对象告诉 Jython 在哪里搜索模块。这些对象中的大多数指向目录,但 Jython 中的 sys.path 中可能有一些特殊项,而不仅仅是指向目录的指针。尝试导入不在 sys.path 中的任何地方的文件(并且也无法在 CLASSPATH 中找到)会引发 ImportError 异常。让我们启动一个命令行并查看 sys.path。

>>> import sys
>>> sys.path
['', '/Users/frank/jython/Lib', '__classpath__', '__pyclasspath__/',
'/Users/frank/jython/Lib/site-packages']

第一个空白条目('')告诉 Jython 在当前目录中查找模块。第二个条目指向 Jython 的 Lib 目录,其中包含核心 Jython 模块。第三和第四个条目是特殊的标记,我们将在后面讨论,最后一个指向 site-packages 目录,当您从 Jython 发出 setuptools 指令时,可以在该目录中安装新库(有关 setuptools 的更多信息,请参见第 XXX 章)。

导入钩子

要了解 Jython 导入 Java 类的方式,我们必须了解一些关于 Python 导入协议的内容。我不会深入到每个细节,因为您需要查看 PEP 302。

简而言之,我们首先尝试在 sys.meta_path 上注册的任何自定义导入器。如果其中一个能够导入请求的模块,则允许该导入器处理它。接下来,我们尝试 sys.path 上的每个条目。对于这些条目中的每一个,我们找到第一个在 sys.path_hooks 上注册的钩子,它可以处理路径条目。如果我们找到一个导入钩子并且它成功地导入了模块,我们就会停止。如果这不起作用,我们会尝试内置的导入逻辑。如果这也失败了,则会抛出 ImportError。所以让我们看看 Jython 的 path_hooks。

sys.path_hooks ————–

>>> import sys
>>> sys.path_hooks
[<type 'org.python.core.JavaImporter'>, <type 'zipimport.zipimporter'>,
<type 'ClasspathPyImporter'>]

这些 path_hooks 条目中的每一个都指定了一个 path_hook,它将尝试导入特殊文件。JavaImporter,顾名思义,允许动态加载在运行时指定的 Java 包和类。例如,如果您想在运行时包含一个 jar,您可以执行以下代码,它将在下次尝试导入时被 JavaImporter 钩子拾取

>>> import sys
>>> sys.path.append("/Users/frank/lib/mysql-connector-java-5.1.6.jar")
>>> import com.mysql
*sys-package-mgr*: processing new jar, '/Users/frank/lib/mysql-connector-java-5.1.6.jar'
>>> dir(com.mysql)
['__name__', 'jdbc']

sys.meta_path

向 sys.meta_path 添加条目允许您添加在尝试任何其他导入之前发生的导入行为,即使是默认的内置导入行为。这可能是一个非常强大的工具,允许您做各种有趣的事情。例如,我将讨论一个与 Jython 2.5 一起提供的实验性模块。该模块是 pycimport。如果您启动 jython 并发出

>>> import pycimport

Jython 将开始扫描您的路径中的 .pyc 文件,如果找到一个,将使用 .pyc 文件加载您的模块。.pyc 文件是 CPython 在编译 Python 源代码时生成的。所以,如果您在导入 pycimport(它向 sys.meta_path 添加了一个钩子)之后,然后发出

>>> import foo

Jython 将扫描您的路径以查找名为 foo.pyc 的文件,如果找到,它将使用 CPython 字节码导入 foo 模块。以下是 pycimport.py 底部的代码,它定义了 MetaImporter 并将其添加到 sys.meta_path

class __MetaImporter(object):
    def __init__(self):
        self.__importers = {}
    def find_module(self, fullname, path):
        if __debugging__: print "MetaImporter.find_module(%s, %s)" % (
            repr(fullname), repr(path))
        for _path in sys.path:
            if _path not in self.__importers:
                try:
                    self.__importers[_path] = __Importer(_path)
                except:
                    self.__importers[_path] = None
            importer = self.__importers[_path]
            if importer is not None:
                loader = importer.find_module(fullname, path)
                if loader is not None:
                    return loader
        else:
            return None

sys.meta_path.insert(0, __MetaImporter())

find_module 方法调用 pycimport 的其他部分并查找 .pyc 文件。如果找到一个,它知道如何解析和执行这些文件,并将相应的模块添加到运行时。很酷吧?

Java 包扫描

虽然您可以要求 Java SDK 使用以下方法为您提供已知 ClassLoader 的所有包的列表

java.lang.ClassLoader#getPackages()

没有相应的

java.lang.Package#getClasses()

这对 Jython 来说是不幸的,因为 Jython 用户希望能够以强大的方式内省他们使用的代码。例如,用户希望能够对 Java 对象和包调用 dir() 以查看它们包含哪些名称

>>> import java.util.zip
>>> dir(java.util.zip)
['Adler32', 'CRC32', 'CheckedInputStream', 'CheckedOutputStream', 'Checksum', 'DataFormatException', 'Deflater', 'DeflaterOutputStream', 'GZIPInputStream', 'GZIPOutputStream', 'Inflater', 'InflaterInputStream', 'ZipEntry', 'ZipException', 'ZipFile', 'ZipInputStream', 'ZipOutputStream', '__name__']
>>> dir(java.util.zip.ZipInputStream)
['__class__', '__delattr__', '__doc__', '__eq__', '__getattribute__', '__hash__', '__init__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', 'available', 'class', 'close', 'closeEntry', 'equals', 'getClass', 'getNextEntry', 'hashCode', 'mark', 'markSupported', 'nextEntry', 'notify', 'notifyAll', 'read', 'reset', 'skip', 'toString', 'wait']

为了在合并命名空间的情况下使这种内省成为可能,需要在 Jython 首次启动时(以及在运行时将 jar 或类添加到 Jython 的路径时)付出一些重大努力。如果您之前曾经运行过 Jython 的新安装,您会认出这个系统在工作时的证据

*sys-package-mgr*: processing new jar, '/Users/frank/jython/jython.jar'
*sys-package-mgr*: processing new jar, '/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Classes/classes.jar'
*sys-package-mgr*: processing new jar, '/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Classes/ui.jar'
*sys-package-mgr*: processing new jar, '/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Classes/laf.jar'
...
*sys-package-mgr*: processing new jar, '/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/lib/ext/sunjce_provider.jar'
*sys-package-mgr*: processing new jar, '/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/lib/ext/sunpkcs11.jar'

这是 Jython 扫描它可以找到的所有 jar 文件以构建内部表示,这些表示在您的 JVM 上可用的包和类。这有一个不幸的副作用,即使 Jython 新安装的第一次启动变得非常缓慢。

Jython 如何找到要扫描的 jar 和类

Jython 使用两个属性来查找 jar 和类。这些设置可以通过命令行设置或注册表提供给 Jython(参见第 XXX 章)。这两个属性是

python.packages.paths
python.packagse.directories

这些属性是逗号分隔的列表,包含了扫描器用来构建其列表的进一步注册表条目。您可能不应该更改这些属性。这些属性指向的属性更有趣。两个可能需要操作的属性是

java.class.path
java.ext.dirs

对于 java.class.path 属性,条目以与您所在的操作系统上的类路径相同的方式分隔(即,在 Windows 上为“;”,在大多数其他系统上为“:”)。每个路径都会检查是否存在 .jar 或 .zip,如果它们具有这些后缀,则会对其进行扫描。

对于 java.ext.dirs 属性,条目以与 java.class.path 相同的方式分隔,但这些条目表示目录。这些目录会搜索以 .jar 或 .zip 结尾的任何文件,如果找到任何文件,则会对其进行扫描。

要控制扫描的 jar,您需要设置这些属性的值。有许多方法可以设置这些属性值,请参阅第 XXX 章了解更多信息。

如果您只使用完整的类导入,则可以完全跳过包扫描。将系统属性 python.cachedir.skip 设置为 true 或(同样)传递您自己的 postProperties 来关闭它。

Python 模块和包与 Java 包

导入 Python 模块和包与将 Java 包导入 Jython 的语义在一些重要的方面有所不同,需要仔细记住。

sys.path

当 Jython 尝试导入模块时,它将在其 sys.path 中以上一节中描述的方式查找,直到找到一个为止。如果它找到的模块表示 Python 模块或包,则此导入将显示“赢家通吃”语义。也就是说,第一个被导入的 Python 模块或包会阻止任何其他可能随后在任何查找中找到的模块或包。这意味着,如果您在 sys.path 中有一个仅包含名称 bar 的模块 foo,然后另一个名为 foo 的模块仅包含名称 baz,那么执行“import foo”将只为您提供 foo.bar,而不是 foo.baz。

这与 Jython 导入 Java 包的情况不同。如果您在路径中有一个包含 bar 的 Java 包 org.foo,以及一个在路径中稍后的包含 baz 的 Java 包 org.foo,那么执行“import org.foo”将合并这两个命名空间,以便您将获得 org.foo.bar 和 org.foo.baz。

同样重要的是要记住,如果您的路径中有一个特定名称的 Python 模块或包与您的路径中的 Java 包冲突,这也将产生赢家通吃的效果。如果 Java 包在路径中排在首位,那么该名称将绑定到合并的 Java 包。如果 Python 模块或包获胜,则不会进行进一步搜索,因此具有冲突名称的 Java 包将永远不会被找到。

命名 Python 模块和包 ———————————-

来自 Java 的开发人员经常会犯一个错误,那就是以与他们建模 Java 包相同的方式建模他们的 Jython 包结构。不要这样做。Java 的反向 URL 约定对于 Java 来说是一个很棒的约定,我甚至可以说是一个绝妙的约定。它在 Java 世界中确实非常有效,因为这些命名空间是合并的。然而,在 Python 世界中,模块和包显示“赢家通吃”语义,这是一种组织代码的灾难性方式。

如果您对 Python 采用这种风格,假设您来自“acme.com”,那么您将设置一个像“com.acme”这样的包结构。如果您尝试使用来自您的供应商 xyz 的库,该库被设置为“com.xyz”,那么您的路径上的第一个库将占用“com”命名空间,您将无法看到另一组包。

正确的 Python 命名 ——————–

Python 约定是尽可能保持命名空间的浅层,并使您的顶级命名空间足够独特,无论它是模块还是包。在上面的 acme 和公司 xyz 的情况下,如果您想将这些整个代码库放在一个命名空间下(不一定是正确的方法 - 作为一般规则,最好按产品而不是按组织进行组织),您可能会以“acme”和“xyz”开头您的包结构。

注意:至少有两组名称对于在 Jython 中命名模块或包来说是特别糟糕的选择。第一个是任何顶级域名,如 org、com、net、us、name。第二个是 Java 语言为其顶级命名空间保留的任何域名:java、javax。

Java 导入示例

我们从一个 Java 类开始,这个类在 Jython 启动时位于 CLASSPATH 中。

package com.foo;
public class HelloWorld {
    public void hello() {
        System.out.println("Hello World!");
    }
    public void hello(String name) {
        System.out.printf("Hello %s!", name);
    }
}

在这里,我们从 Jython 交互式解释器中操作该类。

>>> from com.foo import HelloWorld
>>> h = HelloWorld()
>>> h.hello()
Hello World!
>>> h.hello("frank")
Hello frank!

需要注意的是,由于 HelloWorld 程序位于 Java CLASSPATH 上,它没有经过我们之前提到的 sys.path 处理。在这种情况下,Java 类由 ClassLoader 直接加载。关于 Java ClassLoader 的讨论超出了本书的范围。要了解更多关于 ClassLoader 的信息,请参考(引用?也许指向 Java 语言规范部分)。

结论

在本章中,我们学习了如何将代码划分为模块,以实现组织和重用。我们学习了如何编写模块和包,以及 Jython 系统如何与 Java 类和包交互。这结束了第一部分。我们现在已经涵盖了 Jython 语言的基础知识,并准备学习如何使用 Jython。