Pull to refresh

Comments 11

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

Статья нормальная, но в функции get_dict() вы сделали опечатку. Вы написали 'satus', а надо 'status'. С правками должно выйти так:

return {'status': 200, 'data': 'success'}

Да, Вы правы. Хоть сути это и не меняет, ведь я могу вернуть любую строку. Но замечание по факту) поправлю

Так ведь вы рассказали очевидные вещи, про которые рассказывают в каждом Хабре про ту или другую библиотеку!

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

Далее в корне создадим main.py файл, в который импортируем наш модуль двумя разными способами(об импортах описано в статье):

from ModulesAndPackages.module_examples.http_get.modules.http_get import get_dict as absolute
from http_get.modules.http_get import get_dict as relative

У вас тут не два разных способа импорта (относительный и абсолютный), а один - абсолютный импорт. А тот факт, что вы можете импортировать один и тот же модуль при помощи двух разных абсолютных путей говорит о наличии мусорки в sys.path. При этом один и тот же модуль будет загружен в двух экзмплярах, а это ошибка, которая ведет к тяжелым последствиям.

Относительные импорты в современном питоне начинаются с точки: from .module import name.

А то что вы называете "относительными импортами" существовало в двойке, было объявлено устаревшим и в тройке этот механизм убрали.

 двух разных абсолютных путей говорит о наличии мусорки в sys.path

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

Мусорка в sys.path звучит довольно интересно. Ведь там находится один путь до моего проекта.

Если интересно, то в своем любом проекте вы также можете найти относительные импорты. Исполните sys.path в своих модулях и посмотрите на импорты, бывает они не написаны относительно того пути что вы увидите в первой строке, выводимого этой командой листа. Это и будет относительный импорт, ведь вы явно не прописали корень проекта(если за вас это не делает Pycharm, ведь в PEP8 явно указано воздержаться от такого).

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

На самом деле, как бы странно не звучало, но невозможно загрузить один и тот же модуль дважды. У модуля один и тот же путь(C:\file.py, .\file.py и file.py по факту могут указывать на один файл) и хеш. Если говорить по простому, то сама по себе загрузка модуля происходит по абсолютному пути(C:\...\file.py), чтобы не было путаницы или другого рода проблем. Основная цель импорта это инструментарием указать файл который надо подгрузить, далее все работает с абсолютным путем(от корня диска и до файла).

id(модуля с длинным импортом(по моим словам абсолютным)) = id(модуля с коротким импортом(по моим словам неявно относительным))

Относительные импорты в современном питоне начинаются с точки: from .module import name

Звучит логично, тем более что только такой синтаксис приняли. Но в рамках того же Python я считаю есть не маленькая проблема:

sys.path:

['F:\Study\Habr', ... другие пути питона(сам питон и прочие модули для его работы)]

Исполняемый F:\Study\Habr\ModulesAndPackages\module_examples\main.py (абсолютный путь в рамка ОС), который помимо уже существующего пути проекта(F:\Study\Habr) будет добавлен в sys.path в качестве исполняемого файла:

from ModulesAndPackages.module_examples.http_get.modules.http_get import get_dict as absolute
from http_get.modules.http_get import get_dict as relative


def main():
    # Workable
    print(absolute())
    print(relative())


if __name__ == '__main__':
    main()

Первая строка:

По нашему пути до исполняемого файла (F:\Study\Habr\ModulesAndPackages\module_examples) внезапно ничего найти не получается. Производится поиск по пути проекта(F:\Study\Habr). Находит.

Берет наш путь до проекта(F:\Study\Habr), добавляет к нему путь до файла(.\ModulesAndPackages\module_examples\http_get\modules\http_get), случается магия, файл однозначно найден и определен.

Вторая строка:

По пути до проекта ничего не находит.

По нашему пути до исполняемого файла (F:\Study\Habr\ModulesAndPackages\module_examples) находит необходимый файл, добавляет к нему путь до файла(.\http_get), случается магия, файл однозначно найден и определен.

