Как стать автором
Обновить

Комментарии 62

что-то смысл немного ускользает.
зачем такое при использовании как пакета?
опять же есть __init__
за уши удалось примерную стратегию применения притянуть, но несколько сомнительна
Смотрите, вот есть у вас папка image_utils, в ней куча питоновских файлов, связанных с обработкой файлов. __init__.py делает папку модулем, который можно импортировать для других проектов.

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

python -m image_utils -convert=png someimage.jpg


Так команда не сработает, даже если положить if __name__ == '__main__' внутрь __init__.py. Напишет:

No module named image_utils.__main__; 'image_utils' is a package and cannot be directly executed

Как быть? Кладёте внутрь папки __main__.py, куда добавляете обработку командной строки и всё начинает работать. Бонусом, что не надо больше постоянно писать if __name__ == '__main__'

init.py делает папку модулем, который можно импортировать для других проектов.

Начиная с Python 3.3 __init__.py для этого не нужен.


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

Нужно каждый раз писать python -m, неудобно же. Удобнее и проще регистрировать скрипты.


Я думаю, так мало людей использует __main__.py как раз из-за того, что setup.py представляет более удобный функционал.

python -m venv /path/to/venv туда же. Очень удобно, имхо.
что это делает, и почему удобней делать так?
Создаёт venv — изолированное окружение со своим набором библиотек.
Альтернативный вариант — использовать исполняемый файл pyvenv.
Удобнее это тем, что можно создавать venv с явно указанной версией и инсталляцией python, например `/opt/bin/python3.8 -m venv`. Кроме того, бинарь pyvenv с какой-то версии объявлен депрекейтед.

Использовал, удобно.

Никогда не использовали python -m http.server и python -m compileall .?

Использовал, конечно. А вы никогда не использовали jupyter notebook? По-моему, удобнее, чем python -m jupyter notebook.

но тогда получается теперь нельзя запускать по человечески просто нужный файл.
без всякого этого -m и python

Ответьте на вопрос — а нужно ли это ???
Если очень хочется, ну, или сделайте симлинк на этот main.py из /usr/local/bin, или положите туда тупой двухстрочный шелл-скрипт, который за Вас сделает python -m module-name @$

симлинк на main при таком варианте ничем не поможет.
а шелл-скрипт — замечательное в своей оптимальности решение.

Ага, больше врапперов богу врапперов.
А Вы не подумали, что просто положив скрипт на питоне куда-либо — Вы убиваете всю возможность его версионировать. И доставлять через родное для питона окружение (pip install). Все равно по уму тогда нужно либо начать писать свои пакеты нативные для операционной системы, т.к. упаковывать python скрипты в deb, rpm — это вот все. И правильно прописывать зависимости от других пакетов (поверьте, это сложно). Либо использовать какие-то внешние способы доставки (а-ля ansible).


Касательно примеров вызова python -m против сразу вызов скрипта — смотрите ansible, сам pip, j2 из j2cli и пр. консольные утилиты, написанные на Питоне. Можете даже запилить сравнение, что они делают, чтобы быть доступными для пользователя по "простой" команде. И, да, в половине случаев, если не больше — Вы увидите враппер (может не на Шелл, но на питоне).

pip и пакетные менеджеры с удовольствием будут работать с нормальными питоновоскими скриптами.
а зачем тут вобще врапперы?
просто нужный файлик делаем исполняемым.
инерпретатор указываем env python (env python3)
и все замечательно запускается, версионифецируется

Нет. Неверно. Тот же pip — он устанавливается в систему по определенному пути (/usr/bin/pip), что не оставляет возможности иметь ДВА pip'а в системе. В случае с python3 выкрутились попросту устанавливая его в /usr/bin/pip3, но все равно при определенных обстоятельствах основной указывает не туда, куда надо. Более того — то, что по этому пути это не полноценный pip, а враппер:


#!/usr/bin/python3
# EASY-INSTALL-ENTRY-SCRIPT: 'pip==19.1.1','console_scripts','pip3.7'
__requires__ = 'pip==19.1.1'
import re
import sys
from pkg_resources import load_entry_point

if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(
        load_entry_point('pip==19.1.1', 'console_scripts', 'pip3.7')()
    )

о чем выше я и сообщил.


При использовании его через python3 -m pip — есть гарантия, что он всегда вызывается в нужном окружении.

