导出装饰器,用于管理__all__。

6
一个合适的Python模块将会在名为__all__的列表中列出所有的公共符号。管理这个列表可能会很繁琐,因为你需要把每个符号列出两次。当然有更好的方法,可能使用装饰器,这样只需将导出的符号注释为@export
你会如何编写这样的装饰器?我相信有不同的方法,所以我想看到几个答案,有足够的信息让用户对比各种方法。
6个回答

6

使用装饰器为__all__添加名称是否是好的实践?一文中,Ed L建议将以下内容包含在某个实用程序库中:

import sys

def export(fn):
    """Use a decorator to avoid retyping function/class names.

    * Based on an idea by Duncan Booth:
      http://groups.google.com/group/comp.lang.python/msg/11cbb03e09611b8a
    * Improved via a suggestion by Dave Angel:
      http://groups.google.com/group/comp.lang.python/msg/3d400fb22d8a42e1
    """
    mod = sys.modules[fn.__module__]
    if hasattr(mod, '__all__'):
        name = fn.__name__
        all_ = mod.__all__
        if name not in all_:
            all_.append(name)
    else:
        mod.__all__ = [fn.__name__]
    return fn

我们已经修改了名称以匹配其他示例。如果将其放入本地的实用程序库中,您只需要编写:
from .utility import export

然后开始使用@export。只需一行惯用的Python代码,你就能轻松上手。不过,该模块需要通过使用__module__属性和sys.modules缓存来访问模块,这两者在某些更为深奥的设置中(如自定义导入机制或从另一个模块包装函数以创建此模块中的函数)可能会有问题。 atpublic 软件包Barry Warsaw开发的Python部分也实现了类似的功能。它也提供了基于关键字的语法,但装饰器变体依赖于与上述相同的模式。

这篇很棒的答案Aaron Hall提出了一个非常相似的建议,只是增加了两行代码,因为它没有使用__dict__.setdefault。如果由于某种原因操纵模块__dict__有问题,那么这可能更可取。


1
既然这是一个社区wiki,我已经合并了逻辑,以避免直接使用__dict__。如果你同意,你可以标记上面的评论以供删除。我还会改进其他方面,比如将注释从文档字符串移至回答的末尾,并改进文档字符串以说明用法,以便可能通过doctest测试。对于这个特定问题,我对自己写答案不感兴趣。 - Russia Must Remove Putin
顺便提一下,这不是可传递的。因此,在模块module.py中装饰东西,然后在module.py目录下的__init__.py中执行from module import *将从module导入所有内容,而不仅仅是用export装饰的内容。 - lo tolmencre

4
您可以像这样在模块级别上声明修饰符:

__all__ = []

def export(obj):
    __all__.append(obj.__name__)
    return obj

如果您只在单个模块中使用此代码,那么这非常完美。它只有4行代码(加上一些空行以符合典型的格式化规范),在不同的模块中重复使用并不会太昂贵,但在这些情况下,它确实感觉像是代码重复。


3
您可以在一些实用库中定义以下内容:
def exporter():
    all = []
    def decorator(obj):
        all.append(obj.__name__)
        return obj
    return decorator, all

export, __all__ = exporter()
export(exporter)

# possibly some other utilities, decorated with @export as well

然后在您的公共库中,您可以这样做:
from . import utility

export, __all__ = utility.exporter()

# start using @export

在这里使用该库只需要两行代码。它将__all__的定义和装饰器结合在一起。因此,搜索其中一个时会找到另一个,从而帮助读者快速理解您的代码。以上方式也适用于异乎寻常的环境,例如模块可能无法从sys.modules缓存中获取,或者__module__属性已被篡改等情况。


1

1

虽然其他变体在技术上某种程度上是正确的,但人们也可以确信:

  • 如果目标模块已经声明了__all__,它将被正确处理;
  • 目标只出现一次在__all__中:
# utils.py

import sys

from typing import Any


def export(target: Any) -> Any:
  """
  Mark a module-level object as exported.

  Simplifies tracking of objects available via wildcard imports.

  """
  mod = sys.modules[target.__module__]

  __all__ = getattr(mod, '__all__', None)

  if __all__ is None:
    __all__ = []
    setattr(mod, '__all__', __all__)

  elif not isinstance(__all__, list):
    __all__ = list(__all__)
    setattr(mod, '__all__', __all__)

  target_name = target.__name__
  if target_name not in __all__:
    __all__.append(target_name)

  return target

1
这不是一个装饰器方法,但我认为它提供了你想要的效率水平。

https://pypi.org/project/auto-all/

您可以使用该包提供的两个函数来“开始”和“结束”捕获您想要包含在__all__变量中的模块对象。
from auto_all import start_all, end_all

# Imports outside the start and end functions won't be externally availab;e.
from pathlib import Path

def a_private_function():
    print("This is a private function.")

# Start defining externally accessible objects
start_all(globals())

def a_public_function():
    print("This is a public function.")

# Stop defining externally accessible objects
end_all(globals())

这个包中的函数很简单(只有几行),因此如果您想避免外部依赖,可以将它们复制到您的代码中。


1
这样做的好处是还可以处理那些不是函数或类,因此没有__name__属性的符号导出。当然,这在某些情况下非常有用。 - MvG

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接