Чи повинні оператори імпорту завжди знаходитись у верхній частині модуля?


403

PEP 08 заявляє:

Імпорт завжди розміщується у верхній частині файлу, одразу після будь-яких коментарів та доктрингів модулів, а також перед глобальними модулями та константами.

Однак якщо клас / метод / функція, яку я імпортую, використовується лише в рідкісних випадках, то, безумовно, ефективніше робити імпорт, коли це потрібно?

Чи не це:

class SomeClass(object):

    def not_often_called(self)
        from datetime import datetime
        self.datetime = datetime.now()

ефективніше за це?

from datetime import datetime

class SomeClass(object):

    def not_often_called(self)
        self.datetime = datetime.now()

Відповіді:


283

Імпорт модуля досить швидкий, але не миттєвий. Це означає що:

  • Розміщення імпорту у верхній частині модуля - це добре, оскільки це тривіальна вартість, яка сплачується лише один раз.
  • Якщо ввімкнути імпорт у межах функції, виклики на цю функцію займуть більше часу.

Тож якщо ви дбаєте про ефективність, поставте імпорт на перше місце. Перемістіть їх у функції лише в тому випадку, якщо ваше профілювання покаже, що допоможе (ви зробили профіль, щоб побачити, де найкраще підвищити продуктивність, правда ??)


Найкращі причини, з яких я бачив здійснювати лінивий імпорт, - це:

  • Додаткова підтримка бібліотеки. Якщо у вашого коду є кілька шляхів, які використовують різні бібліотеки, не порушуйте, якщо додаткова бібліотека не встановлена.
  • У __init__.pyплагіні, який можна імпортувати, але фактично не використовувати. Приклади - плагіни Bazaar, які використовують bzrlibліниву рамку завантаження.

17
Джон, це було абсолютно теоретичним питанням, тому у мене не було коду для профілю. У минулому я завжди слідкував за PEP, але сьогодні я написав якийсь код, який змусив мене замислитися, чи правильно це робити. Спасибі за вашу допомогу.
Адам Дж. Форстер

43
> Якщо розміщення імпорту в межах функції, виклики до цієї функції займатимуть більше часу. Власне, я думаю, що ця вартість оплачується лише один раз. Я читав, що Python кешує імпортний модуль, щоб знову його імпортувати лише мінімальні витрати.
линька форма

24
@halfhourhacks Python не повторно імпортує модуль, але він все одно повинен виконати кілька інструкцій, щоб побачити, чи існує модуль / є у sys.modules / тощо
Джон Мілікін,

24
-1. Введення функції імпорту не обов'язково вимагає більше часу. Будь ласка, дивіться мою відповідь на інше питання.
aaronasterling

4
Один із випадків використання - це уникнення кругового імпорту (зазвичай не є розумним, але іноді він підходить). Іноді клас A в модулі m1 викликає метод класу B в модулі m2, який створює інший екземпляр класу A. Якщо метод класу B, який конструює екземпляр класу A, імпорт запускається лише після виконання функції, яка будує екземпляр, циркулярного імпорту уникнути.
Сем Свенбьорхрістіенсенсен

80

Якщо розмістити оператор імпорту всередині функції, можна запобігти круговим залежностям. Наприклад, якщо у вас є два модулі, X.py та Y.py, і їм обом потрібно імпортувати один одного, це призведе до кругової залежності, коли ви імпортуєте один з модулів, що викликає нескінченний цикл. Якщо ви перемістите оператор імпорту в один з модулів, він не намагатиметься імпортувати інший модуль, поки функція не буде викликана, і цей модуль вже буде імпортований, тому не існує нескінченного циклу. Детальніше читайте тут - effbot.org/zone/import-confusion.htm


3
Так, але можна потрапити в пекло залежності.
eigenein

8
Якщо два модулі потрібно імпортувати один одного, з кодом щось серйозно не так.
Анна

Об'єктно-орієнтоване програмування часто призводить мене до кругових залежностей. Клас життєво важливих об'єктів може бути імпортований у кілька модулів. Для того, щоб цей об’єкт виконував власні завдання, можливо, потрібно буде звернутися до одного або декількох із цих модулів. Існують способи уникнути цього, наприклад, надіслати дані об’єкту через аргументи функцій, щоб дозволити йому отримати доступ до іншого модуля. Але бувають випадки, коли це робиться дуже протиінтуїтивно зрозумілим для ООП (зовнішній світ не повинен знати, як він виконує завдання на цій функції).
Роберт

