网站搜索

Python 的 __all__:包、模块和通配符导入


Python 有一种叫做通配符导入的东西,它看起来像from module import *。这种类型的导入允许您快速将模块中的所有对象放入您的命名空间中。然而,在包上使用此导入可能会令人困惑,因为不清楚您要导入什么:子包、模块、对象? Python 有 __all__ 变量来解决这个问题。

__all__ 变量是一个字符串列表,其中每个字符串表示要向通配符导入公开的变量、函数、类或模块的名称。

在本教程中,您将:

  • 了解 Python 中的通配符导入
  • 使用 __all__ 控制您公开通配符导入的模块
  • 控制您在模块中公开的名称
  • 探索 __all__ 变量的其他用例
  • 了解使用 __all__ 的一些好处最佳实践

为了充分利用本教程,您应该熟悉一些 Python 概念,包括模块和包以及导入系统。

在 Python 中导入对象

创建 Python 项目或应用程序时,您需要一种从标准库或第三方库访问代码的方法。您还需要从可能构成您的项目的多个文件中访问您自己的代码。 Python 的导入系统是允许您执行此操作的机制。

导入系统允许您以不同的方式获取对象。您可以使用:

  • 显式导入
  • 通配符导入

在以下部分中,您将学习这两种策略的基础知识。您将了解在每种情况下可以使用的不同语法以及运行 import 语句的结果。

显式导入

在Python中,当您需要从模块中获取特定对象或从包中获取特定模块时,可以使用显式import语句。这种类型的语句允许您将目标对象带到当前命名空间,以便您可以在代码中使用该对象。

要按名称导入模块,可以使用以下语法:

import module [as name]

此语句允许您通过名称导入模块。该模块必须列在 Python 的导入路径中,该路径是运行导入时基于路径的查找器搜索的位置列表。

方括号中的语法部分是可选的,允许您创建导入名称的别名。这种做法可以帮助您避免代码中的名称冲突。

举个例子,假设您有以下模块:

def add(a, b):
    return float(a + b)

def subtract(a, b):
    return float(a - b)

def multiply(a, b):
    return float(a * b)

def divide(a, b):
    return float(a / b)

该示例模块提供了允许您执行基本计算的函数。包含的模块称为“calculations.py”。要导入此模块并使用代码中的函数,请继续在保存文件的同一目录中启动 REPL 会话。

然后运行以下代码:

>>> import calculations

>>> calculations.add(2, 4)
6.0
>>> calculations.subtract(8, 4)
4.0
>>> calculations.multiply(5, 2)
10.0
>>> calculations.divide(12, 2)
6.0

此代码片段开头的 import 语句将模块名称引入到您当前的命名空间中。要使用计算中的函数或任何其他对象,您需要使用带有点符号的完全限定名称。

import calculations as calc

这种做法可以让您避免代码中的名称冲突。在某些情况下,使用限定名称时减少输入的字符数也是常见的做法。

例如,如果您熟悉 NumPy 和 pandas 等库,那么您就会知道使用以下导入是很常见的:

import numpy as np
import pandas as pd

导入模块时使用较短的别名可以通过利用限定名称来方便地使用其内容。

您还可以使用类似的语法来导入 Python 包:

import package [as name]

在这种情况下,Python 会将包的 __init__.py 文件的内容加载到当前命名空间中。如果该文件导出对象,那么这些对象将可供您使用。

最后,如果您想更具体地导入当前名称空间中的内容,则可以使用以下语法:

from module import name [as name]

使用此 import 语句,您可以从给定模块导入特定名称。当您只需要定义许多对象的长模块中的几个名称或者当您不希望代码中发生名称冲突时,建议使用此方法。

要继续使用calculations模块,您可以仅导入所需的函数:

>>> from calculations import add

>>> add(2, 4)
6.0

在此示例中,您仅使用 add() 函数。 from module import name 语法允许您显式导入目标名称。在这种情况下,其余函数和模块本身将无法在您的命名空间或范围中访问。

模块上的通配符导入

当您使用 Python 模块时,通配符导入是一种导入类型,它允许您一次性从模块中获取所有公共名称。这种类型的导入具有以下语法:

from module import *

名称通配符导入源自语句末尾的星号,表示您要从模块导入所有对象。

返回终端窗口并重新启动 REPL 会话。然后,运行以下代码:

>>> from calculations import *

>>> dir()
[
    ...
    'add',
    'divide',
    'multiply',
    'subtract'
]

在此代码片段中,您首先运行通配符导入。此导入使 calculations 模块中的所有名称都可用,并将它们带入您当前的命名空间。内置的 dir() 函数允许您查看当前命名空间中可用的名称。您可以从输出中确认,calculations 中的所有函数现在都可用。

