В публичном модуле нужно управлять тем, какие имена импортируются конструкцией со звёздочкой. Какой механиз...

В публичном модуле нужно управлять тем, какие имена импортируются конструкцией со звёздочкой. Какой механизм Python определяет этот набор и что происходит, если модуль его не задаёт?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Набор имён для импорта со звёздочкой задаёт переменная __all__ — последовательность строк с именами, предназначенными для экспорта. Если __all__ отсутствует, Python импортирует имена, которые не начинаются с символа подчёркивания.

__all__ влияет именно на импорт со звёздочкой и не ограничивает обычный доступ к атрибутам модуля или явный импорт конкретного имени.

Исторический контекст

Импорт со звёздочкой появился как краткий способ перенести набор имён модуля в текущую область видимости. Такой подход удобен для небольших модулей, но без явного соглашения становится неясно, какие имена считаются публичными.

Механизм __all__ позволяет модулю явно описать публичный интерфейс и не раскрывать служебные имена при импорте со звёздочкой.

Постановка проблемы

В модуле могут находиться внутренние функции, константы, импортированные зависимости и вспомогательные классы. Если набор экспортируемых имён определяется неявно, добавление нового имени может неожиданно изменить содержимое пространства имён пользователя.

Это повышает риск конфликтов имён и затрудняет поддержку API. При этом сокрытие имени через __all__ не является механизмом безопасности: атрибут всё равно может быть доступен через сам модуль, если он существует.

Подробное решение

Если модуль содержит __all__, Python использует перечисленные в нём строки как имена для импорта со звёздочкой. Эти имена должны соответствовать атрибутам модуля; __all__ может включать и имена, начинающиеся с подчёркивания.

Если __all__ не задан, используются имена модуля, не начинающиеся с подчёркивания. Это правило применяется к именным атрибутам модуля, а не к локальным переменным вызывающего кода.

__all__ = ["connect", "VERSION"] VERSION = "1.0" _private_state = {} def connect(): return True def helper(): return False

В этом примере импорт со звёздочкой получит connect и VERSION, но не _private_state и helper. При этом helper не становится недоступной: явный импорт этого имени или обращение к атрибуту модуля могут оставаться возможными.

__all__ также не меняет поведение конструкций, импортирующих конкретные имена. Кроме того, содержимое __all__ не обязано совпадать с полным набором атрибутов модуля: оно описывает только заявленный набор для конкретного вида импорта.

Главный компромисс состоит в необходимости поддерживать список __all__ в актуальном состоянии. Явный список делает публичный API предсказуемым, но при добавлении или удалении экспортируемого имени его нужно изменить вручную.

Ситуация из практики

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

Рассматривались два варианта. Можно было переименовать зависимость с ведущим подчёркиванием: это минимальное изменение, но правило остаётся неявным и не защищает от будущих конфликтов. Можно было явно объявить __all__: это требует поддержки списка, зато интерфейс модуля становится документированным и стабильным.

Выбран второй вариант. В результате новые внутренние имена не стали автоматически частью экспортируемого набора, а изменения публичного API начали проходить через явное редактирование __all__.

Что кандидаты часто упускают

  1. Означает ли отсутствие имени в __all__, что к нему нельзя обратиться через модуль?

Нет. __all__ не устанавливает приватность и не удаляет атрибут. Он управляет набором имён, используемым импортом со звёздочкой. Если имя существует в модуле, к нему обычно можно обратиться через квалифицированное имя модуля или импортировать явно.

  1. Что произойдёт, если в __all__ указать несуществующее имя?

При импорте со звёздочкой Python попытается получить каждый перечисленный атрибут. Если указанного имени в модуле нет и оно не предоставляется специальным механизмом вроде динамического доступа к атрибутам, импорт завершится ошибкой AttributeError.

Поэтому __all__ должен быть согласован с фактическим интерфейсом модуля. Это одна из причин, по которой его содержимое следует проверять тестами или статическим анализом.

  1. Распространяется ли __all__ автоматически на подмодули пакета?

Нет. __all__ принадлежит конкретному модулю или конкретному файлу и не становится глобальным списком для всего пакета. Для пакета он может описывать имена, экспортируемые из пространства имён пакета, но доступность подмодулей и их собственные публичные имена регулируются отдельно.

Поэтому при проектировании API пакета нужно явно решить, какие имена экспортирует сам пакет, а какие доступны только через отдельные подмодули.