4
Коли X потребує Y, а Y потребує X, вони є або двома частинами однієї ідеї (тобто їх слід визначати разом), або існує абстракція, що відсутня.
GLRoman

59

Я прийняв практику розміщення всього імпорту у функціях, які їх використовують, а не у верхній частині модуля.

Користь, яку я отримую, - це здатність рефакторифікувати надійніше. Коли я переміщую функцію з одного модуля на інший, я знаю, що функція продовжуватиме працювати зі всією її спадщиною тестування неушкодженою. Якщо у мене є імпорт у верхній частині модуля, коли я переміщую функцію, я вважаю, що закінчую витрачати багато часу на повний та мінімальний імпорт нового модуля. Рефакторинг IDE може зробити це неактуальним.

Існує швидке покарання, як згадується в інших місцях. Я оцінив це у своїй заявці і виявив, що це несуттєво для моїх цілей.

Також приємно бачити всі залежність модуля наперед, не вдаючись до пошуку (наприклад, grep). Однак причина, коли я дбаю про залежність модулів, полягає в тому, що я встановлюю, рефакторинг або переміщу цілу систему, що складається з декількох файлів, а не лише одного модуля. У такому випадку я все одно проведу глобальний пошук, щоб переконатися, що я маю залежності на рівні системи. Тож я не знайшов світового імпорту, щоб допомогти мені зрозуміти систему на практиці.

Зазвичай я ставлю імпорт sysвсередині if __name__=='__main__'чека, а потім передаю аргументи (як sys.argv[1:]) main()функції. Це дозволяє мені використовувати mainв контексті, де sysне було імпортовано.


4
Багато IDE полегшують це рефакторинг коду шляхом оптимізації та автоматичного імпорту необхідних модулів у ваш файл. У більшості випадків PyCharm та Eclipse для мене прийняли правильні рішення. Я б сказав, що існує спосіб отримати таку саму поведінку в emacs або vim.
brent.payne

3
Імпорт всередині оператора if у глобальному просторі імен все ще є глобальним імпортом. Це надрукує аргументи (за допомогою Python 3): def main(): print(sys.argv); if True: import sys; main();Вам потрібно було б увімкнути if __name__=='__main__'функцію для створення нового простору імен.
Дарсінон

4
Це вважає мене чудовим приводом для імпорту в рамках функцій, а не в глобальному масштабі. Я дуже здивований, що ніхто з інших не згадав це зробити з цієї ж причини. Чи є якісь істотні мінуси, окрім продуктивності та багатослівності?
algal

@algal Недоліком є ​​те, що багато пітонів ненавидять це, оскільки ви порушуєте pep codex. Ви повинні переконати членів своєї команди. Штраф за виконання мінімальний. Іноді це навіть швидше, див. Stackoverflow.com/a/4789963/362951
mit

Я вважаю надзвичайно корисним для рефакторингу розміщувати імпорт близько до місця, де я їх використовую. Більше не потрібно прокручувати до вершини та назад стільки часу. Я використовую такі IDE, як піхарм або ідея крила, а також використовую їх рефакторинг, але я не завжди хочу на них покладатися. Переміщення функцій до іншого модуля стає набагато простішим за допомогою цього альтернативного стилю імпорту, як наслідок, я рефактор набагато більше.
міт

39

Більшість часу це було б корисно для ясності та розумного виконання, але це не завжди так. Нижче наведено кілька прикладів обставин, коли імпорт модулів може жити в іншому місці.

По-перше, у вас може бути модуль з одиничним тестом форми:

if __name__ == '__main__':
    import foo
    aa = foo.xyz()         # initiate something for the test

По-друге, у вас може виникнути вимога умовно імпортувати якийсь інший модуль під час виконання.

if [condition]:
    import foo as plugin_api
else:
    import bar as plugin_api
xx = plugin_api.Plugin()
[...]

Можливо, є й інші ситуації, коли ви можете розміщувати імпорт в інших частинах коду.


14

