简介
在这个实验中,你将了解 Python 中两个与导入相关的重要概念。Python 中的模块导入有时会导致复杂的依赖关系,从而引发错误或低效的代码结构。循环导入(即两个或多个模块相互导入)会形成一个依赖循环,如果管理不当,可能会引发问题。
你还将探索动态导入,它允许在运行时而非程序启动时加载模块。这提供了灵活性,并有助于避免与导入相关的问题。本实验的目标是理解循环导入问题,实施解决方案以避免这些问题,并学习如何有效地使用动态模块导入。
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
在这个实验中,你将了解 Python 中两个与导入相关的重要概念。Python 中的模块导入有时会导致复杂的依赖关系,从而引发错误或低效的代码结构。循环导入(即两个或多个模块相互导入)会形成一个依赖循环,如果管理不当,可能会引发问题。
你还将探索动态导入,它允许在运行时而非程序启动时加载模块。这提供了灵活性,并有助于避免与导入相关的问题。本实验的目标是理解循环导入问题,实施解决方案以避免这些问题,并学习如何有效地使用动态模块导入。
让我们先了解一下什么是模块导入。在 Python 中,当你想使用另一个文件(模块)中的函数、类或变量时,你会使用 import
语句。然而,导入的结构方式可能会导致各种问题。
现在,我们来研究一个有问题的模块结构示例。tableformat/formatter.py
中的代码,其导入语句分散在整个文件中。乍一看,这似乎不是什么大问题,但它会带来维护和依赖方面的问题。
首先,打开 WebIDE 文件浏览器,导航到 structly
目录。我们将运行几个命令来了解项目的当前结构。cd
命令用于更改当前工作目录,ls -la
命令用于列出当前目录中的所有文件和目录,包括隐藏文件。
cd ~/project/structly
ls -la
这将显示项目目录中的文件。现在,我们使用 cat
命令查看其中一个有问题的文件,该命令用于显示文件的内容。
cat tableformat/formatter.py
你应该会看到类似于以下的代码:
## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin
class TableFormatter(ABC):
@abstractmethod
def headings(self, headers):
pass
@abstractmethod
def row(self, rowdata):
pass
from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter
def create_formatter(name, column_formats=None, upper_headers=False):
if name == 'text':
formatter_cls = TextTableFormatter
elif name == 'csv':
formatter_cls = CSVTableFormatter
elif name == 'html':
formatter_cls = HTMLTableFormatter
else:
raise RuntimeError('Unknown format %s' % name)
if column_formats:
class formatter_cls(ColumnFormatMixin, formatter_cls):
formats = column_formats
if upper_headers:
class formatter_cls(UpperHeadersMixin, formatter_cls):
pass
return formatter_cls()
注意导入语句位于文件中间。这存在几个问题:
在接下来的步骤中,我们将更详细地探讨这些问题,并学习如何解决它们。
循环导入是指两个或多个模块相互依赖的情况。具体来说,当模块 A 导入模块 B,而模块 B 也直接或间接地导入模块 A 时,就会出现这种情况。这会形成一个依赖循环,Python 的导入系统无法正确解决这个问题。简单来说,Python 在尝试确定先导入哪个模块时会陷入循环,这可能会导致程序出错。
让我们通过代码实验来看看循环导入会如何引发问题。
首先,我们将运行股票程序,检查它在当前结构下是否能正常工作。这一步有助于我们建立一个基准,在进行任何更改之前,了解程序按预期运行的情况。
cd ~/project/structly
python3 stock.py
程序应该能正确运行,并以格式化表格的形式显示股票数据。如果可以正常运行,这意味着当前的代码结构没有循环导入问题,运行良好。
现在,我们要修改 formatter.py
文件。通常,将导入语句移到文件顶部是一个好的做法。这样可以使代码更有条理,一眼就能看懂。
cd ~/project/structly
在 WebIDE 中打开 tableformat/formatter.py
文件。我们将把以下导入语句移到文件顶部,放在现有导入语句之后。这些导入语句是针对不同的表格格式化器的,如文本、CSV 和 HTML 格式。
from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter
现在,文件开头应该如下所示:
## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin
from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter
class TableFormatter(ABC):
@abstractmethod
def headings(self, headers):
pass
@abstractmethod
def row(self, rowdata):
pass
保存文件,然后再次尝试运行股票程序。
python3 stock.py
你应该会看到一条关于 TableFormatter
未定义的错误消息。这是循环导入问题的明显迹象。
问题的产生是由于以下一系列事件:
formatter.py
尝试从 formats/text.py
导入 TextTableFormatter
。formats/text.py
从 formatter.py
导入 TableFormatter
。让我们撤销更改,使程序再次正常工作。编辑 tableformat/formatter.py
文件,将导入语句移回原来的位置(在 TableFormatter
类定义之后)。
## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin
class TableFormatter(ABC):
@abstractmethod
def headings(self, headers):
pass
@abstractmethod
def row(self, rowdata):
pass
from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter
再次运行程序,确认它能正常工作。
python3 stock.py
这表明,尽管从代码组织的角度来看,将导入语句放在文件中间不是最佳做法,但这样做是为了避免循环导入问题。在接下来的步骤中,我们将探索更好的解决方案。
在编程中,循环导入可能是个棘手的问题。我们可以采用注册模式,而非直接导入格式化器类。在这种模式下,子类会向其父类进行自我注册。这是一种常见且有效的避免循环导入的方法。
首先,让我们了解如何获取类的模块名。模块名很重要,因为我们会在注册模式中用到它。为此,我们将在终端中运行一个 Python 命令。
cd ~/project/structly
python3 -c "from structly.tableformat.formats.text import TextTableFormatter; print(TextTableFormatter.__module__); print(TextTableFormatter.__module__.split('.')[-1])"
当你运行这个命令时,会看到如下输出:
structly.tableformat.formats.text
text
这个输出表明,我们可以从类本身提取模块名。稍后,我们会用这个模块名来注册子类。
现在,让我们修改 tableformat/formatter.py
文件中的 TableFormatter
类,添加一个注册机制。在 WebIDE 中打开这个文件。我们将在 TableFormatter
类中添加一些代码,这些代码将帮助我们自动注册子类。
class TableFormatter(ABC):
_formats = { } ## Dictionary to store registered formatters
@classmethod
def __init_subclass__(cls):
name = cls.__module__.split('.')[-1]
TableFormatter._formats[name] = cls
@abstractmethod
def headings(self, headers):
pass
@abstractmethod
def row(self, rowdata):
pass
__init_subclass__
方法是 Python 中的一个特殊方法。每当创建 TableFormatter
的子类时,就会调用这个方法。在这个方法中,我们提取子类的模块名,并将其作为键,把该子类注册到 _formats
字典中。
接下来,我们需要修改 create_formatter
函数,以使用这个注册字典。这个函数负责根据给定的名称创建合适的格式化器。
def create_formatter(name, column_formats=None, upper_headers=False):
formatter_cls = TableFormatter._formats.get(name)
if not formatter_cls:
raise RuntimeError('Unknown format %s' % name)
if column_formats:
class formatter_cls(ColumnFormatMixin, formatter_cls):
formats = column_formats
if upper_headers:
class formatter_cls(UpperHeadersMixin, formatter_cls):
pass
return formatter_cls()
完成这些修改后,保存文件。然后,让我们测试一下程序是否仍然可以正常运行。我们将运行 stock.py
脚本。
python3 stock.py
如果程序能正确运行,就说明我们的修改没有破坏任何功能。现在,让我们查看 _formats
字典的内容,看看注册是如何工作的。
python3 -c "from structly.tableformat.formatter import TableFormatter; print(TableFormatter._formats)"
你应该会看到如下输出:
{'text': <class 'structly.tableformat.formats.text.TextTableFormatter'>, 'csv': <class 'structly.tableformat.formats.csv.CSVTableFormatter'>, 'html': <class 'structly.tableformat.formats.html.HTMLTableFormatter'>}
这个输出证实了我们的子类已正确注册到 _formats
字典中。不过,我们的文件中间仍然存在一些导入语句。在下一步中,我们将使用动态导入来解决这个问题。
在编程中,导入语句用于从其他模块引入代码,以便我们可以使用它们的功能。然而,有时在文件中间放置导入语句会使代码显得有些杂乱,难以理解。在这部分内容中,我们将学习如何使用动态导入来解决这个问题。动态导入是一项强大的功能,它允许我们在运行时加载模块,这意味着我们仅在实际需要某个模块时才加载它。
首先,我们需要移除当前位于 TableFormatter
类之后的导入语句。这些导入属于静态导入,会在程序启动时加载。要完成此操作,请在 WebIDE 中打开 tableformat/formatter.py
文件。打开文件后,找到并删除以下几行代码:
from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter
如果你现在尝试在终端中执行以下命令来运行程序:
python3 stock.py
程序将会运行失败。原因在于格式化器不会被注册到 _formats
字典中。你会看到一条关于未知格式的错误消息。这是因为程序无法找到正常运行所需的格式化器类。
为了解决这个问题,我们将修改 create_formatter
函数。目标是在需要时动态导入所需的模块。按如下所示更新该函数:
def create_formatter(name, column_formats=None, upper_headers=False):
if name not in TableFormatter._formats:
__import__(f'{__package__}.formats.{name}')
formatter_cls = TableFormatter._formats.get(name)
if not formatter_cls:
raise RuntimeError('Unknown format %s' % name)
if column_formats:
class formatter_cls(ColumnFormatMixin, formatter_cls):
formats = column_formats
if upper_headers:
class formatter_cls(UpperHeadersMixin, formatter_cls):
pass
return formatter_cls()
此函数中最重要的一行代码是:
__import__(f'{__package__}.formats.{name}')
这行代码根据格式名称动态导入模块。当模块被导入时,其 TableFormatter
的子类会自动进行自我注册。这要归功于我们之前添加的 __init_subclass__
方法。该方法是 Python 的一个特殊方法,在创建子类时会被调用,在我们的代码中,它用于注册格式化器类。
完成这些修改后,保存文件。然后,使用以下命令再次运行程序:
python3 stock.py
现在,即使我们移除了静态导入,程序也应该能正常运行。为了验证动态导入是否按预期工作,我们将清空 _formats
字典,然后调用 create_formatter
函数。在终端中运行以下命令:
python3 -c "from structly.tableformat.formatter import TableFormatter, create_formatter; TableFormatter._formats.clear(); print('Before:', TableFormatter._formats); create_formatter('text'); print('After:', TableFormatter._formats)"
你应该会看到类似如下的输出:
Before: {}
After: {'text': <class 'structly.tableformat.formats.text.TextTableFormatter'>}
这个输出证实了动态导入在需要时会加载模块并注册格式化器类。
通过使用动态导入和类注册,我们创建了一个更简洁、更易于维护的代码结构。以下是这样做的好处:
create_formatter
函数。在实际场景中,随着时间的推移可能会添加新功能,这种方式非常实用。这种使用动态导入和类注册的模式在插件系统和框架中经常被使用。在这些系统中,组件需要根据用户的需求或程序的要求进行动态加载。
在本次实验中,你学习了 Python 模块导入的重要概念和技术。首先,你了解了循环导入,明白了模块之间的循环依赖如何引发问题,以及为何需要谨慎处理以避免此类问题。其次,你实现了子类注册,这是一种让子类向其父类进行注册的模式,从而无需直接导入子类。
你还使用了 __import__()
函数进行动态导入,仅在需要时在运行时加载模块。这使得代码更加灵活,并有助于避免循环依赖。这些技术对于创建具有复杂模块关系的可维护 Python 包至关重要,并且在框架和库中被广泛使用。将这些模式应用到你的项目中,有助于你构建更具模块化、可扩展性和可维护性的代码结构。