И, да, в половине случаев, если не больше — Вы увидите враппер (может не на Шелл, но на питоне).

Вы ведь в курсе, что никто врапперы для питоновских модулей вручную не пишет, а они генерируются автоматически по записи в entry_points?

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

А не, я слегка облажался: долгий импорт pkg_resources появляется, если устанавливать модуль с опцией --editable, а без неё всё нормально. К несчастью, --editable мне очень часто нужен)

и по прежнему не понимаю, как мне просто проимпортировать пакет теперь.
Очень просто, а главное полезно, спасибо

Гораздо удобнее прописать в setup.py все точки входа. И тогда не нужно запускать интерпретатор с ключом -m, так как все точки входа будут обычными консольными командами. if __name__ == '__main__' в этом случае тоже не нужен.

Ага, больше логики в setup.py.
Я лично считаю упомянутую Вами практику порочной. Не вижу бенефитов от точек входа в setup.py, кроме путаницы, если нужно установить один модуль в два и более интерпретатора.

О какой логике речь? Мне кажется, вы не совсем поняли, что я имею в виду. Нужно лишь дописать в setup.py что-то вроде:


setuptools.setup(
    ...
    entry_points={
        'console_scripts': [
            'my_script=my_module:main',
        ],
    },
    ...

И после установки через pip у вас появится общесистемная команда my_script. И точка входа main может находиться где угодно в соответствии с логикой кода, не обязательно в __main__.py.


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


И это не порочная практика, а повсеместная и рекомендованная Python Packaging Authority. Посмотрите в bin/ своего виртуального окружения. И взгляните на официальный пример проекта от PyPA.

Я про эту штуку выше и писал — про врапперы. По ходу они pip'ом сами формируются и устанавливаются. ОК. Не самый плохой вариант. Уж всяко лучше, чем свои скрипты закидывать в /usr/local/bin...

Этот файл придётся исключать из coverage при тестировании, ведь импорт модуля __main__.py приведёт к запуску программы. Это приведёт к тому, что в __main__.py будет очень простой код, например код ниже, чтобы этот код не требовал покрытия тестами. В итоге, мы вернёмся к if __name__ == '__main__'


import run
run.main()
Да, это так, вы же в __init__.py бизнес логику не прописываете?

Видел очень много проектов, в которых в __init__.py написано очень много логики.

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

И что? Миллион мух не может ошибаться? То что все говнячат — это, во-первых, не означает, что это нормально, а, во-вторых, отвечайте сами за свой продукт.
В третьих, наличие всякой хитрой логики, что в setup.py, что в init.py — за это надо руки отрывать

импорт модуля main.py приведёт к запуску программы

Нет, если в __main__.py точно так же запускать программу внутри if __name__ == '__main__'

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

Всё там нормально будет.
Попробуйте

Спасибо, век живи — век учись

Проверка пройдёт только при запуске напрямую (python my_package/__main__.py) или через -m (python -m my_package). Если __main__.py импортируется из другого модуля, его __name__ будет my_package.__main__.

Считаю что оформлять приложения в виде модулей это плохая идея, вместо этого использую *.py в корне проекта для приложений и подкаталоги для библиотек. Так сразу видно что можно запускать напрямую без поиска __main__.py, не нужно никаких python -m для запуска, не создаётся искусственных связей между приложениями и библиотеками, которые потом мешают расширять проект (т.е. добавлять новые приложения, разбивать и объединять библиотеки, разбивать весь репозиторий). Примерная структура одного из текущих проектов:


├── mycoolservice-updater.py    # backend демон обновляющий данные 
├── mycoolservice-*.py          # какие-то ещё вспомогательные backend утилиты
├── mycoolservice-webapp.py     # веб-приложение
├── mycoolservice               # основная логика, используется всеми 
│   ├── __init__.py
│   └── *.py
└── mycoolserviceweb            # логика специфичная для веб приложения, вьюхи
    ├── __init__.py
    └── *.py

А __main__.py я бы оставил только для особой функциональности модулей типа python -m json.tool

И потом получать геморрой с публикацией всех этих скриптов на PyPI?

Никакого геморроя. Много кто так и делает, см., например, ansible и black.

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


setup(
    # ...
    scripts=[
        'bin/ansible',
        'bin/ansible-playbook',
        'bin/ansible-pull',
        'bin/ansible-doc',
        'bin/ansible-galaxy',
        'bin/ansible-console',
        'bin/ansible-connection',
        'bin/ansible-vault',
        'bin/ansible-config',
        'bin/ansible-inventory',
    ],
    # ...
)

Оформлять приложения в виде модулей это наиболее правильная идея, которая может возникнуть в рамках инфраструктуры и экосистемы питона. Типичный пример. Мне часто приходится работать с несколькими версиями питона на одной машине. И речь не про 2 vs 3, а про версии даже внутри одной линейки. Если скрипт запускать как исполняемый файл, то велика вероятность, что он запуститься не тем интерпретатором. В случае вызова python3.6 -m имя_модуля такой двусмысленности нет. И более того — если интерпретатор вырубается на отсутствующий модуль, то будет понятно, что делать.

Скрипт можно запускать точно так же, причём на 1 аргумент короче.

Сомнительное преимущество. Здесь скорее дело в том, как "скрипт" установлен — как отдельно стоящий скрипт в /usr/bin и аналогах или как полноценный питон модуль.
В любом случае вопрос ещё в том, как грамотно обернуть питон-модуль — или в pypi, или в нативный пакет для операционной системы. Меня, честно говоря, очень расстраивает, когда setup.py приносит в систему то, что не ожидается изначально. Примеров масса.

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


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

Ну, чего все такие категоричные? Я вообще ума приложить не могу.
Часть вещей — действительно имеет смысл ставить в venv. Даже тот же хваленый airflow. Ansible — тоже скорее да, чем нет. Пользоваться, конечно, становится неудобно, но для сервисов это не проблема — обернул в сервис и понеслась. Часть вещей — не имеет смысла ставить в venv. Вообще — это какие-то общесистемные вещи, например, пакет для управления docker. Или еще что-то подобное. Ну, и опять же — мой тезис, что setup.py в теории может любую дрянь притащить в систему. Мне кажется, что не нужно пытаться противопоставлять средства нативной доставки приложений (deb/rpm в первую очередь) и pip, а все-таки пытаться их дружить. Благо в первых все-таки есть средства рекурсивной валидации зависимостей — для Питон-экосистемы это прям больное место.

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


Насчёт дряни — это к мейнтейнеру. Скрипты deb-пакетов тоже могут дел натворить. Или вы плавно ведёте к экзотике вроде Nix/Guix?

Ставить как нативные для системы особого смысла нет.

Не будь смысла, опакечивание Python модулей не было бы распространено повсеместно: https://repology.org/projects/?search=python%3A


Лишняя возня со сборкой, а премуществ никаких особо и нет.

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

Не будь смысла, опакечивание Python модулей не было бы распространено повсеместно:

Я, возможно, потерял нить разговора, но речь шла о собственных проектах. Не берусь судить о всех, но лично мне проще устанавливать пакеты через pip прямо из репозитория git, чем поддерживать пакеты и держать репозитории для разных операционных систем. Чтоб не компилировать каждый раз бинарные модули, можно держать один репозиторий с wheel-файлами. Это удобнее, если, например, разработка ведётся под MacOS, на серверах Ubuntu, а в докере вообще какой-нибудь Alpine.


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

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


У меня же сложилось такое мнение (не обязательно верное). Если пакет зависит от каких-то нативных библиотек, то ничто не мешает проверять наличие этих библиотек при компиляции и установке. Или вовсе помещать их в wheel-файл. Пакеты под линукс следует собирать не на коленке, а в официальном докере manylinux1, что даст какую-то уверенность в работоспособности. Мне кажется, что перечисленные проблемы — это не проблема способа дистрибуции, а проблема мейнтейнера. Вот, тот же numpy. Отлично ставится через pip, хотя насквозь бинарный.


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


В общем, всё средства хороши на своём месте.

Мы тоже pip'ом ставим из гита. Это действительно удобно и способствует быстрой разработке.
Но нормальные пакеты (в первую очередь deb/rpm, во вторую pypi/wheel) становятся жизненно необходимы, если этими модулями будут пользоваться другие люди. Условно — решил проблему — поделись с сообществом. Ничего там сверхсекретного в твоей разработке скорее всего нет. Тот же ML — там ноу-хау в моделях (т.е. весовых коэффициентах), а не во фреймворках обучения.


Касательно дружить — да, все верно, что setup py ничего не знает о системе, куда будет установлен. Скажем, Вам нужна библиотека для работы с БД Postgres. В разных дистрибутивах она может быть разной версии, лежать по разным путям и устанавливаться из пакетов с разными именами. Унификации здесь нет и в ближайшее время не предвидится. В результате ставя питон-пакет из официального репозитория ОС — эти зависимости подтягиваются, т.к. мейнтейнеры их описали и гарантируют их. А ставя через pip — либо как повезет, либо там будет дичь в setup.py.


Насчёт дичи в deb/rpm — да, она тоже возможна, но мейнтейнеры официальных репозиториев такое не пропустят, т.е. проблема возможна только со сторонними репами

или в pypi, или в нативный пакет для операционной системы

Я ни разу не встречал проблем с опакечиванием питоновых приложений хоть с PyPI, хоть напрямую с GitHub — setup.py делает всё что нужно и создаёт исполняемые файлы в bin — это работает и c __main.py__, и со скриптами.


Проблема, повторюсь, возникает при работе с репозиторием, потому что с __main__.py скриптов в нём не будет и чтобы пользоваться приложением из чекаута репозитория вам придётся, по сути, сначала руками распарсить entry_points из setup.py, или искать __main__.py.

И это прекрасно работает для одного проекта или как запуск некоторых модулей вполне себе хорошее решение:
`python -m venv`
`json.tool` — выше написали :)
А вот если это действительно приложение, да еще и не одно вращается на сервере или более того в контейнерах, то использование __main__.py или просто run.py даже вредит. Когда приходит OOM Killer, мы не видим какое конкретно приложение было «убито», а только python. Стараемся использовать шебанг в service_name.py или иное указываем в README.md.

