简介
在这个实验中,你将学习与 Python 包组织相关的重要概念。首先,你将学习如何在 Python 模块中使用 __all__
来控制导出的符号。这项技能对于管理从模块中暴露的内容至关重要。
其次,你将了解如何组合子模块以简化导入操作,并掌握模块拆分技术,从而更好地组织代码。这些实践将提高你的 Python 代码的可读性和可维护性。
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
在这个实验中,你将学习与 Python 包组织相关的重要概念。首先,你将学习如何在 Python 模块中使用 __all__
来控制导出的符号。这项技能对于管理从模块中暴露的内容至关重要。
其次,你将了解如何组合子模块以简化导入操作,并掌握模块拆分技术,从而更好地组织代码。这些实践将提高你的 Python 代码的可读性和可维护性。
当你开始使用 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 *
语句时,每个子模块导出哪些符号。
structure.py
:touch ~/project/structly/structure.py
此命令在你的项目的 structly
目录中创建一个名为 structure.py
的新文件。创建文件后,我们需要添加 __all__
变量。在文件顶部,紧接在导入语句之后添加以下行:
__all__ = ['Structure']
这行代码告诉 Python,当有人使用 from structure import *
时,仅会导入 Structure
符号。保存文件并退出编辑器。
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_*
函数。保存文件并退出编辑器。
tableformat.py
:touch ~/project/structly/tableformat.py
此命令在 structly
目录中创建一个名为 tableformat.py
的新文件。在文件顶部附近添加以下行:
__all__ = ['create_formatter', 'print_table']
这行代码指定,当有人使用 from tableformat import *
时,仅会导入 create_formatter
和 print_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 *
这些行从 structure
、reader
和 tableformat
子模块导入所有导出的符号。模块名前的点 (.
) 表示这些是相对导入,即从同一包内进行导入。保存文件并退出编辑器。
让我们创建一个简单的测试文件来验证我们的更改是否有效。这个测试文件将尝试导入我们在 __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_formatter
和 print_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__
列表。这个列表应该包含我们想要导出的所有符号。这些符号根据它们的来源进行分组,例如 structure
、reader
和 tableformat
模块。
## 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 *
模式。这有几个原因:
import *
时,很难判断一个符号来自哪个模块,这会使你的代码更难理解和维护。然而,在特定情况下,使用 import *
是合适的:
import *
可以更方便地访问所有必要的符号。__all__
定义了清晰的接口时。通过使用 __all__
列表,你可以控制导出哪些符号,从而更安全地使用 import *
。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(超文本标记语言)格式格式化数据。我们将把这个模块重新组织成一个包结构,为每种格式化器类型创建单独的文件。这将使代码更具模块化,更易于管理。
在开始重新组织代码之前,清理所有 Python 缓存文件是个不错的主意。这些文件是 Python 为加速代码执行而创建的,但在你对代码进行更改时,它们有时会导致问题。
cd ~/project/structly
rm -rf __pycache__
在上述命令中,cd ~/project/structly
将当前目录更改为你项目中的 structly
目录。rm -rf __pycache__
删除 __pycache__
目录及其所有内容。-r
选项表示递归,这意味着它将删除 __pycache__
目录内的所有文件和子目录。-f
选项表示强制,这意味着它将在不要求确认的情况下删除文件。
现在,让我们为我们的包创建一个新的目录结构。我们将创建一个名为 tableformat
的目录,并在其中创建一个名为 formats
的子目录。
mkdir -p tableformat/formats
mkdir
命令用于创建目录。-p
选项表示父目录,如果必要的父目录不存在,它将创建所有必要的父目录。因此,如果 tableformat
目录不存在,它将首先被创建,然后在其中创建 formats
目录。
接下来,我们将把原始的 tableformat.py
文件移动到新结构中,并将其重命名为 formatter.py
。
mv tableformat.py tableformat/formatter.py
mv
命令用于移动或重命名文件。在这种情况下,我们将 tableformat.py
文件移动到 tableformat
目录并将其重命名为 formatter.py
。
现在我们需要为每个格式化器创建文件,并将相关代码移动到这些文件中。
touch tableformat/formatter.py
touch
命令用于创建一个空文件。在这种情况下,我们在 tableformat
目录中创建一个名为 formatter.py
的文件。
我们将把 TableFormatter
基类以及任何通用实用函数(如 print_table
和 create_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 *
时应该导入哪些符号。在这种情况下,我们指定只应导入 TableFormatter
、print_table
和 create_formatter
符号。
TableFormatter
类是所有其他格式化器类的基类。它定义了两个方法 headings
和 row
,这些方法应由子类实现。
print_table
函数是一个实用函数,它接受一个对象列表、一个列名列表和一个格式化器对象,并以格式化表格的形式打印数据。
create_formatter
函数是一个工厂函数,它接受一个格式名称作为参数,并返回一个合适的格式化器对象。
进行这些更改后,保存并退出文件。
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
类,并实现了 headings
和 row
方法,以纯文本格式格式化数据。
进行这些更改后,保存并退出文件。
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
类,并实现 headings
和 row
方法,以 CSV 格式格式化数据。
进行这些更改后,保存并退出文件。
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
类,并实现 headings
和 row
方法,以 HTML 格式格式化数据。
进行这些更改后,保存并退出文件。
在 Python 中,__init__.py
文件用于将目录标记为 Python 包。我们需要在 tableformat
和 formats
目录中都创建 __init__.py
文件。
tableformat
包创建一个文件touch tableformat/__init__.py
将以下内容添加到文件中:
## Re-export the original symbols from tableformat.py
from .formatter import *
此语句从 formatter.py
文件中导入所有符号,并在你导入 tableformat
包时使这些符号可用。
进行这些更改后,保存并退出文件。
formats
包创建一个文件touch tableformat/formats/__init__.py
你可以将此文件留空,或者添加一个简单的文档字符串:
'''
Format implementations for different output formats.
'''
文档字符串简要描述了 formats
包的作用。
进行这些更改后,保存并退出文件。
让我们创建一个简单的测试,以验证我们的更改是否正确。
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 包至关重要。它们使你能够控制用户的访问范围,简化导入过程,并在项目扩展时对代码进行逻辑组织。