Python Заметки
2.31K subscribers
58 photos
2 videos
2 files
212 links
Интересные заметки и обучающие материалы по Python

Контакт: @paulwinex

⚠️ Рекламу на канале не делаю!⚠️

Хештеги для поиска:
#tricks
#libs
#pep
#basic
#regex
#qt
#django
#2to3
#source
#offtop
Download Telegram
Как разделить строку с shell-командой на отдельные аргументы в виде списка?
Если сделать просто сплит по пробелу то получим то что надо, кроме случаев со вставками текста с пробелами. Например так:

>>> '-arg "I Am Groot"'.split(' ')
['-arg', '"I', 'Am', 'Groot"']

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

А что делать, если нужно обратное действие? Объединить аргументы из списка в строку и при этом добавить кавычки в аргумент с пробелами.
Конечно, если вы используете subprocess то он сам всё разрулит. Но если вам нужна именно команда одной строкой, то можно воспользоваться готовой функцией в том же subprocess.

>>> from subprocess import list2cmdline
>>> list2cmdline(['-arg', 'I Am Groot'])
'-arg "I Am Groot"'

Он также позаботится об экранировании уже имеющихся кавычек

>>> list2cmdline(['-arg', 'I Am "Groot"'])
'-arg "I Am \"Groot\""'

А вот так он может "схлопнуть" в команду JSON

>>> list2cmdline(['--json', json.dumps({'key': 'value'})])
'--json "{\"key\": \"value\"}"'

_______________
Возможно кто-то спросит, а зачем соединять аргументы в строку если subprocess сам это сделает а os.system не наш путь?
Мне как-то потребовалось отправлять команду на удалённое выполнение и в API поддерживалось указание команды только строкой. Так что всякое бывает)

#libs #basic
Подразумеваемые неймспейсы или неявные пакеты.

Этот функционал добавлен в Python 3.3
Что он означает?

Ранее, до 3.3 пакетами считались лишь директории, в которых есть файл __init__.py.
Этот файл одновременно являлся свидетельством того, что директория это Python-пакет, и служил "телом" этого пакета. То есть местом, где можно написать код, как это делается внутри модуля. Этот код исполняется в момент импорта пакета, так что его принято называть "код инициализации пакета".

Начиная с версии 3.3 Любая директория считается пакетом и Python будет пытаться использовать любую директорию для импорта.

Конечно, не любую в файловой системе, а только те что находятся в sys.path.

Это значит, что теперь __init__.py нужно делать только если:

🔸 вам требуется создать код инициализации пакета
🔸 нужна совместимость со старыми версиями Python

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

repo_name/
my_library/
__init__.py
main.py
examples/
exam1.py
exam2.py

В этом репозитории пакетом является только my_library, остальные директории это не пакеты, это просто дополнительный код в файлах. Директория examples не добавлена в sys.path, в ней нет рабочих модулей. Но если она лежит рядом с my_library, то Python вполне сможет импортнуть из неё модули, так как посчитает что examples это валидный пакет.

Конечно, пример несколько надуманный. Никто не будет добавлять корень репозитория в sys.path. Но, я думаю, суть ясна. Иногда директория это просто директория а не пакет!

#basic #pep
Первая директория в sys.path

🔸 Когда вы запускаете Python-интерпретатор в интерактивном режиме, в системные пути (sys.path) в самое начало добавляется текущая рабочая директория

>>> for path in sys.path:
... print(f'"{path}"')
""
"/usr/lib/python37.zip"
"/usr/lib/python3.7"
...

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

🔸 Если вы запускаете интерпретатор передавая скрипт как аргумент, то история получается иная. На первом месте будет директория в которой располагается скрипт. А текущая рабочая директория игнорируется.

Пишем скрипт с таким содержанием:

# script.py
import sys
for path in sys.path:
print(f'"{path}"')

Запускаем

python3 /home/user/dev/script.py

Получаем

"/home/user/dev"
"/usr/lib/python37.zip"
"/usr/lib/python3.7"
...

🔸 Если вы запускаете скрипт по имени модуля то на первом месте будет домашняя директория текущего юзера

python3 -m script

"/home/user"
"/usr/lib/python37.zip"
"/usr/lib/python3.7"
...

Скрипт должен быть доступен для импорта