Очень спорный аргумент
В случае оом — лучше настроить эти питоновские скрипты как юниты системди и указать им политику рестарта...

Способ реализации запуска/перезапуска приложения это инфраструктурный вопрос и легко решаем. Самостоятельно перезапущенное приложение показатель ненормальной работы сервиса и с этим надо разбираться. Представим ситуацию, что количество пользователей возросло, а мы используем по-прежнему сервер с 2ГБ ОЗУ. Мы получим ООМ периодически и systemd будет это отрабатывать. И в логах периодически будем видеть
[1395278.160214] python invoked oom-killer: gfp_mask=0xd0, order=0, oom_score_adj=996

Для одного-двух проектов это легко отыскать и исправить проблему. Если же больше, то иди-разберись какое из 30+ приложений упало. А если это еще приложение в kubernetes/swarm со скейлом 3 (мой вариант), то совсем не радостно становится искать логи упавшего сервиса. А еще надо как то в тикете описать что нужно править :)

Вы несомненно правы, что искать по логам ООМкиллера не очень удобно. Но это очень похоже на борьбу не с причиной проблемы, а со следствием. Смотрите.


  1. В случае системд юнитов, почему я вообще о них заговорил, часть головной боли по задаче "понять, что отвалилось" перекладывается на системд. Ес-но, это не полное решение. Рестарт — это такая же затычка и Вам решать нужно ли его ставить (как будто в кубе приложения автоматически не перезапускаются в случае сбоя).
  2. Обвесить все мониторингом. Вообще все. Любое долгоживущие приложение должно быть под мониторингом. Упало? Получили Алерт и это увидели. С пакетными заданиями сложнее, но тоже реально. В этом кейсе становится сложнее понять, если отвалился внутренний компонент приложения, а не его основная часть, но тоже нужно смотреть по ситуации — наверняка есть решение.
  3. В идеальном мире — оом быть не должно. Разработчики должны ставить сайзинг на все свои решения. С небольшим, но запасом. И в лучшем случае ООМ никогда не будет, но ценой наличия незадействованных ОЗУ.