Перший варіант дійсно більш ефективний, ніж другий, коли функція викликається або нульовою, або одноразовою. Однак при другому та наступних викликах підхід "імпортувати кожен дзвінок" насправді менш ефективний. Перегляньте посилання на техніку ледачого завантаження, яка поєднує найкращі з обох підходів, роблячи "лінивий імпорт".

Але є інші причини, ніж ефективність, чому ви можете віддавати перевагу одній над іншою. Один із підходів дозволяє комусь, хто читає код, набагато зрозумілішими залежність, яку має цей модуль. Вони також мають дуже різні характеристики відмов - перший вийде з ладу під час завантаження, якщо немає модуля "datetime", а другий не вийде з ладу, поки метод не буде викликаний.

Додано Примітка: У IronPython імпорт може бути дещо дорожчим, ніж у CPython, оскільки код в основному збирається під час імпорту.


1
Неправда, що перший працює краще: wiki.python.org/moin/PythonSpeed/…
Джейсон Бейкер

Він краще, якщо метод ніколи не викликається, тому що імпорт ніколи не відбувається.
Керт Хагенлохер

Щоправда, але вона діє гірше, якщо метод викликається не один раз. І переваги від продуктивності, які ви отримаєте від негайного імпорту модуля, в більшості випадків незначні. Винятки будуть, якщо модуль дуже великий або таких функцій багато.
Джейсон Бейкер

У світі IronPython початковий імпорт значно дорожчий, ніж у CPython;). Приклад "ледачого імпорту" у вашому посиланні - це, мабуть, найкраще загальне загальне рішення.
Керт Хагенлохер

Сподіваюся, ви не заперечуєте, але я це відредагував у вашому дописі. Це корисна інформація, яку потрібно знати.
Джейсон Бейкер

9

Курт вказує на те, що другий варіант зрозуміліший і вийде з ладу під час завантаження, а не пізніше та несподівано.

Зазвичай я не переживаю за ефективність завантаження модулів, оскільки це (а) досить швидко, і (б) в основному відбувається лише при запуску.

Якщо вам доводиться завантажувати важкі модулі у несподівані часи, можливо, має більше сенсу динамічно завантажувати їх за допомогою __import__функції та обов’язково збирати ImportErrorвинятки та обробляти їх розумним чином.


8

Я б не турбувався про ефективність завантаження модуля на передній частині занадто сильно. Пам'ять, яку займає модуль, не буде дуже великою (якщо припустити, що вона є досить модульною), а вартість запуску буде незначною.

У більшості випадків потрібно завантажити модулі у верхній частині вихідного файлу. Для когось, хто читає ваш код, це набагато простіше сказати, яка функція або об’єкт вийшов з якого модуля.

Одна з вагомих причин імпорту модуля в інше місце в коді - це якщо він використовується в операторі налагодження.

Наприклад:

do_something_with_x(x)

Я можу налагодити це за допомогою:

from pprint import pprint
pprint(x)
do_something_with_x(x)

Звичайно, інша причина імпорту модулів в інше місце в коді - це якщо вам потрібно динамічно імпортувати їх. Це тому, що у вас майже немає вибору.

Я б не турбувався про ефективність завантаження модуля на передній частині занадто сильно. Пам'ять, яку займає модуль, не буде дуже великою (якщо припустити, що вона є досить модульною), а вартість запуску буде незначною.


Ми говоримо про десятки мілісекунд вартості запуску за модуль (на моїй машині). Це не завжди є незначним, наприклад, якщо це впливає на реакцію веб-програми на натискання користувача.
Євгеній Сергєєв

6

Це лише компроміс, який може вирішити зробити тільки програміст.

Випадок 1 економить деяку пам’ять і час запуску, не імпортуючи модуль часу (і не роблячи будь-яку ініціалізацію, яка може знадобитися) до необхідності. Зауважте, що здійснення імпорту "лише при виклику" також означає робити це "кожен раз, коли викликається", тому кожен виклик після першого все ще несе додаткові накладні витрати на імпорт.

Випадок 2 заощадити час виконання і латентність шляхом імпорту DateTime заздалегідь , так що not_often_called () буде повертати швидше , коли вона буде називається, а також не піддаючись накладними витратами імпорту при кожному виклику.