На что это влияет?
На видимость модулей для импорта. Если вы ждёте, что, запустив скрипт по пути, сможете импортировать модули из текущей рабочей директории, то вы ошибаетесь. Придётся добавлять путь os.getcwd() в sys.path самостоятельно или заранее объявлять переменную PYTHONPATH.

#basic
Многие из тех кто активно работал с Python2 несколько удивлены, почему в Python3 удобная функция reload() переехала из builtin в imp а потом и в importlib?
Ну было же удобно! А теперь лишний импорт😖

Дело в том, что начиная с Python3.3 функция reload() переписана на Python вместо .
Что это нам даёт?

🔸 Такой код проще поддерживать и развивать

🔸 Python код легче читать, изучать и понимать.
Сравните это ➡️ и это ➡️.

🔸 Как результат пункта 2, проще писать свои расширения импорта. Например, пользовательский импортёр с какой-либо хитрой логикой по аналогии с импортом из zip архивов.

А есть ли у этого решения недостатки? Да, они всегда есть.

🔹 Так как это не builtin функция, её следует импортнуть перед использованием

🔹 Скорость замедлилась примерно на 5%. Очевидно, что это совершенно не критично. К тому же от версии к версии логика импорта будет оптимизироваться и ускоряться.
В самом начале файла importlib/__init__.py мы видим такой импорт:

import _imp  # Just the builtin component, NOT the full Python module

То есть часть функционала по прежнему написана на Си, но достаточно низкоуровневая.

#basic
Вопросы про переменную PYTHONPATH

🔸 Как она определяет пути поиска модулей при импорте?

Пути поиска модулей находятся в списке sys.path. Как формируется этот список?
Исходя из документации мы может выделить 3 основных этапа.

▫️ Путь к запускаемому скрипту или рабочая директория
▫️ Переменная PYTHONPATH
▫️ Стандартные пути к библиотекам

Это значит, что все три этапа выполняются в момент инициализации интерпретатора. Результат заполняет список sys.path. В том числе и пути, указанные в переменной PYTHONPATH.

🔸 Можно ли добавлять новые пути в эту переменную в Python-коде?

Можно, но учитывая, что используется она только во время старта интерпретатора, никакого эффекта это иметь не будет.
Для изменения путей поиска модулей в коде нужно изменять непосредственно список sys.path.

🔸 Можно ли указать много путей для поиска?

Да, с помощью переменной PYTHONPATH можно указать несколько директорий, разделённых символом разделения пути. Для Linux это символ ":", для Windows это ";".
Например:

export PYTHONPATH=/mnt/libs:~/mylibs

#basic #tricks
Мы уже знаем, что на текущую сессию интерпретатора изменение PYTHONPATH никак не повлияет. Но если вы запустите дочерний процесс, то он унаследует окружение текущего процесса, а значит и изменения в любых переменных будут на него влиять.
Вот небольшой пример:

Объявляем переменную

user@host:~$ export PYTHONPATH=/path1

Запускаем интерпретатор

user@host:~$ python3

Проверим что в sys.path

>>> import sys
>>> print(sys.path)
['', '/path1', '/usr/lib/...', ...]

Добавляем что-то в переменную

>>> import os
>>> os.emviron['PYTHONPATH'] = '/path1:/path2'
>>> print(sys.path)
['', '/path1', '/usr/lib/...', ...]

Изменений нет. Но давайте запустим дочерний процесс и посмотрим там

>>> os.system('python3')
# теперь мы находимся в другом процессе
>>> import sys
>>> print(sys.path)
['', '/path1', '/path2', '/usr/lib/...', ...]

Тоже самое будет и с subprocess, так как по умолчанию текущее окружение тоже наследуется.

>>> import subprocess
>>> subprocess.call(['python3', '-c', 'import sys;print(sys.path)'])
['', '/path1', '/path2', '/usr/lib/...', ...]

______________________
Лучшей практикой является передача энвайронмента явно через аргумент env!

import subprocess
subprocess.call(cmd, env={'PYTHONPATH': '...'})

Это поможет точно понимать какое окружение будет у запускаемого процесса и при этом не изменять окружение текущего процесса.

#basic
Бывает начинающие в процессе обучения создают файлы с именем модуля который они изучают. В результате на тестовых запусках ничего не работает😱