Надо было начать с того, что я не разработчик. Я устраняю следствие, а после пишу тикет с описанием проблемы и ссылкой на логи. Далее устраняют причины.

2. Не получится. На некоторых приложениях настроен kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale.
3. Запас есть, но он тоже ограничен.
Кубер при скейлинге так же может упереться в ограничение по количеству подов на рабочей ноде (110 штук, такое у нас пока не возможно) или в лимит ОЗУ прописанный в деплоях. Период опроса мониторинга 15 секунд и когда словишь «эффект домино», то несколько сервисов могут остановится в этом промежутке и на дашборде не будет видно в какой последовательности это произошло. Единственное место это логи ОС, но там раньше было по 2-4 записи подряд на подобии указанной мной в предыдущем комменте. Т.е. какой процесс был прибит ООМ, а какой был остановлен автоскейлером приходилось искать по тоннам логов потому, что stack trace в syslog указывал на python, а не конкретное приложение.

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


  1. Не получится. На некоторых приложениях настроен kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale.

В условиях ограниченных ресурсов. Ну, а что — молодцы! К сожалению, не вижу Вашей инфры, поэтому не могу дать аргументированное мнение, но выглядит прям странно.


Период опроса мониторинга 15 секунд и когда словишь «эффект домино», то несколько сервисов могут остановится в этом промежутке и на дашборде не будет видно в какой последовательности это произошло