当您完全确定需要给定模块定义的所有对象时,使用通配符导入是一个快速的解决方案。实际上,这种情况很少见,您最终只会用不需要的对象和名称弄乱您的命名空间。

PEP 8 中明确不鼓励使用通配符导入,因为他们说:

通配符导入(from import *应该避免,因为它们使得命名空间中存在哪些名称变得不清楚,从而使读者和许多自动化工具感到困惑。通配符导入有一个合理的用例,即重新发布内部接口作为公共 API 的一部分(例如,使用可选加速器模块中的定义覆盖接口的纯 Python 实现,以及哪些定义将被覆盖)提前不知道被覆盖)。 (来源)

通配符导入的主要缺点是您无法控制导入的对象。你不能具体说明。因此,您可能会让代码的用户感到困惑,并用不必要的对象扰乱他们的命名空间。

尽管不鼓励通配符导入,但某些库和工具仍然使用它们。例如,如果您搜索使用 Tkinter 构建的应用程序,那么您会发现许多示例都使用以下形式:

from tkinter import *

通过此导入,您可以访问 tkinter 模块中定义的所有对象,如果您开始学习如何使用此工具,这将非常方便。

您可能会发现许多其他工具和第三方库在其文档中使用通配符导入作为代码示例,这没关系。但是,在实际项目中,您应该避免这种类型的导入。

实际上,您无法控制代码的用户如何管理其导入。因此,您最好为通配符导入准备代码。您将在接下来的部分中了解如何执行此操作。首先,您将了解如何在包上使用通配符导入。

通配符导入和非公共名称

Python 有一个完善的命名约定,允许您告诉代码的用户模块中的给定名称何时供内部或外部使用。

如果对象的名称以单个前导下划线开头,则该名称被视为非公开,因此仅供内部使用。相反,如果名称以小写或大写字母开头,则该名称是public,因此是模块公共 API 的一部分。

当给定模块中有非公共名称时,您应该知道通配符导入不会导入这些名称。假设您有以下模块:

from math import pi as _pi

class Circle:
    def __init__(self, radius):
        self.radius = _validate(radius)

    def area(self):
        return _pi * self.radius**2

class Square:
    def __init__(self, side):
        self.side = _validate(side)

    def area(self):
        return self.side**2

def _validate(value):
    if not isinstance(value, int | float) or value <= 0:
        raise ValueError("positive number expected")
    return value

在此模块中,您有两个非公共对象 _pi_validate()。您知道这一点是因为他们的名字中有一个前导下划线。如果有人在此模块上运行通配符导入,则不会导入非公开名称:

>>> from shapes import *

>>> dir()
[
    'Circle',
    'Square',
    ...
]

如果您查看 dir() 的输出,您会发现您的文件中只有 CircleSquare 类可用。当前命名空间。非公共对象 _pi_validate() 不可用。因此,通配符导入不会导入非公开名称。

包上的通配符导入

到目前为止,您已经了解通配符导入如何与模块一起使用。您还可以对包使用这种类型的导入。在这种情况下,语法是相同的,但您需要使用包名称而不是模块名称:

from package import *

现在,当您运行这种类型的导入时会发生什么?您可能期望此导入会导致 Python 搜索文件系统,找到包中存在的模块和子包,然后导入它们。

但是,执行此文件系统搜索可能需要很长时间。此外,导入模块可能会产生不需要的副作用,因为导入模块时,该模块中的所有可执行代码都会运行。

由于这些潜在问题,Python 有 __all__ 特殊变量,它允许您显式定义要在给定包中公开给通配符导入的模块列表。您将在下一节中探索详细信息。

使用 __all__ 为通配符导入准备包

Python 在处理包上的通配符导入时有两种不同的行为。这两种行为都取决于包的 __init__.py 文件中是否存在 __all__ 变量。

  • 如果 __init__.py 未定义 __all__,那么当您在包上运行通配符导入时不会发生任何事情。
  • 如果__init__.py定义了__all__,那么其中列出的对象将被导入。

为了说明第一个行为,请继续创建一个名为 shapes/ 的新文件夹。在文件夹内,创建以下文件:

shapes/
├── __init__.py
├── circle.py
├── square.py
└── utils.py

暂时将 __init__.py 文件留空。获取 shapes.py 文件的代码并将其拆分为其余文件。单击下面的可折叠部分查看如何执行此操作:

from math import pi as _pi

from shapes.utils import validate

class Circle:
    def __init__(self, radius):
        self.radius = validate(radius)

    def area(self):
        return _pi * self.radius**2