Крім ефективності, легше бачити залежності модуля на передній панелі, якщо заяви про імпорт є ... вперед. Приховування їх у коді може ускладнити легко знайти, від яких модулів залежить щось.

Особисто я зазвичай слідкую за PEP, за винятком таких речей, як тести одиниць, і такі, які я не хочу завжди завантажувати, бо знаю, що вони не використовуватимуться за винятком тестового коду.


2
-1. Основні витрати на імпорт трапляються лише вперше. Витрати на пошук модуля sys.modulesможна легко компенсувати економією лише пошуку локальної назви замість глобального імені.
aaronasterling

6

Ось приклад, коли весь імпорт знаходиться на самому верху (це єдиний раз, коли мені це потрібно зробити). Я хочу мати змогу припинити підпроцес і в Un * x, і в Windows.

import os
# ...
try:
    kill = os.kill  # will raise AttributeError on Windows
    from signal import SIGTERM
    def terminate(process):
        kill(process.pid, SIGTERM)
except (AttributeError, ImportError):
    try:
        from win32api import TerminateProcess  # use win32api if available
        def terminate(process):
            TerminateProcess(int(process._handle), -1)
    except ImportError:
        def terminate(process):
            raise NotImplementedError  # define a dummy function

(Огляд: те, що сказав Джон Міллікін .)


6

Це як у багатьох інших оптимізаціях - ви жертвуєте деякою читабельністю для швидкості. Як згадував Джон, якщо ви зробили домашнє завдання з профілювання і визнали, що це є досить корисною зміною, і вам потрібна додаткова швидкість, тоді перейдіть до цього. Напевно, було б добре помітити всі інші імпорти:

from foo import bar
from baz import qux
# Note: datetime is imported in SomeClass below

4

Ініціалізація модуля відбувається лише один раз - при першому імпорті. Якщо модуль, про який йде мова, зі стандартної бібліотеки, ви, ймовірно, імпортуєте його і з інших модулів у вашій програмі. Для такого модуля, який є таким поширеним, як дата, також, ймовірно, це залежність від ряду інших стандартних бібліотек. Заява про імпорт тоді коштувала б дуже мало, оскільки інціалізація модуля вже відбулася б. Все, що він робить у цей момент, - прив'язування існуючого об'єкта модуля до локальної області.

З'єднайте цю інформацію з аргументом для читабельності, і я б сказав, що найкраще мати імпорт імпорту в межах модуля.


4

Просто, щоб заповнити відповідь Мо та оригінальне запитання:

Коли нам доводиться мати справу з круговими залежностями, ми можемо зробити кілька «трюків». Припустимо , що ми працюємо з модулями a.pyі b.pyякі містять x()і б y()відповідно. Тоді:

  1. Ми можемо перемістити один із from importsвнизу модуля.
  2. Ми можемо перемістити одну з from importsвнутрішньої функції або методу, який фактично вимагає імпорту (це не завжди можливо, оскільки ви можете використовувати його з кількох місць).
  3. Ми можемо змінити одне з двох from importsдля імпорту, який виглядає так:import a

Отже, для висновку. Якщо ви не маєте справу з круговими залежностями і робите якусь хитрість, щоб їх уникнути, то краще поставити весь свій імпорт на перше місце через причини, які вже пояснюються в інших відповідях на це питання. І будь ласка, виконуючи ці "трюки", включайте коментар, це завжди вітається! :)


4

Окрім вже наданих відмінних відповідей, варто зазначити, що розміщення імпорту - це не лише питання стилю. Іноді модуль має неявні залежності, які потрібно імпортувати або ініціалізувати спочатку, а імпорт верхнього рівня може призвести до порушень необхідного порядку виконання.

Ця проблема часто виникає в API Python API Apache Spark, де вам потрібно ініціалізувати SparkContext перед тим, як імпортувати будь-які пакети чи модулі pyspark. Найкраще розміщувати імпорт Pyspark у межах, де SparkContext гарантовано буде доступний.


4

Я був здивований, що вже не побачив фактичних цифр витрат для повторних перевірок завантаження, опублікованих уже, хоча є багато хороших пояснень, чого очікувати.

Якщо ви імпортуєте вгорі, ви приймаєте навантаження, незважаючи ні на що. Це досить мало, але зазвичай в мілісекундах, а не наносекундах.

