Декоратор экспорта, который управляет __all__

Собственный модуль Python перечислит все его общедоступные символы в списке под названием __all__. Управление этим списком может быть утомительным, так как вам придется перечислить каждый символ дважды. Конечно, есть лучшие способы, возможно, с использованием декораторов, поэтому можно просто аннотировать экспортированные символы как @export.

Как бы вы написали такого декоратора? Я уверен, что есть разные способы, поэтому я хотел бы получить несколько ответов с достаточной информацией о том, что пользователи могут сравнивать подходы друг к другу.

Ответ 1

Вы можете просто объявить декоратор на уровне модуля следующим образом:

__all__ = []

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

Это идеально, если вы используете его только в одном модуле. В 4 строках кода (плюс, вероятно, некоторые пустые строки для типичных методов форматирования), это не слишком дорого, чтобы повторять это в разных модулях, но в таких случаях оно похоже на дублирование кода.

Ответ 2

В Является ли хорошей практикой добавлять имена в __all__, используя декоратор?, Ed L предлагает следующее, чтобы быть включенным в некоторую библиотеку утилиты:

import sys

def export(f):
    """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, all_ = f.__name__, mod.__all__
        if name not in __all__:
            all_.append(name)
    else:
        mod.__all__ = [fn.__name__]
    return f

Мы адаптировали имя для соответствия другим примерам. С этим в локальной библиотеке утилиты вы просто пишете

from .utility import export

а затем начните использовать @export. Только одна строка идиоматического Python, вы не можете получить намного проще, чем это. С другой стороны, модуль требует доступа к модулю с помощью свойства __module__ и кеша sys.modules, оба из которых могут быть проблематичными в некоторых более эзотерических установках (например, пользовательские механизмы импорта или функции переноса из другого модуль для создания функций в этом модуле).

Часть python atpublic пакет Barry Warsaw делает что-то похожее на это. Он также предлагает синтаксис на основе ключевых слов, но вариант декоратора основан на тех же шаблонах, которые были использованы выше.

Этот отличный ответ от Aaron Hall предлагает нечто очень похожее, с еще двумя строками кода, поскольку он не использует __dict__.setdefault. Возможно, было бы предпочтительным, если по какой-то причине манипулирование модулем __dict__ проблематично.

Ответ 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__ было изменено или какое-то подобное.

Ответ 4

https://github.com/russianidiot/public.py имеет еще одну реализацию такого декоратора. Его основной файл в настоящее время составляет 160 строк! Ключевыми моментами являются тот факт, что он использует модуль inspect для получения соответствующего модуля на основе текущего стека вызовов.

Ответ 5

Это не подход декоратора, но он обеспечивает уровень эффективности, который, я думаю, вам нужен.

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())

Функции в пакете тривиальны (несколько строк), поэтому их можно скопировать в код, если вы хотите избежать внешних зависимостей.