from shapes.utils import validate

class Square:
    def __init__(self, side):
        self.side = validate(side)

    def area(self):
        return self.side**2
def validate(value):
    if not isinstance(value, int | float) or value <= 0:
        raise ValueError("positive number expected")
    return value

在此示例包中,__init__.py 文件未定义 __all__ 变量。因此,如果您在此包上运行通配符导入,则不会将任何名称导入到您的命名空间中:

>>> from shapes import *

>>> dir()
[
    '__annotations__',
    '__builtins__',
    ...
]

在此示例中,dir() 函数显示通配符导入并未为当前命名空间带来任何名称。 circlesquareutils 模块在您的命名空间中不可用。

如果您没有在包中定义 __all__,则语句 from package import * 不会将目标包中的所有模块导入到当前命名空间中。在这种情况下,导入语句仅确保包已导入并运行 __init__.py 中的任何代码。

如果要准备用于通配符导入的 Python 包,则需要在包的 __init__.py 文件中定义 __all__ 变量。 __all__ 变量应该是一个字符串列表,其中包含当有人使用通配符导入时您想要从包中导出的名称。

继续将以下行添加到文件中:

__all__ = ["circle", "square"]

通过在 __init__.py 文件中定义 __all__ 变量,您可以建立通配符导入将带入命名空间的模块名称。在这种情况下,您只想从包中导出 circlesquare 模块。

现在,在交互式会话中运行以下代码:

>>> from shapes import *

>>> dir()
[
    ...
    'circle',
    'square'
]

现在,当您在 shapes 包上运行通配符导入时,circlesquare 模块将在您的命名空间中可用。请注意,utils 模块不可用,因为您没有在 __all__ 中列出它。

作为软件包作者,您有责任构建此列表并使其保持最新。当您发布软件包的新版本时,保持列表最新至关重要。在这种情况下,还需要注意的是,如果 __all__ 包含未定义的名称,您将收到 AttributeError 异常。

最后,如果您将 __all__ 定义为空列表,则不会从您的包中导出任何内容。这就像没有在包中定义 __all__ 一样。

使用 __all__ 公开模块和包中的名称

您已经知道,当您在模块上运行通配符导入时,您将导入该模块中的所有公共常量、变量、函数、类和其他对象。有时,这种行为是可以的。但是,在某些情况下,您需要对模块导出的内容进行精细控制。您还可以使用 __all__ 来实现此目标。

__all__ 的另一个有趣的用例是当您需要从包中导出特定名称或对象时。在这种情况下,您还可以以稍微不同的方式使用 __all__

在以下部分中,您将学习如何使用 __all__ 来控制模块导出的名称以及如何从包中导出特定名称。

模块中的名称

您可以使用 __all__ 变量显式控制模块向通配符导入公开的名称。从这个意义上说,__all__允许您建立模块的公共接口或API。这种技术也是一种显式传达模块 API 的方法。

如果您有一个包含许多公共名称的大型模块,那么您可以使用 __all__ 来创建可导出名称的列表,以便通配符导入不会污染代码用户的命名空间。

一般来说,模块可以有几种不同类型的名称:

  • 公共名称是模块公共接口的一部分。
  • 非公开名称仅供内部使用。
  • 导入的名称是模块作为公共或非公共名称导入的名称。

如您所知,公共名称是以小写或大写字母开头的名称。非公开名称是以单个下划线开头的名称。

最后,导入的名称是在模块中作为公共名称导入的名称。这些名称也从该模块导出。因此,这就是为什么您会在许多代码库中看到如下所示的导入:

import sys as _sys

在此示例中,您将 sys 模块导入为 _sysas 说明符允许您为导入的对象创建别名。在这种情况下,别名是非公开名称。通过在导入语句中添加这个微小的内容,当有人在模块上使用通配符导入时,可以防止 sys 被导出。

因此,如果您不想从模块导出导入的对象,请使用 as 说明符和导入对象的非公共别名。

理想情况下,__all__ 列表应仅包含在包含模块中定义的公共名称。举个例子,假设您有以下模块,其中包含允许您发出 HTTP 请求的函数和类:

import requests

__all__ = ["get_page_content", "WebPage"]

BASE_URL = "http://example.com"

def get_page_content(page):
    return _fetch_page(page).text

def _fetch_page(page):
    url = f"{BASE_URL}/{page}"
    return requests.get(url)

class WebPage:
    def __init__(self, page):
        self.response = _fetch_page(page)

    def get_content(self):
        return self.response.text

