Что такое import в Python?

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

Плюсы:

  • Повышается читаемость кода.

  • Код логически разбит по "узлам", его поиск и дальнейший отлов ошибок становится понятнее и проще.

  • Для разработки в команде это дает более четкое понимание, что и где делает каждый при выполнении "задания".

Минусы:

  • Нужно понимать, что делается и для чего.

Как использовать import?

Синтаксис import в Python достаточно прост и интуитивно понятен:

# В данной строке импортируется something_we_want
import something_we_want 

# В данной строке импортируется something_we_want, как aww(логично и просто)
import something_we_want as aww 

# В данной строке импортируется из something_we_want something(логично и просто)
from something_we_want import something

# В данной строке импортируется из something_we_want something, как s(логично и просто)
from something_we_want import something as s

# Синтаксис as позволяет обращаться к импортируемому по новому нами описанному 
# далее имени(это работает только в рамках нашего файла)

Что можно импортировать?

Для более глубокого понимания import стоит рассмотреть пример, представленный ниже.

something_we_want.py:

def something():
    pass


somedata = 5

main.py:

# 1 случай
import something_we_want
something_we_want.something()

import something_we_want
print(something_we_want.somedata)

# 2 случай
import something_we_want as aww
aww.something()

import something_we_want as aww
print(aww.somedata)

# 3 случай
from something_we_want import something
something()

from something_we_want import somedata
print(somedata)

# 4 случай
from something_we_want import something as s
s()

from something_we_want import somedata as sd
print(sd)

# Классы импортируются по аналогии с функциями

Красиво, читаемо и понятно.

В чем же подвох?

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

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

По своему опыту использования данного языка, сложилось отчетливое ощущение главной идеи ООП(все есть объект). Что же в этом плохого?

Все файлы, функции и тд. это объект. Но что это за объект и класс стоят за фалами(модулями)?

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

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

Ветвистая структура приложения и существующие подходы импортирования

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

Пример ветвистой структуры:

  • main.py

    • app(директория)

      • models(директория)

        • User.py

        • ...

      • handlers(директория)

        • auth.py

        • ...

      • ...

    • ...

  • ...

Существует 2 подхода импортирования(лучше выбрать один и придерживаться его весь проект):

  1. Именованный

  2. Неименованный

Пример именованного импорта из models.py в auth.py:

# auth.py
from app.models import User

Пример неименованного импорта из models.py в auth.py:

# auth.py
from ..models import User

# Количество точек указывает на сколько (обьектов) мы поднимаемся от исходного.
# В данном примере первая точка поднимает нас на уровень обьекта handlers,
# А вторая точка поднимает нас на уровень обьекта app

Это два абсолютно разных подхода. В первом случае мы "идем" из "корня"(входной точки нашего приложения). Во втором случае мы "идем" от "листа"(нашего файла).

Плюсы и минусы подходов импорта:

Именованный

Неименованный

Плюсы

Видна структура импорта и приложения.

Высокая читаемость.

Видна часть структуры импорта.

Программисту не нужно знать полную структуру приложения.

Импорт не зависит от точки входа.

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

Минусы

Импорт зависит от точки входа.

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

Снижается читаемость импорта.

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

P.S.

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

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