Всё потому, что появилась коллизия имён. Например, изучаете вы модуль datetime, и создаёте с таким именем файл (ну логично же 😄) прямо в рабочей директории.
Потом, при попытке импортировать datetime модуль, из-за приоритета импорта будет импортирован файл из рабочий директории а не оригинальная библиотека. Ведь имя файла это суть имя модуля!

А знаете ли вы, что не все стандартные модули можно так перезаписать? Коллизии имён не подвержены builtin модули. Они всегда стоят на первом месте в приоритете импорта, поэтому их нельзя заменить.

Полный список таких модулей можно посмотреть в списке sys.builtin_module_names.

То есть, вы сломаете весь Python если назовёте свой модуль os или site, но если назовёте time или gc то ничего страшного не случится)))

Тем не менее, никогда не называйте модули уже занятыми именами!!! ⚠️

Я всегда рекомендую всем своим файлам делать именной префикс из 2-3 символов. Например я называю свои проекты так:

pw_project_name
pw_ui_tools.py
pw_something/main.py

Либо под ситуацию

tst_scriptname.py 
(не "test" чтобы не подхватывал pytest)
dbg_script.py
maya_ui.py
hou_menu_tools.py

И искать проще, и коллизий нет.

#tricks #basic
Как работает функция reload()?

Эта функция нужна для того, чтобы перезагрузить изменившийся код из py-файла без рестарта интерпретатора.
Дело в том, что любой импортированный модуль при повторном импорте не будет перечитывать файл. Функция импорта вернёт уже загруженный в память объект модуля. Чтобы обновить код, нужно либо перезапустить всю программу, либо использовать функцию reload()

from importlib import reload
reload(my_module)

🔸 Функция reload() принимает в качестве аргумента только объект модуля или пакета. Она не может перезагрузить класс или функцию. Только весь файл целиком!

🔸 Перезагрузка пакета перезагрузит только его файл __init__.py, если он есть. Но не вложенные модули.

🔸Она не может перезагрузить ранее не импортированный модуль.

🔸При вызове функция reload() перечитывает и перекомпилирует код в файле, создавая новые объекты. После создания новых объектов перезаписывается ранее созданный неймспейс этого модуля.
Это значит, что если где-то этот модуль импортирован через import и обращение к атрибутам происходит через неймспейс (имя) модуля, то такие атрибуты обновятся.
Если какие-либо объекты из этого модуля импортированы через from то они будут ссылаться на старые объекты.

Напишем простой модуль

# mymodule.py
x = 1

Теперь импортируем модуль и отдельно переменную х из модуля

>>> import mymodule
>>> from mymodule import x
>>> print(mymodule.x)
1
>>> print(x)
1

Не перезапуская интерпретатор вносим изменения в модуль

# mymodule.py
x = 2

Делаем перезагрузку модуля и проверяем х ещё раз

>>> reload(mymodule)
>>> print(mymodule.x)
2
>>> print(x)
1

То же самое будет если присвоить любой объект переменной (даже словарь или список)

Повторный импорт обновляет значение

>>> from mymodule import x
>>> print(x)
2

🔸Созданные инстансы классов не обновятся после перезагрузки модуля. Их придётся пересоздать.

#tricks #basic
Что делать если нужно поставить какую-то Python-библиотеку а root-прав нет? То есть в систему библиотеку никак и ничего не поставить.
Есть как минимум два способа это решить правильно!

🔸 Сделать виртуальное окружение и ставить там что угодно.
Это позволит создать полностью независимое исполняемое окружение для ваших приложений.
Все библиотеки будут храниться в домашней директории юзера а значит доступ на запись имеется.
Создать очень просто:

python3 -m venv ~/venvs/myenvname

Теперь активируем окружение

# Linux
source ~/venvs/myenvname/bin/activate

# Windows
%userprofile%\venvs\myenvname\Scripts\activate.bat

Можно ставить любые библиотеки и запускать приложение.
Это стандартный метод работы с любым проектом. Если еще не используете его, то пора начинать. Даже при наличии root доступа!

🔸 Бывает, что нет возможности запустить приложение из своего виртуального окружения. Например, его запускает какой-то сервис от вашего юзера и вставить активацию окружения вы не можете.

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