在此示例模块中,您导入 requests 库。接下来,定义 __all__ 变量。在此示例中,__all__ 包括 get_page_content() 函数和 WebPage 类,它们都是公共名称。

请注意,辅助函数 _fetch_page() 仅供内部使用。因此,您不想将其暴露给通配符导入。此外,您不希望 BASE_URL 常量或导入的 requests 模块暴露给通配符导入。

以下是模块响应通配符导入的方式:

>>> from webreader import *

>>> dir()
[
    'WebPage',
    ...
    'get_page_content'
]

当您在 webreader 模块上运行通配符导入时,仅导入 __all__ 中列出的名称。现在继续注释掉定义 __all__ 的行,重新启动 REPL 会话,然后再次运行导入:

>>> from webreader import *

>>> dir()
[
    'BASE_URL',
    'WebPage',
    ...
    'get_page_content',
    'requests'
]

快速查看 dir() 的输出显示,现在您的模块导出了所有公共名称,包括 BASE_URL 甚至导入的 requests 库。

__all__ 变量可让您完全控制模块向通配符导入公开的内容。但是,请注意,__all__ 不会阻止您使用显式导入从模块导入特定名称:

>>> from webreader import _fetch_page

>>> dir()
[
    ...
    '_fetch_page'
]

请注意,您可以使用显式导入从给定模块中引入任何名称,甚至是上例中 _fetch_page() 等非公共名称。

包中的名称

在上一节中,您学习了如何使用 __all__ 定义向通配符导入公开哪些对象。有时,您想要在包级别执行类似的操作。如果您想控制包向通配符导入公开的对象和名称,那么您可以在包的 __init__.py 文件中执行类似以下操作:

from module_0 import name_0, name_1, name_2, name_3
from module_1 import name_4, name_5, name_6

__all__ = [
    "name_0",
    "name_1",
    "name_2",
    "name_3",
    "name_4",
    "name_5",
    "name_6",
]

import 语句告诉 Python 从包中的每个模块中获取名称。然后,在 __all__ 中,将导入的名称列为字符串。如果您的包包含许多模块,并且您希望提供直接的导入路径,则此技术非常有用。

作为此技术在实践中如何工作的示例,请返回到 shapes 包并更新 __init__.py 文件,如以下代码所示:

from shapes.circle import Circle
from shapes.square import Square

__all__ = ["Circle", "Square"]

在此更新中,您添加了两个显式导入,以从各自的模块获取 CircleSquare 类。然后,将类名作为字符串添加到 __all__ 变量中。

以下是该包现在如何响应通配符导入:

>>> from shapes import *

>>> dir()
[
    'Circle',
    'Square',
    ...
]

您的 shapes 包向通配符导入公开了 CircleSquare 类。这些类是您定义为包的公共接口的类。请注意此技术如何促进对名称的直接访问,否则您将必须通过限定名称导入。

探索 Python 中 __all__ 的替代用例

除了允许您控制模块和包向通配符导入公开的内容之外,__all__ 变量还可以用于其他目的。您可以使用 __all__ 来迭代构成包或模块的公共接口的名称和对象。当您需要公开 dunder 名称时,您还可以利用 __all__

迭代包的接口

由于 __all__ 通常是一个 list 对象,因此您可以使用它来迭代构成模块接口的对象。使用 __all__ 相对于 dir() 的优点是,包作者已明确定义了他们认为属于包公共接口一部分的名称。如果您迭代 __all__,则无需像迭代 dir(module) 时那样过滤掉非公共名称。

例如,假设您有一个模块,其中有几个共享相同接口的相似类。这是一个玩具示例:

__all__ = ["Car", "Truck"]

class Car:
    def start(self):
        print("The car is starting")

    def drive(self):
        print("The car is driving")

    def stop(self):
        print("The car is stopping")

class Truck:
    def start(self):
        print("The truck is starting")

    def drive(self):
        print("The truck is driving")

    def stop(self):
        print("The truck is stopping")

在本模块中,您有两个代表车辆的类。它们共享相同的界面,因此您可以在类似的地方使用它们。您还定义了 __all__ 变量,将两个类列为字符串。

现在假设您想在循环中使用这些类。你怎么能这样做呢?您可以使用 __all__ ,如下面的代码所示:

>>> import vehicles

>>> for v in vehicles.__all__:
...     vehicle = getattr(vehicles, v)()
...     vehicle.start()
...     vehicle.drive()
...     vehicle.stop()
...
The car is starting
The car is driving
The car is stopping
The truck is starting
The truck is driving
The truck is stopping

