Оба варианта имеют свое применение. Однако в большинстве случаев лучше импортировать за пределы функций, а не внутри них.
Performance
Это было упомянуто в нескольких ответах, но, по моему мнению, им всем не хватает полного обсуждения.
При первом импорте модуля в интерпретатор Python он будет работать медленно, независимо от того, находится он на верхнем уровне или внутри функции. Это медленно, потому что Python (я фокусируюсь на CPython, он может отличаться для других реализаций Python) делает несколько шагов:
- Находит пакет.
- Проверяет, был ли пакет уже преобразован в байт-код (известный каталог
__pycache__
или файлы .pyx
), и если нет, он преобразует их в байт-код.
- Python загружает байт-код.
- Загруженный модуль помещается в
sys.modules
.
Последующий импорт не должен делать все это, потому что Python может просто вернуть модуль из sys.modules
. Поэтому последующий импорт будет намного быстрее.
Возможно, что функция в вашем модуле на самом деле используется не очень часто, но это зависит от import
, который занимает довольно много времени. Тогда вы могли бы на самом деле переместить import
внутри функции. Это ускорит импорт вашего модуля (потому что он не должен импортировать долго загружаемый пакет), однако, когда функция будет использована в конечном итоге, она будет медленной при первом вызове (потому что тогда модуль должен быть импортирован). Это может повлиять на воспринимаемую производительность, потому что вместо того, чтобы замедлять всех пользователей, вы замедляете только тех, которые используют функцию, которая зависит от зависимости медленной загрузки.
Однако поиск в sys.modules
не бесплатный. Это очень быстро, но это не бесплатно. Так что если вы на самом деле вызываете функцию, которая import
является пакетом очень часто, вы заметите слегка ухудшенную производительность:
import random
import itertools
def func_1():
return random.random()
def func_2():
import random
return random.random()
def loopy(func, repeats):
for _ in itertools.repeat(None, repeats):
func()
%timeit loopy(func_1, 10000)
# 1.14 ms ± 20.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit loopy(func_2, 10000)
# 2.21 ms ± 138 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Это почти в два раза медленнее.
Очень важно понимать, что aaronasterling немного "обманул" в ответе . Он заявил, что выполнение импорта в функции на самом деле делает функцию быстрее. И в некоторой степени это правда. Это потому, что Python ищет имена:
- Сначала проверяется локальная область.
- Затем проверяется окружающая область.
- Затем проверяется следующая окружающая область действия
- ...
- Глобальная область действия проверена.
Таким образом, вместо проверки локальной области, а затем проверки глобальной области, достаточно проверить локальную область, поскольку имя модуля доступно в локальной области. Это на самом деле делает это быстрее! Но это техника, которая называется «Циклически-инвариантное движение кода» . Это в основном означает, что вы уменьшаете накладные расходы на то, что делается в цикле (или многократно), сохраняя это в переменной перед циклом (или повторными вызовами). Таким образом, вместо import
включения его в функцию вы также можете просто использовать переменную и присвоить ее глобальному имени:
import random
import itertools
def f1(repeats):
"Repeated global lookup"
for _ in itertools.repeat(None, repeats):
random.random()
def f2(repeats):
"Import once then repeated local lookup"
import random
for _ in itertools.repeat(None, repeats):
random.random()
def f3(repeats):
"Assign once then repeated local lookup"
local_random = random
for _ in itertools.repeat(None, repeats):
local_random.random()
%timeit f1(10000)
# 588 µs ± 3.92 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f2(10000)
# 522 µs ± 1.95 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f3(10000)
# 527 µs ± 4.51 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Хотя вы можете ясно видеть, что выполнение повторного поиска для глобального random
медленное, практически нет разницы между импортом модуля внутри функции или назначением глобального модуля в переменной внутри функции.
Это можно довести до крайности, также избегая поиска функции внутри цикла:
def f4(repeats):
from random import random
for _ in itertools.repeat(None, repeats):
random()
def f5(repeats):
r = random.random
for _ in itertools.repeat(None, repeats):
r()
%timeit f4(10000)
# 364 µs ± 9.34 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f5(10000)
# 357 µs ± 2.73 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Снова намного быстрее, но между импортом и переменной почти нет различий.
Необязательные зависимости
Иногда импорт на уровне модуля может быть проблемой. Например, если вы не хотите добавлять другую зависимость во время установки, но модуль был бы действительно полезен для некоторых дополнительных функциональных возможностей. Решение о том, должна ли зависимость быть необязательной, не должно приниматься легкомысленно, потому что это повлияет на пользователей (либо если они получат неожиданный ImportError
, либо иным образом пропустят «интересные функции»), и усложнит установку пакета со всеми функциями для нормальных зависимостей pip
или conda
(просто чтобы упомянуть два менеджера пакетов) работают «из коробки», но для необязательных зависимостей пользователи должны позже устанавливать пакеты вручную (есть некоторые опции, которые позволяют настроить требования, но опять же бремя установки «правильно» ложится на пользователя).
Но, опять же, это можно сделать двумя способами:
try:
import matplotlib.pyplot as plt
except ImportError:
pass
def function_that_requires_matplotlib():
plt.plot()
или
def function_that_requires_matplotlib():
import matplotlib.pyplot as plt
plt.plot()
Это может быть более настраиваемым путем предоставления альтернативных реализаций или настройки исключения (или сообщения), которое видит пользователь, но это основная суть.
Подход верхнего уровня мог бы быть немного лучше, если кто-то хочет предоставить альтернативное «решение» для необязательной зависимости, однако обычно люди используют импорт в функции. Главным образом потому, что это приводит к более чистой трассировке стека и короче.
Круговой импорт
Импорт в функции может быть очень полезен, чтобы избежать ImportErrors из-за циклического импорта. Во многих случаях циклический импорт является признаком «плохой» структуры пакета, но если абсолютно невозможно избежать циклического импорта, «круг» (и, следовательно, проблемы) решается путем помещения импорта, приводящего к кругу, внутрь функции, которые на самом деле его используют.
Не повторяйся
Если вы фактически поместите все функции импорта в функцию вместо области видимости модуля, вы добавите избыточность, поскольку вполне вероятно, что функции требуют одинакового импорта. Это имеет несколько недостатков:
- Теперь у вас есть несколько мест, чтобы проверить, не устарел ли какой-либо импорт.
- В случае, если вы неправильно ввели какой-либо импорт, вы узнаете это только при запуске определенной функции, а не во время загрузки. Поскольку у вас больше операторов импорта, вероятность ошибки возрастает (не намного), и она становится чуть более важной для тестирования всех функций.
Дополнительные мысли:
Я редко сталкиваюсь со списком импортных товаров в верхней части моих модулей, половина или больше которых мне больше не нужны, потому что я реорганизовал их.
В большинстве сред IDE уже есть средство проверки неиспользуемых импортов, поэтому для их удаления достаточно всего лишь нескольких щелчков мышью. Даже если вы не используете IDE, время от времени вы можете использовать скрипт проверки статического кода и исправить это вручную. В другом ответе упоминается пилинт, но есть и другие (например, пифлакс).
Я редко случайно загрязняю свои модули содержимым других модулей
Именно поэтому вы обычно используете __all__
и / или определяете свои подмодули функций и импортируете только соответствующие классы / функции / ... в основной модуль, например __init__.py
.
Также, если вы считаете, что вы слишком сильно загрязнили пространство имен модуля, вам, вероятно, следует рассмотреть возможность разбиения модуля на подмодули, однако это имеет смысл только для десятков импортов.
Еще один (очень важный) момент, который следует упомянуть, если вы хотите уменьшить загрязнение пространства имен, - это исключить импорт from module import *
. Но вы также можете избежать импорта from module import a, b, c, d, e, ...
, который импортирует слишком много имен, и просто импортировать модуль и получить доступ к функциям с помощью module.c
.
В качестве последнего средства вы всегда можете использовать псевдонимы, чтобы избежать загрязнения пространства имен при "общем" импорте, используя: import random as _random
. Это усложнит понимание кода, но при этом станет ясно, что должно быть публично видно, а что нет. Это не то, что я бы порекомендовал, вы должны просто обновлять список __all__
(это рекомендуемый и разумный подход).
Резюме
Влияние на производительность заметно, но почти всегда оно будет микрооптимизировано, поэтому не позволяйте руководству, в котором вы размещаете импорт, руководствоваться микро-эталонами. За исключением случаев, когда зависимость действительно медленная на первом import
, и она используется только для небольшого подмножества функций. Тогда это может фактически оказать видимое влияние на воспринимаемую производительность вашего модуля для большинства пользователей.
Используйте общепринятые инструменты для определения общедоступного API, я имею в виду переменную __all__
. Поддерживать его актуальность может быть немного раздражающим, но также проверяет все функции на предмет устаревшего импорта или когда вы добавляете новую функцию для добавления всех соответствующих импортов в эту функцию. В конечном итоге вам, вероятно, придется выполнять меньше работы, обновляя __all__
.
Это действительно не имеет значения, какой вы предпочитаете, оба работают. Если вы работаете в одиночку, вы можете подумать о плюсах и минусах и сделать то, что вы считаете лучшим. Однако, если вы работаете в команде, вам, вероятно, следует придерживаться известных шаблонов (которые будут импортироваться на верхнем уровне с __all__
), потому что это позволяет им делать то, что они (вероятно) всегда делали.