Комментарии (15)


  1. foxin
    29.01.2023 02:07
    +6

    Именованный - Минусы

    Импорт зависит от точки входа.

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

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

    вообще это полезная штука, да. независимо от импортов.

    Код сильно связан с приложением.

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

    Что по сути усложняет отладку, тестирование, и тд.

    по сути ничего не усложняется, если не усложнять без надобности.

    Программист становится сильно вовлеченным в проект.

    ????‍♂️

    PS
    в начале поста не хватает большими красными буками капсом:
    tl; dr: используйте абсолютные импорты, а относительные не используйте, и будет хорошо.


    1. artyc99 Автор
      29.01.2023 10:40
      +1

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

      Точки входа в программу. Если просто то

      if __name__ == "__main__":

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

      вообще это полезная штука, да. независимо от импортов.

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

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

      От того, что у тебя явно прописано местоположения модулей, названия папок/директорий и тп. что осложняет выдергивание кода его запуск в других местах. О чем я, выдернуть модуль из контроллеров и засунуть его в либу или хендрлер без переписывания импортов не возможно(импорты бывают объемными, на моей памяти доходило до 30+ строк), а также просто изолированный запуск.

      по сути ничего не усложняется, если не усложнять без надобности.

      См. ответ на первую цитату.

      PS
      в начале поста не хватает большими красными буками капсом:
      tl; dr: используйте абсолютные импорты, а относительные не используйте, и будет хорошо.

      Спасибо, за совет, рассмотрю его и постараюсь исправить статью. А если говорить о частном, статья направлена на освящение методов, а не рекомендации(делай так 2 раза в день и спина болеть не будет) и это было описано.


  1. dopusteam
    29.01.2023 15:58

    Плюсы:

    Для разработки в команде это дает более четкое понимание, что и где делает каждый при выполнении "задания".

    Минусы:

    Нужно понимать, что делается и для чего

    Как это так? Импорты улучшают понимание, но нужно понимать?

    Вообще, не хватает обзора варианта без импортов, имхо. Не понятно с чем идёт сравнение импортов и какие там минусы


    1. artyc99 Автор
      29.01.2023 17:32

      Я Вас понял, последующие статьи буду писать с учётом полученного опыта.

      Пока распишу варианты:

      1) жестокий и в лоб. Пишем все в одном файле. Это много кода который не разбит на файлы и тд. Ну это достаточно неудобно от 1 тысячи строк кода.

      2) бьем на файлы и папки. Появляется логика из серии парка database и файлик user где описано взаимодействие с базой, а вчастности с таблицей User. Что это даёт: при импорте мы видим:

      from app.database import User

      По одной строке нам понятно что мы работаем с базой данных и таблицей Users.

      Если сравнить то имея функцию get_user() можно иметь вариации. Получить информацию пользователя который обращается к нашему сервису или пользователь которого достаём из базы данных. Соответственно когда все в одном файле появляется момент что непонятно что есть что. А тут просто и четко следует из импорта.

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

      Соответственно когда идёт работа в команде и у одного задача реализовать работу с пользователями(человек будет работать с сущностями User), а другому нужно сделать работу с изображениями(будет работать с сущностями Images). Если мы разбиваем все на файлы то понятно, кто где копается. А если файл один, то это все пишется в одном файле и это достаточно неудобно(система контроля версий гит. Один файл меняет толпа людей. Лог изменений громадный и непонятно что менялось, надо читать все и это грустно)

      Если данных пояснений жалко то напишите комментарий я постараюсь реализовать свою идею в рамках гитхаба и скинуть ссылку для более полноценного понимания)


  1. Coriolis
    29.01.2023 18:19

    Спасибо за статью. Коллеги, дак а что на счёт юнит-тестов? Как для них удобней будет, относительный таки? Как это правильно делать?


    1. artyc99 Автор
      29.01.2023 18:39

      Планирую сделать подобную статью. Как писать тесты и сделать срез о подходах.


  1. philosoph
    30.01.2023 09:13

    Интересно, но мало :-) Кстати, насколько помню, импорты с точками вместо имён у меня почти никогда не получались.

    Ну и главное - не затронута совсем такая бяка, как циклический импорт. Что делать, если в модуль "А" нужно импортировать модуль"Б", и наоборот - в модуль "Б" нужно импортировать модуль "А". Мне из-за этого пришлось отказаться от разбиения модуля на два, а также от аннотаций функций. Скажем, модуль "А" использует функции из "Б", а функции в "Б" работают с типами из "А"...


    1. andreymal
      30.01.2023 11:55
      +1

      Импорты для аннотаций можно завернуть в if typing.TYPE_CHECKING: чтобы в рантайме они не импортировались


      1. artyc99 Автор
        30.01.2023 13:50

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


    1. artyc99 Автор
      30.01.2023 13:46
      +1

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

      Поэтому возникновение циклического импорта, для меня диковина. Из вариантов это более тщательный подход в выделении логических блоков кода для их выноса в другие модули. Или метод топорный, но рабочий, это сделать третий модуль, туда импортировать необходимое и описать уже их симбиоз…(в рамках ооп подобная идея называется ромбовидное наследование, по факту цикл, но из-за того что это ромб оно работает однонаправленно)

      Тоесть основная проблема цикла в импортах, это неоднозначность направления(если рассматривать как дерево, то вверх и вниз). Если вы это исключите, то у вас не будет проблем)

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

      P.S.

      На досуге постараюсь подумать, где идейно можно и нужно применить циклический импорт.


      1. philosoph
        30.01.2023 15:41

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


    1. AnonimYYYs
      31.01.2023 09:16
      +1

      Буквально вчера решал данную проблему. Вроде как решил с помощью указания:

      # child_classes.py
      from base_class import BaseClass
      
      __all__ = [
        "ChildClass1",
        "ChildClass2",
      ]
      
      class ChildClass1(BaseClass):
        pass
      
      class ChildClass2(BaseClass):
        pass
      
      ...
      # base_class.py
      from ..root import child_classes
      
      __all__ = [
        "BaseClass",
      ]
      
      def BaseClass:
      
        @classmethod
        def create(cls, params):
          return getattr(child_classes, cls.__name__)(params)

      Суть решения в использовании "all", в котором указываются функции и классы, которые надо импортить при импорте.

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


      1. artyc99 Автор
        31.01.2023 18:02

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


  1. enkryptor
    30.01.2023 23:25
    +1

    Все файлы, функции и тд. это объект. Но что это за объект и что за класс за ним стоит?

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

    Совершенно непонятно, что тут имел в виду автор. Singleton — это конкретный порождающий паттерн, предполагающий, что объект сам управляет своим жизненным циклом (создаёт себя и следит за тем, чтобы был создан только один экземпляр). Singleton это класс с методом навроде get_instance(), который всегда возвращает один и тот же экземпляр класса. Если экземпляр класса создаётся другим классом, это уже не Singleton, даже если экземпляр создаётся один.

    Функции это объекты класса function. Класс function это не Singleton, так же как и любой объект класса type. Класс int это не синглтон, у него нет метода get_instance, а вызов int() вернёт новый инстанс в зависимости от переданного аргумента.

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


    1. artyc99 Автор
      31.01.2023 03:21

      объект сам управляет своим жизненным циклом (создаёт себя и следит за тем, чтобы был создан только один экземпляр)

      Раскройте пожалуйста эту мысль, просто на своей памяти я не могу вспомнить ни единого объекта который бы порождал себя сам, и сам управлял своим жизненным циклом. Тоесть в моем представлении объект класса Singletone управляет экземплярами(ом) данного класса.

      Singleton это класс с методом навроде get_instance(), который всегда возвращает один и тот же экземпляр класса.

      В моем понимании import является своего рода методом который вызывает регистрацию запрашиваемого модуля в ядре(CPython допустим), и возвращает экземпляр класса. Что и есть своего рода get_instance().

      Функции это объекты класса function. Класс function это не Singleton,
      так же как и любой объект класса type. Класс int это не синглтон, у него
      нет метода get_instance, а вызов int() вернёт новый инстанс в
      зависимости от переданного аргумента.

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

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

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

      # main.py
      if __name__ == '__main__':
      
          # Выводим int = 2
          import test1
          print(test1.a)
      
          # Изменяем int = 2 на 4 и выводим
          import test2
          test2.new_t()
      
          # Выводим тот же int когда-то равный двум(уже четырем)
          import test1
          print(test1.a)
      # test1.py
      a = 2
      # test2.py
      def new_t():
          import test1
          test1.a = 4
          print(test1.a)
      # print
      2
      4
      4

      Поэтому файлы(модули) = объект, а ввиду набора определенных свойств импорта эти объекты порождают один и тот же экземпляр класса. В совокупности этих факторов я и написал про паттерн Singletone.

      В примере, id(a) будет разный, поскольку как вы правильно заметили int иммутабельный, но входит в состав мутабельного экземпляра. Хоть было бы проще привести в пример id(test1)*, но это бы не показало какие проблемы подобные импорты порождают. И поскольку данная проблема преследует при мутабельных и иммутабельных составляющих модулей, я решил что будет логично осветить это в теме import а не мутабельности.

      P.S.
      С остальным содержанием комментария автора согласен и считаю его мысли не противоречищами с идеями излагаемыми в статье.

      * id(test1) = id(test1) при новом импорте.