На данном этапе мы имеем:

  1. При абсолютном импорте Python почему-то ищет импортируемые зависимости рядом, пытаясь как бы от листа найти корень. А если это не возможно пытается выполнять поиск от корня(проекта). То есть осуществляет поиск не от корня проекта, а от исполняемого файла. На самом деле, тут просто замешана логика запуска(путь до исполняемого файла помещается в список с наименьшим индексом, ведь он первый кто становится известным).

  2. Логически проект это совокупность директорий(до питона, до вспомогательных вещей, и т.д.), а не файл/файлы. Естественно в директориях лежат файлы и реализовано взаимодействие. Но тут как раз таки вступает в сила понятие относительного и абсолютного пути. В рамках приведенного выше примера, при перемещении исполняемого файла первая строка будет указывать на одно и тоже место в проекте, при это вторая строка будет искать модуль рядом, что в свою очередь говорит об относительности данного импорта(он зависит от того где находится в рамках одного проекта и окружения(venv)).

  3. При помощи первого импорта и пути проекта возможно восстановить путь до файла, а при помощи второго импорта такое возможно только, используя путь до исполняемого файла. То есть он зависим от места где лежит и что лежит рядом.

  4. Разработчики языка Python называют все без точки при импорте, абсолютным(PEP0328). Хотя не о какой абсолютности речи не идет. Я все так же пишу свой импорт относительно файла(исполняемого) и оно работает. Я перемещаю исполняемый файл в рамках проекта, окружаю его модулями с теме же названиями и функциями, но меняю их функционал. Все файл ссылается уже на другие модули. Нет однозначности, которой они пытались достигнуть отказавшись от относительности в исполняемых файлах, точнее будет сказать что она просто осталась с другой реализацией и не более, что усложнило понимание что есть что.

  5. Последовательность поиска. Сначала выполняется поиск по пути исполняемого файла, затем по пути проекта. Только так и никак иначе! По факту, то что мы находим по пути проекта, те импорты абсолютные(можно однозначно определить файл, независимо от местоположения исполняемого). Как только мы говорим что при другом положении исполняемого файла, меняются и импорты, то они относительные.

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

А то что вы называете "относительными импортами" существовало в двойке, было объявлено устаревшим и в тройке этот механизм убрали.

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

P.S.

В данном случае хочется вставить цитату Поперечного:

Ели хоть раз желтый снег? А он лимонный. Вам один раз сказали, что там, а Вы ходите, уши развесили, каждый день мимо вкусноты. На вас фруктовый лёд такие бабки наживает, потому что вы не умеете оспаривать родительский авторитет.

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

На самом деле, как бы странно не звучало, но невозможно загрузить один и тот же модуль дважды.

Вы крайне серьезно заблуждаетесь.

id(модуля с длинным импортом(по моим словам абсолютным)) = id(модуля с коротким импортом(по моим словам неявно относительным))

Чушь.

У модуля один и тот же путь(C:\file.py, .\file.py и file.py по факту могут указывать на один файл) и хеш.

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

На самом деле это легко проверить, возьмем ваш же любезно предоставленный репозиторий и любимый PyCharm. Добавим в функцию main всего пару принтов:

import sys

from ModulesAndPackages.module_examples.http_get.modules.http_get import get_dict as absolute
from http_get.modules.http_get import get_dict as relative


def main():
    print(absolute is relative)
    print(absolute.__module__ is relative.__module__)
    print(id(sys.modules["http_get.modules.http_get"]))
    print(id(sys.modules["ModulesAndPackages.module_examples.http_get.modules.http_get"]))

    # Workable
    print(absolute())
    print(relative())

И что же мы видим?

False
False
139668730660880
139668732362352
{'satus': 200, 'data': 'success'}
{'satus': 200, 'data': 'success'}

Почему так происходит? Да потому модуль загружен в двух экземплярах, в sys.modules существует две записи, указывающие на каждый из загруженных модулей соответственно.

P.S. Может быть стоит сперва изучить букварь по языку, прежде чем статьи на Хабр писать?

