Запретить пакеты Python при повторном экспортировании импортированных имен

В пакете Python у меня есть файловая структура

package/
    __init__.py
    import_me.py

Предполагается, что файл import_me.py предоставляет фрагменты функций:

import re
import sys

def hello():
    pass

чтобы package.import_me.hello можно было импортировать динамически через import. К сожалению, это также позволяет импортировать re и sys как package.import_me.re и package.import_me.sys соответственно.

Есть ли способ предотвратить повторный экспорт импортированных модулей в import_me.py? Предпочтительно это должно выходить за рамки искажения имени или префикса подчеркивания импортированных модулей, поскольку в моем случае это может создать проблему безопасности в некоторых случаях.

Ответы

Ответ 1

Нет простого способа запретить импорт глобального имени из модуля; Python просто не построен таким образом.

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

Что вы можете сделать, это импортировать зависимые модули с лидирующим знаком подчеркивания, который является стандартной идиомой Python для передачи "подробностей реализации, использования на свой страх и риск":

import re as _re
import sys as _sys

def hello():
    pass

Заметка

Хотя просто удаление импортированных модулей как способ не позволить им импортироваться, похоже, что это может сработать, на самом деле это не так:

import re
import sys

def hello():
    sys
    print('hello')

del re
del sys

а затем импортировать и использовать hello:

>>> import del_mod
>>> del_mod.hello()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "del_mod.py", line 5, in hello
    sys
NameError: global name 'sys' is not defined

Ответ 2

Нет простого способа запретить импорт глобального имени из модуля; но на самом деле вам не нужно. Python позволяет использовать локальный импорт вместо глобального:

def foo():
    import sys
    print(sys.copyright)

sys.copyright # Throws NameError

Уютный и простой.

На самом деле, я считаю, что использование локального импорта должно быть хорошей практикой, а глобальные - лишь данью C или наследию.

UPD: Очевидным недостатком является то, что import sys statement будет выполняться каждый раз, когда вызывается эта функция, что может быть недоступным. Но вместо этого вы можете создать вызываемый объект:

class Callable(object):
    import sys as _sys
    def __call__(self):
        print(self._sys.copyright)

foo = Callable()
foo()

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

Ответ 4

1. Функция инициализации

Альтернативой может быть обертывание определений в функцию инициализации.

## --- exporttest.py ---
def _init():
    import os                       # effectively hidden

    global get_ext                  # effectively exports it
    def get_ext(filename):
        return _pointless_subfunc(filename)

                                      # underscore optional, but good 
    def _pointless_subfunc(filename): # for the sake of documentation
        return os.path.splitext(filename)[1]

    if __name__ == '__main__':      # for interactive debugging (or doctest)  
        globals().update(locals())  # we want all definitions accessible
        import doctest
        doctest.testmod()

_init()

print('is ''get_ext'' accessible?           ', 'get_ext' in globals())
print('is ''_pointless_subfunc'' accessible?', '_pointless_subfunc' in globals())
print('is ''os'' accessible?                ', 'os' in globals())

Для сравнения:

>>> python3 -m exporttest
is ''get_ext'' accessible?            True
is ''_pointless_subfunc'' accessible? True
is ''os'' accessible?                 True

>>> python3 -c "import exporttest"
is ''get_ext'' accessible?            True
is ''_pointless_subfunc'' accessible? False
is ''os'' accessible?                 False

1.1. преимущества

  • Фактическое скрытие импорта.
  • Более удобный для интерактивного поиска кода, поскольку dir(exporttest) беспорядок.

1.2. Недостатки

  • К сожалению, в отличие от import MODULE as _MODULE шаблона import MODULE as _MODULE, он не играет хорошо с pylint.

    C:  4, 4: Invalid constant name "get_ext" (invalid-name)
    W:  4, 4: Using global for 'get_ext' but no assignment is done (global-variable-not-assigned)
    W:  5, 4: Unused variable 'get_ext' (unused-variable)
    

2. Объятия __all__

После дальнейшего чтения я обнаружил, что питонический способ сделать это - полагаться на __all__. Он контролирует не только то, что экспортируется from MODULE import *, но и то, что появляется в help(MODULE), и в соответствии с мантрой "Мы все взрослые здесь" это собственная ошибка пользователей, если он использует все, что не задокументировано как общественности.

2.1. преимущества

Инструмент имеет лучшую поддержку для этого подхода (например, посредством поддержки редактора для автомиграций через библиотеку importmagic).

2.2. Недостатки

Лично я нахожу, что целая "мы все взрослые" мантра весьма наивна, но это питонический путь.

Ответ 5

EDIT: Это не работает в большинстве случаев. См. Другие ответы.


Я знаю, что этот вопрос старый, но если вы просто

import re
import sys

def hello():
    pass

del re
del sys

то вы не сможете импортировать re или sys из import_me.py, и таким образом вам не нужно перемещать импорт из начала файла.