循环和动态模块导入

PythonPythonBeginner
立即练习

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

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

简介

在这个实验中,你将了解 Python 中两个与导入相关的重要概念。Python 中的模块导入有时会导致复杂的依赖关系,从而引发错误或低效的代码结构。循环导入(即两个或多个模块相互导入)会形成一个依赖循环,如果管理不当,可能会引发问题。

你还将探索动态导入,它允许在运行时而非程序启动时加载模块。这提供了灵活性,并有助于避免与导入相关的问题。本实验的目标是理解循环导入问题,实施解决方案以避免这些问题,并学习如何有效地使用动态模块导入。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ModulesandPackagesGroup(["Modules and Packages"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/ModulesandPackagesGroup -.-> python/importing_modules("Importing Modules") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/inheritance("Inheritance") subgraph Lab Skills python/importing_modules -.-> lab-132531{{"循环和动态模块导入"}} python/classes_objects -.-> lab-132531{{"循环和动态模块导入"}} python/inheritance -.-> lab-132531{{"循环和动态模块导入"}} end

理解导入问题

让我们先了解一下什么是模块导入。在 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()

注意导入语句位于文件中间。这存在几个问题:

  1. 这使得代码更难阅读和维护。当你查看一个文件时,你期望在开头看到所有的导入语句,这样你就能快速了解该文件依赖哪些外部模块。
  2. 这可能会导致循环导入问题。当两个或多个模块相互依赖时,就会发生循环导入,这可能会导致错误,并使你的代码行为异常。
  3. 这违反了 Python 将所有导入语句放在文件顶部的约定。遵循约定可以使你的代码更易读,也更便于其他开发者理解。

在接下来的步骤中,我们将更详细地探讨这些问题,并学习如何解决它们。

探究循环导入

循环导入是指两个或多个模块相互依赖的情况。具体来说,当模块 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 未定义的错误消息。这是循环导入问题的明显迹象。

问题的产生是由于以下一系列事件:

  1. formatter.py 尝试从 formats/text.py 导入 TextTableFormatter
  2. formats/text.pyformatter.py 导入 TableFormatter
  3. 当 Python 尝试解析这些导入时,它会陷入循环,因为它无法决定先完全导入哪个模块。

让我们撤销更改,使程序再次正常工作。编辑 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'>}

这个输出证实了动态导入在需要时会加载模块并注册格式化器类。

通过使用动态导入和类注册,我们创建了一个更简洁、更易于维护的代码结构。以下是这样做的好处:

  1. 现在所有的导入语句都位于文件顶部,这符合 Python 的编程规范。这使得代码更易于阅读和理解。
  2. 我们消除了循环导入的问题。循环导入可能会导致程序出现诸如无限循环或难以调试的错误等问题。
  3. 代码更加灵活。现在,我们可以添加新的格式化器,而无需修改 create_formatter 函数。在实际场景中,随着时间的推移可能会添加新功能,这种方式非常实用。

这种使用动态导入和类注册的模式在插件系统和框架中经常被使用。在这些系统中,组件需要根据用户的需求或程序的要求进行动态加载。

✨ 查看解决方案并练习

总结

在本次实验中,你学习了 Python 模块导入的重要概念和技术。首先,你了解了循环导入,明白了模块之间的循环依赖如何引发问题,以及为何需要谨慎处理以避免此类问题。其次,你实现了子类注册,这是一种让子类向其父类进行注册的模式,从而无需直接导入子类。

你还使用了 __import__() 函数进行动态导入,仅在需要时在运行时加载模块。这使得代码更加灵活,并有助于避免循环依赖。这些技术对于创建具有复杂模块关系的可维护 Python 包至关重要,并且在框架和库中被广泛使用。将这些模式应用到你的项目中,有助于你构建更具模块化、可扩展性和可维护性的代码结构。