python3 -m site

Вы получите что-то такое:

sys.path = [
'/home/user',
'/usr/lib/python37.zip',
'/usr/lib/python3.7',
'/usr/lib/python3.7/lib-dynload',
'/home/user/.local/lib/python3.7/site-packages',
...
]
USER_BASE: '/home/user/.local'
USER_SITE: '/home/user/.local/lib/python3.7/site-packages'
ENABLE_USER_SITE: True

Нас интересует параметр USER_SITE. Это путь к пользовательским библиотекам, которые доступны по умолчанию, если они есть.
Именно сюда будут устанавливаться модули если добавить флаг --user при установке чего-либо через pip

pip install --user requests

Для этой команды не нужны root-права.
После неё можно запускать системный интерпретатор без виртуальных окружений и установленная библиотека будет доступна для текущего юзера.

Параметр USER_BASE показывает корневую директорию для хранения user-библиотек. Её можно изменить с помощью переменной окружения PYTHONUSERBASE

export PYTHONUSERBASE=~/pylibs
python3 -m site
...
USER_BASE: '/home/user/pylibs'
USER_SITE: '/home/user/pylibs/lib/python3.7/site-packages'

Получается некоторое подобие виртуального окружения для бедных 😁 которое можно менять через эту переменную (не делайте так! Лучше venv!)

🔸 Дописывание пути в PYTHONPATH
Этот способ не входит в список "двух правильных", но тоже рабочий. Здесь придётся сделать всё несколько сложней.
Сначала ставим библиотеку в любое место указывая путь установки

pip3 install -t ~/mylibs modulename

Библиотека установится без привязки к какому-либо интерпретатору. То есть по умолчанию не будет видна. Теперь в нужный момент добавляем этот путь в sys.path или в PYTHONPATH.

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

Минусы такого подхода:
▫️Нужно всем хостам пробить нужный путь в .bashrc или ещё куда-то чтобы он сетапился на старте.
▫️Чем больше хостов тем больше нагрузка на сеть. Иногда такой способ не подходит именно по этой причине. Тогда Ansible вам в помощь.
▫️Не очень подходит если хосты с разными операционками. Некоторые библиотеки различаются для Linux и Windows (там, где есть бинарники) и приходится мудрить более сложные схемы.

#tricks #basic
Как правильно проверить атрибуты доступа файла? То есть доступна ли запись в файл или является ли он исполняемым?
Для этого в Python есть функция os.access()
Проверять так:

os.access(path, flag)

Функция вернёт bool в зависимости от наличия указанного флага.

Всего есть 4 флага проверки:

os.F_OK - наличие файла на диске
os.R_OK - доступ на чтение
os.W_OK - доступ на запись
os.X_OK - доступ на исполнение

Например, вместо try-except лучше делать так (пример из документации):

if os.access("myfile", os.R_OK):
with open("myfile") as fp:
return fp.read()

#basic #libs
https://devdocs.io/python~3.9/

Интерактивный поиск по документации Python. Просто наберите имя функции, класса или модуля и сразу получите статью.
Поиск умный, может найти неточное совпадение. Например когда допустили опчепятку в слове. Попробуйте поискать по слову "mltprcsng"

#basic
Один из самых удобных способов записать данные это использование готовых форматов, такие как JSON или YAML.
Из плюсов такого подхода стоит отметить вот что:

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

Но есть и минусы

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

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

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

🔸 очень быстрая запись
🔸 чтение значительно быстрей чем JSON, YAML итд
🔸 размер файла значительно меньше, так как нет разметки
🔸 можно записывать данные по мере поступления не загружая всё в память
🔸 можно извлечь любую часть данных независимо

Из минусов

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

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

Представьте себе JPG-картинку. По сути это немного мета-информации и большой массив пикселей. Тоже самое со звуком или видео файлом. Поэтому, если вы попробуете открыть картинку в текстовом редакторе вы увидите что-то вроде такого

f15d cd29 a564 4578 ...
09e2 9bc4 a696 1253 ...
84e9 4de1 3b23 c24a ...
2534 5161 28e0 709d ...
...

Это и есть записанные байтики. И для их чтения требуется определённый софт который знает что с ними делать. Под каждый тип файла.

К чему это я? Читайте в следующем посте...

#tricks #basic