Стиль кодирования импорта Python - PullRequest
62 голосов
/ 25 января 2009

Я обнаружил новый паттерн. Хорошо ли известен этот образец или каково его мнение?

По сути, мне трудно перебирать исходные файлы вверх и вниз, чтобы выяснить, какие модули импорта доступны и т. Д., Теперь вместо

import foo
from bar.baz import quux

def myFunction():
    foo.this.that(quux)

Я перемещаю все свои операции импорта в функцию, в которой они фактически используются. Например:

def myFunction():
    import foo
    from bar.baz import quux

    foo.this.that(quux)

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

Во-вторых, я редко сталкиваюсь со списком импортных товаров в верхней части моих модулей, половина или больше которых мне больше не нужны, потому что я реорганизовал их. Наконец, я нахожу этот шаблон НАМНОГО проще для чтения, поскольку каждое упомянутое имя находится прямо в теле функции.

Ответы [ 11 ]

106 голосов
/ 25 января 2011

(Ранее) голос с самым высоким рейтингом на этот вопрос красиво отформатирован, но абсолютно не соответствует производительности. Позвольте мне продемонстрировать

Performance

Топ импорта

import random

def f():
    L = []
    for i in xrange(1000):
        L.append(random.random())


for i in xrange(1000):
    f()

$ time python import.py

real        0m0.721s
user        0m0.412s
sys         0m0.020s

Импорт в теле функции

def f():
    import random
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(1000):
    f()

$ time python import2.py

real        0m0.661s
user        0m0.404s
sys         0m0.008s

Как видите, импорт модуля в функцию может быть более эффективным. Причина этого проста. Он перемещает ссылку из глобальной ссылки в локальную ссылку. Это означает, что по крайней мере для CPython компилятор будет выдавать LOAD_FAST инструкции вместо LOAD_GLOBAL инструкций. Это, как следует из названия, быстрее. Другой отвечающий искусственно завышал производительность при просмотре в sys.modules путем импорта при каждой итерации цикла .

Как правило, лучше импортировать сверху, но производительность не причина, по которой вы часто обращаетесь к модулю. Причины в том, что можно легче отследить, от чего зависит модуль, и это согласуется с большей частью остальной части вселенной Python.

53 голосов
/ 25 января 2009

У этого есть несколько недостатков.

Тестирование

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

import mymodule
mymodule.othermodule = module_stub

Тебе придется сделать

import othermodule
othermodule.foo = foo_stub

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

Отслеживание зависимостей

Это делает неочевидным, от каких модулей зависит ваш модуль. Это особенно раздражает, если вы используете много сторонних библиотек или реорганизуете код.

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

Примечания по производительности

Из-за того, что модули Python кэшируют, производительность не падает. На самом деле, поскольку модуль находится в локальном пространстве имен, есть небольшой выигрыш в производительности при импорте модулей в функцию.

Top Import

import random

def f():
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(10000):
    f()


$ time python test.py 

real   0m1.569s
user   0m1.560s
sys    0m0.010s

Импорт в теле функции

def f():
    import random
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(10000):
    f()

$ time python test2.py

real    0m1.385s
user    0m1.380s
sys     0m0.000s
21 голосов
/ 25 января 2009

Несколько проблем с этим подходом:

  • Не сразу видно при открытии файла, от каких модулей он зависит.
  • Это запутает программы, которые должны анализировать зависимости, такие как py2exe, py2app и т. Д.
  • А как насчет модулей, которые вы используете во многих функциях? Вы либо получите много избыточных импортов, либо вам придется иметь некоторые в верхней части файла и некоторые внутренние функции.

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

В некоторых ситуациях, когда я обнаружил полезными импорт внутри функций:

  • Чтобы справиться с циклическими зависимостями (если вы действительно не можете их избежать)
  • Код платформы

Кроме того: размещение импорта внутри каждой функции на самом деле не заметно медленнее, чем в верхней части файла. При первой загрузке каждого модуля он помещается в sys.modules, а каждый последующий импорт требует только времени на поиск модуля, что довольно быстро (он не перезагружается).

10 голосов
/ 25 января 2009

Еще одна полезная вещь, которую стоит отметить, это то, что синтаксис from module import * внутри функции был удален в Python 3.0.

Здесь есть краткое упоминание об этом в разделе "Удаленный синтаксис":

http://docs.python.org/3.0/whatsnew/3.0.html

4 голосов
/ 25 января 2009

Я бы посоветовал вам попытаться избежать from foo import bar импорта. Я использую их только в пакетах, где разбиение на модули - это деталь реализации, и в любом случае их не будет много.

Во всех других местах, где вы импортируете пакет, просто используйте import foo, а затем укажите его полное имя foo.bar. Таким образом, вы всегда можете сказать, откуда происходит определенный элемент, и вам не нужно вести список импортированных элементов (в действительности это всегда будет устаревшим и импортировать уже не используемые элементы).

Если foo - действительно длинное имя, вы можете упростить его с помощью import foo as f и затем написать f.bar. Это по-прежнему намного удобнее и понятнее, чем поддержка всего импорта from.

3 голосов
/ 26 января 2009

Люди очень хорошо объяснили, почему следует избегать встроенного импорта, а не альтернативных рабочих процессов, чтобы в первую очередь устранить причины, по которым вы хотите их получить.

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

Для проверки неиспользованного импорта я использую pylint . Он выполняет статический (ish) -анализ кода Python, и одна из (многих) проверяемых вещей - неиспользуемый импорт. Например, следующий скрипт ..

import urllib
import urllib2

urllib.urlopen("http://stackoverflow.com")

.. выдаст следующее сообщение:

example.py:2 [W0611] Unused import urllib2

Что касается проверки доступного импорта, я обычно полагаюсь на завершение TextMate (довольно упрощенное) - когда вы нажимаете клавишу Esc, оно завершает текущее слово с другими в документе. Если я сделал import urllib, urll[Esc] расширится до urllib, если нет, я перейду к началу файла и добавлю импорт.

2 голосов
/ 25 января 2009

Возможно, вы захотите взглянуть на накладные расходы на импорт в вики Python. Вкратце: если модуль уже был загружен (посмотрите на sys.modules), ваш код будет работать медленнее. Если ваш модуль еще не был загружен и будет foo загружаться только при необходимости, что может быть равно нулю, тогда общая производительность будет лучше.

2 голосов
/ 25 января 2009

Я считаю, что это рекомендуемый подход в некоторых случаях / сценариях. Например, в Google App Engine рекомендуется лениво загружать большие модули, поскольку это сведет к минимуму затраты на прогрев экземпляров новых виртуальных машин / интерпретаторов Python. Взгляните на Google Engineer's презентацию, описывающую это. Однако имейте в виду, что не означает , что означает, что вы должны загружать все свои модули лениво.

2 голосов
/ 25 января 2009

С точки зрения производительности вы можете увидеть это: Должны ли операторы импорта Python всегда быть в верхней части модуля?

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

0 голосов
/ 30 апреля 2018

Оба варианта имеют свое применение. Однако в большинстве случаев лучше импортировать за пределы функций, а не внутри них.

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__), потому что это позволяет им делать то, что они (вероятно) всегда делали.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...