Якщо ви імпортуєте в межах функції (функцій), ви приймаєте лише звернення для завантаження тоді, коли і коли вперше буде викликана одна з цих функцій. Як багато хто зазначав, якщо цього зовсім не відбувається, ви економите час завантаження. Але якщо функція (и) додзвонилися багато, ви берете неодноразовий хоча і набагато меншим хітом (для перевірки того, що він вже був завантажений, а не на самому ділі перезавантаження). З іншого боку, як зазначав @aaronasterling, ви також трохи заощадите, тому що імпорт у межах функції дозволяє використовувати трохи швидші локальні пошукові зміни, щоб пізніше визначити ім’я ( http://stackoverflow.com/questions/477096/python- стиль імпорту-кодування / 4789963 # 4789963 ).

Ось результати простого тесту, який імпортує декілька речей з внутрішньої функції. Звіти про час (в Python 2.7.14 на 2.3 GHz Intel Core i7) показані нижче (2-й дзвінок, який приймає більше, ніж пізніші дзвінки, здається послідовним, хоча я не знаю чому).

 0 foo:   14429.0924 µs
 1 foo:      63.8962 µs
 2 foo:      10.0136 µs
 3 foo:       7.1526 µs
 4 foo:       7.8678 µs
 0 bar:       9.0599 µs
 1 bar:       6.9141 µs
 2 bar:       7.1526 µs
 3 bar:       7.8678 µs
 4 bar:       7.1526 µs

Код:

from __future__ import print_function
from time import time

def foo():
    import collections
    import re
    import string
    import math
    import subprocess
    return

def bar():
    import collections
    import re
    import string
    import math
    import subprocess
    return

t0 = time()
for i in xrange(5):
    foo()
    t1 = time()
    print("    %2d foo: %12.4f \xC2\xB5s" % (i, (t1-t0)*1E6))
    t0 = t1
for i in xrange(5):
    bar()
    t1 = time()
    print("    %2d bar: %12.4f \xC2\xB5s" % (i, (t1-t0)*1E6))
    t0 = t1

Зміни в режимі виконання, ймовірно, пов'язані зі збільшенням частоти процесора у відповідь на навантаження. Краще почати тести на швидкість із другою напруженою роботою, щоб швидкість тактової частоти процесора зростала.
Хань-Кванг Ніенхуйс

3

Я не прагну надати повну відповідь, бо інші це вже дуже добре зробили. Я просто хочу згадати один випадок використання, коли мені здається особливо корисним імпортувати модулі всередині функцій. У моєму додатку в якості плагінів використовуються пакети і модулі python, що зберігаються в певному місці. Під час запуску програми додаток проходить всі модулі в розташуванні та імпортує їх, потім він заглядає всередину модулів і якщо знаходить деякі точки монтажу для плагінів (у моєму випадку це підклас певного базового класу, що має унікальний Ідентифікатор) він реєструє їх. Кількість плагінів велика (зараз десятки, але, можливо, сотні в майбутньому), і кожен з них використовується досить рідко. Імпорт інших бібліотек у верхній частині моїх модулів плагінів був трохи штрафом під час запуску програми. Особливо деякі бібліотеки третьої сторони важкі для імпорту (наприклад, імпорт сюжетно навіть намагається підключитися до Інтернету та завантажити щось, що додало приблизно одну секунду до запуску). Оптимізувавши імпорт (викликаючи їх лише у функціях, де вони використовуються) у плагінах мені вдалося зменшити запуск з 10 секунд до якихось 2 секунди. Це велика різниця для моїх користувачів.

Тому моя відповідь "ні", не завжди ставте імпорт у верхній частині своїх модулів.


3

Цікаво, що жодна відповідь досі не згадувала паралельну обробку, де, можливо, ПОТРІБНО, що імпорт функціонує, коли серіалізований код функції - це те, що пересувається до інших ядер, наприклад, як у випадку іпіпараллелу.


1

Підвищення продуктивності може бути імпортованим змінними / локальним визначенням всередині функції. Це залежить від використання імпортованої речі всередині функції. Якщо ви багато разів циклічаєте та отримуєте доступ до глобального об'єкта модуля, імпорт його як локальний може допомогти.

test.py

X=10
Y=11
Z=12
def add(i):
  i = i + 10

runlocal.py

from test import add, X, Y, Z

    def callme():
      x=X
      y=Y
      z=Z
      ladd=add 
      for i  in range(100000000):
        ladd(i)
        x+y+z

    callme()

run.py

from test import add, X, Y, Z

def callme():
  for i in range(100000000):
    add(i)
    X+Y+Z

callme()

Час роботи в Linux показує невеликий прибуток

/usr/bin/time -f "\t%E real,\t%U user,\t%S sys" python run.py 
    0:17.80 real,   17.77 user, 0.01 sys
/tmp/test$ /usr/bin/time -f "\t%E real,\t%U user,\t%S sys" python runlocal.py 
    0:14.23 real,   14.22 user, 0.01 sys

справжній - настінні годинники. Користувач час у програмі. sys - час для системних дзвінків.

https://docs.python.org/3.5/reference/executionmodel.html#resolution-of-names


1

Читабельність

На додаток до продуктивності запуску, має бути аргумент читабельності для локалізації importоператорів. Наприклад, візьміть номери ліній python від 1283 до 1296 в моєму першому проекті python:

listdata.append(['tk font version', font_version])
listdata.append(['Gtk version', str(Gtk.get_major_version())+"."+
                 str(Gtk.get_minor_version())+"."+
                 str(Gtk.get_micro_version())])

import xml.etree.ElementTree as ET

xmltree = ET.parse('/usr/share/gnome/gnome-version.xml')
xmlroot = xmltree.getroot()
result = []
for child in xmlroot:
    result.append(child.text)
listdata.append(['Gnome version', result[0]+"."+result[1]+"."+
                 result[2]+" "+result[3]])

Якщо importвислів знаходився у верхній частині файлу, мені доведеться прокрутити довгий шлях або натиснути Home, щоб дізнатися, щоET було. Тоді мені доведеться повернутися до рядка 1283, щоб продовжити читання коду.

Дійсно, навіть якщо import вислів знаходився у верхній частині функції (або класу), так як багато хто розмістить його, потрібно буде підключення сторінок вгору і назад.

Відображення номера версії Gnome рідко буде здійснено, тому importвгорі файлу буде внесено непотрібні затримки запуску.


0

Мені хотілося б згадати про моє використання, дуже схоже на те, яке згадували @John Millikin та @VK:

Необов’язковий імпорт

Я роблю аналіз даних за допомогою Jupyter Notebook, і я використовую той самий ноутбук IPython як шаблон для всіх аналізів. У деяких випадках мені потрібно імпортувати Tensorflow, щоб зробити кілька швидких запускань моделі, але іноді я працюю в місцях, де tensorflow не встановлений / повільно імпортувати. У цих випадках я інкапсулюю свої залежні від Tensorflow операції в допоміжній функції, імпортую tensorflow всередині цієї функції і прив’язую її до кнопки.

Таким чином, я міг би зробити "перезапустити і запустити все" без необхідності чекати імпорту або відновити решту комірок, коли він не працює.


0

Це захоплююча дискусія. Як і багато інших, я ніколи навіть не розглядав цю тему. Мені вдалося ввімкнути імпорт у своїх функціях через те, що я хочу використовувати ORD Django в одній із моїх бібліотек. Мені довелося телефонуватиdjango.setup() перед імпортом моїх модельних класів, і тому, що це було у верхній частині файлу, його перетягували в повністю не-код бібліотеки Django через конструкцію інжектора IoC.

Я наче трохи зламався і в кінцевому підсумку поставив django.setup() сингл-конструктор і відповідний імпорт у верхній частині методу кожного класу. Зараз це спрацювало нормально, але мені стало неприємно, оскільки імпорт не був на вершині, а також я почав турбуватися про додатковий час імпорту. Потім я приїхав сюди і з великим інтересом прочитав, що всі сприймають це.

У мене довгий фон C ++ і тепер використовую Python / Cython. Я вважаю, що чому б не поставити імпорт у функцію, якщо це не спричинить у вас профільне вузьке місце. Це лише як оголошення місця для змінних безпосередньо перед тим, як вони вам знадобляться. Біда в тому, що у мене є тисячі рядків коду з усім імпортом вгорі! Тому я думаю, що я зроблю це відтепер і зміню непарний файл тут і там, коли проходжу і встигну.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.