在此示例中,您首先导入 vehicles 模块。然后,对 __all__ 变量启动一个 for 循环。由于 __all__ 是一个字符串列表,因此您可以使用内置的 getattr() 函数从 vehicles 访问指定的对象。这样,您就迭代了构成模块公共 API 的类。

访问非公开名称和 Dunder 名称

当您编写模块和包时,有时您会使用以双下划线开头和结尾的模块级名称。这些名称通常称为dunder 名称。您可能需要向通配符导入公开一些 dunder 常量,例如 __version__ 和 __author__ 。

请记住,默认行为是不导入这些名称,因为它们以下划线开头。要解决此问题,您可以在 __all__ 变量中显式列出这些名称。

为了说明这种做法,请返回您的 webreader.py 文件并更新它,如下面的代码所示:

import requests

__version__ = "1.0.0"
__author__ = "Real Python"

__all__ = ["get_page_content", "WebPage", "__version__", "__author__"]

BASE_URL = "http://example.com"

def get_page_content(page):
    return _fetch_page(page).text

# ...

在此更新中,您定义了两个使用 dunder 名称的模块级常量。第一个常量提供有关模块版本的信息,第二个常量保存作者的姓名。

以下是通配符导入在此模块上的工作原理:

>>> from webreader import *

>>> dir()
[
    'WebPage',
    ...
    '__author__',
    ...
    '__version__',
    'get_page_content'
]

现在,当有人在 webreader 模块上使用通配符导入时,他们会将 dunder 变量导入到其命名空间中。

在 Python 中使用 __all__:优点和最佳实践

到目前为止,您已经了解了很多关于 __all__ 变量以及如何在代码中使用它的知识。虽然您不需要使用 __all__,但它使您可以完全控制包和模块向通配符导入公开的内容。

__all__ 变量也是一种向包和模块的用户传达他们应该将代码的哪些部分用作公共接口的方法。

以下是 __all__ 可以提供的主要优势的快速摘要:

  • 控制向通配符导入公开的内容:使用 __all__ 允许您显式指定包和模块的公共接口。这种做法可以防止意外使用不应从模块外部使用的对象。它在模块的内部实现与其公共 API 之间提供了清晰的界限。
  • 增强可读性:使用 __all__ 可以让其他开发人员快速了解哪些对象构成了代码的 API,而无需检查整个代码库。这提高了代码的可读性并节省了时间,特别是对于具有多个模块的大型项目。
  • 减少命名空间混乱:使用 __all__ 允许您列出要向通配符导入公开的名称。这样,您就可以防止其他开发人员使用不必要或冲突的名称污染他们的名称空间。

尽管 Python 中不鼓励导入通配符,但您无法控制代码的用户在使用代码时将执行的操作。因此,使用 __all__ 是限制代码错误使用的好方法。

以下是在代码中使用 __all__最佳实践的快速列表:

  • 尝试始终在包和模块中定义__all__此变量使您可以明确控制其他开发人员可以使用通配符导入来导入的内容。
  • 利用__all__作为显式定义包和模块的公共接口的工具。这种做法可以让其他开发人员清楚哪些对象供外部使用,哪些对象供外部使用。仅供内部使用。
  • 保持 __all__ 的焦点。__all__ 变量不应包含模块中的每个对象,而应包含公共 API 的一部分。
  • __all__ 与良好的文档结合使用。有关公共 API 中每个对象的预期用途和行为的清晰文档是对 __all__ 的最佳补充。
  • 在所有包和模块中使用 __all__ 保持一致。这种做法可以让其他开发人员更好地了解如何使用您的代码。
  • 定期检查和更新 __all____all__ 变量应始终反映代码 API 中的最新更改。定期维护 __all__ 可确保您的代码保持干净且可用。

最后,请记住 __all__ 仅影响通配符导入。如果您的代码的用户从包或模块中导入特定对象,那么即使 __all__ 中没有列出该对象,该对象也会被导入。

结论

现在您知道 Python 中的通配符导入是什么了。您已经了解到,这些导入允许您快速从模块和包中获取所有公共对象。为了控制该过程,Python 有 __all__ 变量,您可以在模块和包中将其定义为可用于通配符导入的对象列表。

__all__ 变量是一个字符串列表,其中每个字符串代表变量、函数、类或模块的名称。

在本教程中,您学习了如何:

  • 在 Python 中使用通配符导入
  • 使用 __all__ 控制您向通配符导入公开的模块
  • 控制您在模块中公开的名称
  • 探索 __all__ 变量的其他用例
  • 了解使用 __all__好处最佳实践

有了这些知识,您现在可以编写更健壮、可读且可靠的模块和包,并具有针对代码中通配符导入的显式行为。

相关文章