Да, вы правы, а я ошибся. Но только в том случае если этот импорт произведен в исполняющем файле. Если написанные Ваши принты исполнить, используя другую точку входа в программу(вызвать main из другого файла), то это будет один и тот же модуль(место в котором я ошибся...). Почему так происходит:
При внесении модуля с абсолютным импортом, строится дерево импорта с использованием одного нейминга импорта от корня проекта, а при неявно относительном строится новое дерево уже с использованием неймингов относительно исполняющего файла. Как итог, в словаре зарегистрированных модулей не находится искомый и регистрируется еще раз. Что повлечет за собой выделение новой памяти для мутабельных объектов. При явном относительном импорте дерево строится верно, а повторной регистрации не происходит.
Спасибо что поправили, подумаю как внести вашу правку в статью.

По поводу букваря, я думаю он уже мне не поможет, раз уже статья написана.

При абсолютном импорте Python почему-то ищет импортируемые зависимости рядом

Почему-то... На самом деле никакой магии нет.

При запуске скрипта python path/to/script.py, путь к директории в которой находится запускаемый скрипт добавляется в sys.path. Дальше никакой магии нет и поиск импортируемых модулей и пакетов происходит при помощи sys.path. (На самом деле все сильно сложнее, но для понимания работы этого пока будет достаточно).

Когда вы импортируете http_get.modules.http_get, сперва будет проверен кеш модулей sys.modules, и если модуль с указанным именем в нем не обнаружен, будет произведена попытка поиска модуля в sys.path.

Пакет http_get будет найден непосредственно в sys.path и все подмодули будут импортированы относительно него. Во время импорта интерпретатор выполняет код модуля, создает объект модуля и создает ссылку на него в словаре sys.modules.

Ключем в этом словаре выступает полное имя модуля, значением соответственно ссылка на объект модуля. Таким образом в sys.modules появилась новая запись с ключем http_get.modules.http_get.

И тут PyCharm подкладывает вам и всем новичкам утку. При запуске при помощи PyCharm, по-умолчанию PyCharm устанавливает переменную окружения PYTHONPATH, значением которой передает путь к корню проекта (или точней к "Sources Root").

Забавно, что вне PyCharm ваш код просто так работать не будет. Попробуйте запустить свой main.py из консоли, не трогая PYTHONPATH.

Таким образом директория проекта тоже попадает в sys.path.Теперь у вас в sys.path два пересекающихся пути.

Таким образом, когда вы импортируете ModulesAndPackages.module_examples.http_get.modules.http_get, будет снова проверен sys.modules, и так как этот модуль имеет совершенно другое имя, он не будет найден в sys.modules, а соответственно будет произведена попытка его поиска в sys.path. Что и успешно происходит, так как PyCharm заботливо подложил путь к корню проекта в sys.path.

Это типичная ошибка новичков, использующих IDE и не имеющих понимания ни работы системы импортов, ни инструментов, с которыми они работают. К сожалению преисполнившись самоуверенности они начинают строчить статьи на Хабр.

А почему же это проблема? Да, потому что два экземпляра модуля это два совершенно разных независимых объекта в памяти, которые имеют независимое состояние. И хоть мы все знаем, что глобальные переменные (глобальные состояние) - зло, рано или поздно это вызовет проблему, которую к слову не легко обнаружить и которая проявляется неочевидным образом.

P.S. Поэтому если уж используете PyCharm, я настоятельно рекомендую отключать эти две опции в настройках запуска:

И учиться работать, не трогая PYTHONPATH или sys.path без необходимости. Для этого всего лишь нужно грамотно огранизовать структуру проекта.

Материал для дальнейшего изучения:

  1. https://docs.python.org/3/library/sys.html#sys.modules

  2. https://docs.python.org/3/library/sys.html#sys.path

  3. https://docs.python.org/3/reference/import.html

  4. https://packaging.python.org/en/latest/tutorials/packaging-projects/

Интересно, что вы обратили внимание на слова, которые я пояснил далее по тексту. Да соглашусь, что добавление корня проекта в PYTHONPATH плохая практика. Но увы, как бы не было прискорбно говорить, это первое что пришло в голову при возникновении мысли объяснить работу с модулями и пакетами при разных импортах наглядно, а главное понятно. С учетом того, что это также демонстрирует логику их поиска.
Спасибо за разъяснения они достаточно хорошие. Попробуйте изложить свое виденье в статье, я думаю у вас получится хороший материал.

Sign up to leave a comment.

Articles