控制符号与组合子模块

PythonPythonBeginner
立即练习

This tutorial is from open-source community. Access the source code

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

在这个实验中,你将学习与 Python 包组织相关的重要概念。首先,你将学习如何在 Python 模块中使用 __all__ 来控制导出的符号。这项技能对于管理从模块中暴露的内容至关重要。

其次,你将了解如何组合子模块以简化导入操作,并掌握模块拆分技术,从而更好地组织代码。这些实践将提高你的 Python 代码的可读性和可维护性。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ModulesandPackagesGroup(["Modules and Packages"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/scope("Scope") python/ModulesandPackagesGroup -.-> python/importing_modules("Importing Modules") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") subgraph Lab Skills python/function_definition -.-> lab-132530{{"控制符号与组合子模块"}} python/scope -.-> lab-132530{{"控制符号与组合子模块"}} python/importing_modules -.-> lab-132530{{"控制符号与组合子模块"}} python/classes_objects -.-> lab-132530{{"控制符号与组合子模块"}} end

理解包导入的复杂性

当你开始使用 Python 包时,你很快就会意识到导入模块可能会变得相当复杂和冗长。这种复杂性会使你的代码更难读写。在这个实验中,我们将仔细研究这个问题,并学习如何简化导入过程。

当前的导入结构

首先,让我们打开终端。终端是一个强大的工具,它允许你与计算机的操作系统进行交互。终端打开后,我们需要导航到项目目录。项目目录是存储与我们的 Python 项目相关的所有文件的地方。为此,我们将使用 cd 命令,它代表 “change directory”(更改目录)。

cd ~/project

现在我们已经进入了项目目录,让我们来查看 structly 包的当前结构。Python 中的包是一种组织相关模块的方式。我们可以使用 ls -la 命令列出 structly 包内的所有文件和目录,包括隐藏文件。

ls -la structly

你会注意到 structly 包中有几个 Python 模块。这些模块包含我们可以在代码中使用的函数和类。然而,如果我们想使用这些模块中的功能,目前需要使用很长的导入语句。例如:

from structly.structure import Structure
from structly.reader import read_csv_as_instances
from structly.tableformat import create_formatter, print_table

这些长导入路径写起来很麻烦,特别是如果你需要在代码中多次使用它们。它们还会使你的代码可读性降低,当你试图理解或调试代码时,这可能会成为一个问题。在这个实验中,我们将学习如何以一种使这些导入更简单的方式来组织包。

让我们先看看包的 __init__.py 文件的内容。__init__.py 文件是 Python 包中的一个特殊文件。当包被导入时,它会被执行,并且可以用来初始化包并设置任何必要的导入。

cat structly/__init__.py

你可能会发现 __init__.py 文件要么是空的,要么只包含很少的代码。在接下来的步骤中,我们将修改这个文件以简化我们的导入语句。

目标

在这个实验结束时,我们的目标是能够使用更简单的导入语句。与我们之前看到的长导入路径不同,我们将能够使用如下语句:

from structly import Structure, read_csv_as_instances, create_formatter, print_table

甚至:

from structly import *

使用这些更简单的导入语句将使我们的代码更简洁,更易于处理。在编写和维护代码时,它还能为我们节省时间和精力。

使用 __all__ 控制导出的符号

在 Python 中,当你使用 from module import * 语句时,你可能希望控制从模块中导入哪些符号(函数、类、变量)。这就是 __all__ 变量发挥作用的地方。from module import * 语句是一种将模块中的所有符号导入到当前命名空间的方式。然而,有时你并不想导入每个符号,特别是当符号很多或者有些符号是模块内部使用的时候。__all__ 变量允许你精确指定使用该语句时应导入哪些符号。

什么是 __all__

__all__ 变量是一个字符串列表。这个列表中的每个字符串代表一个符号(函数、类或变量),当有人使用 from module import * 语句时,模块会导出这些符号。如果模块中未定义 __all__ 变量,import * 语句将导入所有不以下划线开头的符号。以下划线开头的符号通常被视为模块的私有或内部符号,不应该直接导入。

修改每个子模块

现在,让我们将 __all__ 变量添加到 structly 包的每个子模块中。这将帮助我们控制当有人使用 from module import * 语句时,每个子模块导出哪些符号。

  1. 首先,让我们修改 structure.py
touch ~/project/structly/structure.py

此命令在你的项目的 structly 目录中创建一个名为 structure.py 的新文件。创建文件后,我们需要添加 __all__ 变量。在文件顶部,紧接在导入语句之后添加以下行:

__all__ = ['Structure']

这行代码告诉 Python,当有人使用 from structure import * 时,仅会导入 Structure 符号。保存文件并退出编辑器。

  1. 接下来,让我们修改 reader.py
touch ~/project/structly/reader.py

此命令在 structly 目录中创建一个名为 reader.py 的新文件。现在,浏览文件,找出所有以 read_csv_as_ 开头的函数。这些函数就是我们想要导出的函数。然后,添加一个包含所有这些函数名的 __all__ 列表。它应该类似于以下内容:

__all__ = ['read_csv_as_instances', 'read_csv_as_dicts', 'read_csv_as_columns']

请注意,实际的函数名可能会根据你在文件中找到的内容而有所不同。确保包含你找到的所有 read_csv_as_* 函数。保存文件并退出编辑器。

  1. 现在,让我们修改 tableformat.py
touch ~/project/structly/tableformat.py

此命令在 structly 目录中创建一个名为 tableformat.py 的新文件。在文件顶部附近添加以下行:

__all__ = ['create_formatter', 'print_table']

这行代码指定,当有人使用 from tableformat import * 时,仅会导入 create_formatterprint_table 符号。保存文件并退出编辑器。

__init__.py 中统一导入

现在每个模块都定义了它要导出的内容,我们可以更新 __init__.py 文件以导入所有这些符号。__init__.py 文件是 Python 包中的一个特殊文件。当包被导入时,它会被执行,并且可以用来初始化包并从子模块导入符号。

touch ~/project/structly/__init__.py

此命令在 structly 目录中创建一个新的 __init__.py 文件。将文件内容更改为:

## structly/__init__.py

from .structure import *
from .reader import *
from .tableformat import *

这些行从 structurereadertableformat 子模块导入所有导出的符号。模块名前的点 (.) 表示这些是相对导入,即从同一包内进行导入。保存文件并退出编辑器。

测试我们的更改

让我们创建一个简单的测试文件来验证我们的更改是否有效。这个测试文件将尝试导入我们在 __all__ 变量中指定的符号,如果导入成功,则打印一条成功消息。

touch ~/project/test_structly.py

此命令在项目目录中创建一个名为 test_structly.py 的新文件。将以下内容添加到文件中:

## A simple test to verify our imports work correctly

from structly import Structure
from structly import read_csv_as_instances
from structly import create_formatter, print_table

print("Successfully imported all required symbols!")

这些行尝试从 structly 包中导入 Structure 类、read_csv_as_instances 函数以及 create_formatterprint_table 函数。如果导入成功,程序将打印消息 “Successfully imported all required symbols!”。保存文件并退出编辑器。现在让我们运行这个测试:

cd ~/project
python test_structly.py

cd ~/project 命令将当前工作目录更改为项目目录。python test_structly.py 命令运行 test_structly.py 脚本。如果一切正常,你应该会在屏幕上看到消息 “Successfully imported all required symbols!”。

✨ 查看解决方案并练习

从包中导出所有内容

在 Python 中,包的组织对于有效管理代码至关重要。现在,我们将进一步完善包的组织。我们将定义在包级别应该导出哪些符号。导出符号意味着让某些函数、类或变量可供代码的其他部分或可能使用你包的其他开发者使用。

向包中添加 __all__

当你使用 Python 包时,你可能希望控制当有人使用 from structly import * 语句时哪些符号是可访问的。这就是 __all__ 列表发挥作用的地方。通过在包的 __init__.py 文件中添加一个 __all__ 列表,你可以精确控制当有人使用 from structly import * 语句时哪些符号是可用的。

首先,让我们创建或更新 __init__.py 文件。如果文件不存在,我们将使用 touch 命令来创建它。

touch ~/project/structly/__init__.py

现在,打开 __init__.py 文件并添加一个 __all__ 列表。这个列表应该包含我们想要导出的所有符号。这些符号根据它们的来源进行分组,例如 structurereadertableformat 模块。

## structly/__init__.py

from .structure import *
from .reader import *
from .tableformat import *

## Define what symbols are exported when using "from structly import *"
__all__ = ['Structure',  ## from structure
           'read_csv_as_instances', 'read_csv_as_dicts', 'read_csv_as_columns',  ## from reader
           'create_formatter', 'print_table']  ## from tableformat

添加代码后,保存文件并退出编辑器。

理解 import *

在大多数 Python 代码中,通常不建议使用 from module import * 模式。这有几个原因:

  1. 它可能会用意外的符号污染你的命名空间。这意味着你的当前命名空间中可能会出现你意想不到的变量或函数,这可能会导致命名冲突。
  2. 它会使特定符号的来源不明确。当你使用 import * 时,很难判断一个符号来自哪个模块,这会使你的代码更难理解和维护。
  3. 它可能会导致遮蔽问题。当局部变量或函数与另一个模块中的变量或函数同名时,就会发生遮蔽,这可能会导致意外的行为。

然而,在特定情况下,使用 import * 是合适的:

  • 对于设计为作为一个整体使用的包。如果一个包旨在作为一个单一单元使用,那么使用 import * 可以更方便地访问所有必要的符号。
  • 当一个包通过 __all__ 定义了清晰的接口时。通过使用 __all__ 列表,你可以控制导出哪些符号,从而更安全地使用 import *
  • 对于交互式使用,例如在 Python REPL(读取 - 求值 - 打印循环)中。在交互式环境中,一次性导入所有符号可能会很方便。

使用 import * 进行测试

为了验证我们可以一次性导入所有符号,让我们创建另一个测试文件。我们将使用 touch 命令来创建文件。

touch ~/project/test_import_all.py

现在,打开 test_import_all.py 文件并添加以下内容。这段代码从 structly 包中导入所有符号,然后测试一些重要的符号是否可用。

## Test importing everything at once

from structly import *

## Try using the imported symbols
print(f"Structure symbol is available: {Structure is not None}")
print(f"read_csv_as_instances symbol is available: {read_csv_as_instances is not None}")
print(f"create_formatter symbol is available: {create_formatter is not None}")
print(f"print_table symbol is available: {print_table is not None}")

print("All symbols successfully imported!")

保存文件并退出编辑器。现在,让我们运行测试。首先,使用 cd 命令导航到项目目录,然后运行 Python 脚本。

cd ~/project
python test_import_all.py

如果一切设置正确,你应该会看到确认所有符号都已成功导入的信息。

✨ 查看解决方案并练习

模块拆分以实现更好的代码组织

随着你的 Python 项目不断发展,你可能会发现单个模块文件变得非常大,并且包含多个相关但不同的组件。当这种情况发生时,将模块拆分为包含子模块的包是一种很好的做法。这种方法可以让你的代码更有条理、更易于维护,并且更具可扩展性。

了解当前结构

tableformat.py 模块是一个大型模块的典型例子。它包含几个格式化器类,每个类负责以不同的方式格式化数据:

  • TableFormatter(基类):这是所有其他格式化器类的基类。它定义了其他类将继承和实现的基本结构和方法。
  • TextTableFormatter:这个类以纯文本格式格式化数据。
  • CSVTableFormatter:这个类以 CSV(逗号分隔值)格式格式化数据。
  • HTMLTableFormatter:这个类以 HTML(超文本标记语言)格式格式化数据。

我们将把这个模块重新组织成一个包结构,为每种格式化器类型创建单独的文件。这将使代码更具模块化,更易于管理。

步骤 1:清理缓存文件

在开始重新组织代码之前,清理所有 Python 缓存文件是个不错的主意。这些文件是 Python 为加速代码执行而创建的,但在你对代码进行更改时,它们有时会导致问题。

cd ~/project/structly
rm -rf __pycache__

在上述命令中,cd ~/project/structly 将当前目录更改为你项目中的 structly 目录。rm -rf __pycache__ 删除 __pycache__ 目录及其所有内容。-r 选项表示递归,这意味着它将删除 __pycache__ 目录内的所有文件和子目录。-f 选项表示强制,这意味着它将在不要求确认的情况下删除文件。

步骤 2:创建新的包结构

现在,让我们为我们的包创建一个新的目录结构。我们将创建一个名为 tableformat 的目录,并在其中创建一个名为 formats 的子目录。

mkdir -p tableformat/formats

mkdir 命令用于创建目录。-p 选项表示父目录,如果必要的父目录不存在,它将创建所有必要的父目录。因此,如果 tableformat 目录不存在,它将首先被创建,然后在其中创建 formats 目录。

步骤 3:移动并重命名原始文件

接下来,我们将把原始的 tableformat.py 文件移动到新结构中,并将其重命名为 formatter.py

mv tableformat.py tableformat/formatter.py

mv 命令用于移动或重命名文件。在这种情况下,我们将 tableformat.py 文件移动到 tableformat 目录并将其重命名为 formatter.py

步骤 4:将代码拆分为单独的文件

现在我们需要为每个格式化器创建文件,并将相关代码移动到这些文件中。

1. 创建基础格式化器文件

touch tableformat/formatter.py

touch 命令用于创建一个空文件。在这种情况下,我们在 tableformat 目录中创建一个名为 formatter.py 的文件。

我们将把 TableFormatter 基类以及任何通用实用函数(如 print_tablecreate_formatter)保留在这个文件中。该文件应该类似于以下内容:

## Base TableFormatter class and utility functions

__all__ = ['TableFormatter', 'print_table', 'create_formatter']

class TableFormatter:
    def headings(self, headers):
        '''
        Emit table headings.
        '''
        raise NotImplementedError()

    def row(self, rowdata):
        '''
        Emit a single row of table data.
        '''
        raise NotImplementedError()

def print_table(objects, columns, formatter):
    '''
    Make a nicely formatted table from a list of objects and attribute names.
    '''
    formatter.headings(columns)
    for obj in objects:
        rowdata = [getattr(obj, name) for name in columns]
        formatter.row(rowdata)

def create_formatter(fmt):
    '''
    Create an appropriate formatter given an output format name.
    '''
    if fmt == 'text':
        from .formats.text import TextTableFormatter
        return TextTableFormatter()
    elif fmt == 'csv':
        from .formats.csv import CSVTableFormatter
        return CSVTableFormatter()
    elif fmt == 'html':
        from .formats.html import HTMLTableFormatter
        return HTMLTableFormatter()
    else:
        raise ValueError(f'Unknown format {fmt}')

__all__ 变量用于指定当你使用 from module import * 时应该导入哪些符号。在这种情况下,我们指定只应导入 TableFormatterprint_tablecreate_formatter 符号。

TableFormatter 类是所有其他格式化器类的基类。它定义了两个方法 headingsrow,这些方法应由子类实现。

print_table 函数是一个实用函数,它接受一个对象列表、一个列名列表和一个格式化器对象,并以格式化表格的形式打印数据。

create_formatter 函数是一个工厂函数,它接受一个格式名称作为参数,并返回一个合适的格式化器对象。

进行这些更改后,保存并退出文件。

2. 创建文本格式化器

touch tableformat/formats/text.py

我们将只把 TextTableFormatter 类添加到这个文件中。

## Text formatter implementation

__all__ = ['TextTableFormatter']

from ..formatter import TableFormatter

class TextTableFormatter(TableFormatter):
    '''
    Emit a table in plain-text format
    '''
    def headings(self, headers):
        print(' '.join('%10s' % h for h in headers))
        print(('-'*10 + ' ')*len(headers))

    def row(self, rowdata):
        print(' '.join('%10s' % d for d in rowdata))

__all__ 变量指定当你使用 from module import * 时只应导入 TextTableFormatter 符号。

from ..formatter import TableFormatter 语句从父目录的 formatter.py 文件中导入 TableFormatter 类。

TextTableFormatter 类继承自 TableFormatter 类,并实现了 headingsrow 方法,以纯文本格式格式化数据。

进行这些更改后,保存并退出文件。

3. 创建 CSV 格式化器

touch tableformat/formats/csv.py

我们将只把 CSVTableFormatter 类添加到这个文件中。

## CSV formatter implementation

__all__ = ['CSVTableFormatter']

from ..formatter import TableFormatter

class CSVTableFormatter(TableFormatter):
    '''
    Output data in CSV format.
    '''
    def headings(self, headers):
        print(','.join(headers))

    def row(self, rowdata):
        print(','.join(str(d) for d in rowdata))

与前面的步骤类似,我们指定 __all__ 变量,导入 TableFormatter 类,并实现 headingsrow 方法,以 CSV 格式格式化数据。

进行这些更改后,保存并退出文件。

4. 创建 HTML 格式化器

touch tableformat/formats/html.py

我们将只把 HTMLTableFormatter 类添加到这个文件中。

## HTML formatter implementation

__all__ = ['HTMLTableFormatter']

from ..formatter import TableFormatter

class HTMLTableFormatter(TableFormatter):
    '''
    Output data in HTML format.
    '''
    def headings(self, headers):
        print('<tr>', end='')
        for h in headers:
            print(f'<th>{h}</th>', end='')
        print('</tr>')

    def row(self, rowdata):
        print('<tr>', end='')
        for d in rowdata:
            print(f'<td>{d}</td>', end='')
        print('</tr>')

同样,我们指定 __all__ 变量,导入 TableFormatter 类,并实现 headingsrow 方法,以 HTML 格式格式化数据。

进行这些更改后,保存并退出文件。

步骤 5:创建包初始化文件

在 Python 中,__init__.py 文件用于将目录标记为 Python 包。我们需要在 tableformatformats 目录中都创建 __init__.py 文件。

1. 为 tableformat 包创建一个文件

touch tableformat/__init__.py

将以下内容添加到文件中:

## Re-export the original symbols from tableformat.py
from .formatter import *

此语句从 formatter.py 文件中导入所有符号,并在你导入 tableformat 包时使这些符号可用。

进行这些更改后,保存并退出文件。

2. 为 formats 包创建一个文件

touch tableformat/formats/__init__.py

你可以将此文件留空,或者添加一个简单的文档字符串:

'''
Format implementations for different output formats.
'''

文档字符串简要描述了 formats 包的作用。

进行这些更改后,保存并退出文件。

步骤 6:测试新结构

让我们创建一个简单的测试,以验证我们的更改是否正确。

cd ~/project
touch test_tableformat.py

将以下内容添加到 test_tableformat.py 文件中:

## Test the tableformat package restructuring

from structly import *

## Create formatters of each type
text_fmt = create_formatter('text')
csv_fmt = create_formatter('csv')
html_fmt = create_formatter('html')

## Define some test data
class TestData:
    def __init__(self, name, value):
        self.name = name
        self.value = value

## Create a list of test objects
data = [
    TestData('apple', 10),
    TestData('banana', 20),
    TestData('cherry', 30)
]

## Test text formatter
print("\nText Format:")
print_table(data, ['name', 'value'], text_fmt)

## Test CSV formatter
print("\nCSV Format:")
print_table(data, ['name', 'value'], csv_fmt)

## Test HTML formatter
print("\nHTML Format:")
print_table(data, ['name', 'value'], html_fmt)

此测试代码从 structly 包中导入必要的函数和类,创建每种类型的格式化器,定义一些测试数据,然后通过以相应格式打印数据来测试每个格式化器。

进行这些更改后,保存并退出文件。现在运行测试:

python test_tableformat.py

你应该会看到相同的数据以三种不同的方式(文本、CSV 和 HTML)进行格式化。如果你看到了预期的输出,这意味着你的代码重组成功了。

✨ 查看解决方案并练习

总结

在本次实验中,你学习了几种重要的 Python 包组织技术。首先,你掌握了使用 __all__ 变量来明确定义模块导出的符号。其次,你通过从顶级包重新导出子模块符号,创建了一个更用户友好的包接口。

这些技术对于构建简洁、可维护且用户友好的 Python 包至关重要。它们使你能够控制用户的访问范围,简化导入过程,并在项目扩展时对代码进行逻辑组织。