Search
Write a publication
Pull to refresh
8
15
Артур @artyc99

Backend developer

Send message

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

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

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

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

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

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

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

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

 двух разных абсолютных путей говорит о наличии мусорки в 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.

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

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

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

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

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

По поводу подхода инстанцирования вопросов нет, и я понимаю как оно живет(в общих чертах).

Ну и если там было объявление класса, то будет создан объект типа type .

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

но я бы убрал упоминание про Singleton

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

P.S.

Именно из-за двойной ответственности его и называют антипаттерном, в отличие от, например, Service Locator'а, который тоже умеет инстанцировать класс в единичном экземпляре.

Стоит заметить, что в некотором случае локатор служб фактически является анти-шаблоном.

https://ru.wikipedia.org/wiki/Локатор_служб#:~:text=Стоит заметить%2C что в некотором случае локатор служб фактически является анти-шаблоном

Опять таки тут возникает вопрос вкусовщины.

У меня нет противоречий, что синглтон антипаттерн.

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

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

Раскройте пожалуйста эту мысль, просто на своей памяти я не могу вспомнить ни единого объекта который бы порождал себя сам, и сам управлял своим жизненным циклом. Тоесть в моем представлении объект класса 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) при новом импорте.

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

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

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

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

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

P.S.

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

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

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

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

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

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

from app.database import User

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

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

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

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

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

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

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

if __name__ == "__main__":

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

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

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

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

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

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

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

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

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

Information

Rating
1,093-rd
Location
Москва, Москва и Московская обл., Россия
Date of birth
Registered
Activity

Specialization

Backend Developer, Web Developer
Middle
Git
Python
Linux
Docker
OOP
Nginx
Database
Designing application architecture
Design patterns
Algorithms and data structures