последовательность нужна для пост-мортема, а не для починить "прямо сейчас". Очень странная у Вас конфигурация, прям скажу, потому что кубер ведь должен мочь перезапускать поды. А если даже это не так — у Вас же мониторинг на прометеусе наверняка есть? Эластик есть, куда приложения логи отправляют?


Т.е. какой процесс был прибит ООМ, а какой был остановлен автоскейлером приходилось искать по тоннам логов потому, что stack trace в syslog указывал на python, а не конкретное приложение.

ну, и чем лог syslog'а поможет в случае, если отвалился не python процесс с pid=1, а какой-то дочерний? Вы просто сделали несколько слоев абстракций (k8s -> docker -> linux) и пытаетесь как будто использовать не тот инструмент исследования не для той задачи.

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

пост-мортем используется для локализации проблемы со стороны разработчиков то же. Вроде я не писал про невозможность перезапуска подов? Кубер с этим очень хорошо справляется.
Я писал про цепочку падения микросервисов. Они восстанавливаются сразу же, но это не есть хорошо. По этому пишу пост-мортем, делаю тикет, туда всю инфу пишу и уже разработка разрабатывает :) Так что в одиночку я ничего не устраняю. У меня на это гораздо больше времени уйдет, чем у человека который писал этот микросервис.

ну, и чем лог syslog'а поможет в случае, если отвалился не python процесс с pid=1, а какой-то дочерний?

Jun 17 09:33:06 slave-1 kernel: [ pid ] uid tgid total_vm rss nr_ptes swapents oom_score_adj name
Jun 17 09:33:06 slave-1 kernel: [25793] 0 25793 394 62 4 0 0 http_server
Jun 17 09:33:06 slave-1 kernel: [25851] 0 25851 1911 1488 7 0 0 python
Jun 17 09:33:06 slave-1 kernel: Memory cgroup out of memory: Kill process 25851 (python) score 1459 or sacrifice child


видно какой сервис отвалился. Не просто python, а http_server
Стараемся использовать шебанг в service_name.py или иное указываем в README.md.

Просто зарегистрируйте точку входа в setup.pyи скрипт запуска с шебангом будет создан автоматически. А для Windows и вовсе exe-файл, если вдруг вам Windows нужен.

Мне, как начинающему разрабочику на python, тяжело от таких статей. 7 разных мнений в комментариях, до конца непонятно — как и когда использовать __main__. Причём в официальной документации инфы об этом настолько мало, что просто страшно.

Официальная документация рекомендует использовать setuptools для модулей, а те — регистрировать точки входа в setup.py. Инфы не мало, просто она неочевидным образом распределена. Если только начинаете разбираться, то посмотрите на официальный пример модуля. Там много комментариев, и охвачены разные аспекты: от пакетирования до тестирования.


__main__.py имеет смысл использовать, если мы не делаем полноценный модуль, который будем устанавливать, а просто сделали модуль в виде папочки или zip-архива и так и запускаем.

Простите, но разве это не сработает?
chmod +x ./myapp/__main__.py
./myapp/__main__.py

Это плохой подход. Вот причины:


  • нужно знать путь к модулю (а мало ли в какое окружение он будет установлен),
  • нужно прописать shebang,
  • это не работает в Windows, например.

Регистрация точек входа в setup.py и __main__.py лишены этих недостатков.

Начиная с питона 3.6 (вроде) на Windows так уже сработает.

Мне вот стало интересно. Условно у меня есть приложение на Flask'e, я в свою очередь решил не создавать в папке app файл __main__.py, а настроить .env и запускать через flask run.
Что мне делать в таком случае? Лично я еще не нашел решения, как прописать, что именно фласк должен запускать

Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории