Недавно возникла такая задача: требовалось из Python скрипта запустить дочерний процесс, тоже Python скрипт, и получить от него некоторые данные. В моём случае это был некий словарь который мог быть сериализован в JSON формат, но это не так важно.
Какие есть варианты это сделать?
1️⃣ Передать дочернему процессу путь к файлу куда и будет записан результат.
После завершение дочернего процесса просто читаем данные из файла.
✅ легко и понятно, все так умеют делать
✅ можно перемещаться по файлу через seek
✅ можно прочитать когда-нибудь потом
❌ обращение к файловой системе, бывает относительно не быстро
❌ какое-то время файл будет доступен любому процессу, небезопасно
❌ только полная запись данных перед чтением (на самом деле есть вариант чтения во время записи, но это не то что мы хотим делать😖)
2️⃣ TCP/UDP сокет
✅ универсально, даже для неродственных процессов
✅ нет обращения в файловой системе (Unix-сокеты это почти файлы но всё равно не совсем)
✅ можно стримить данные
❌ нужна какая-то система авторизация чтобы обезопасить доступ
❌ оверхед для простой передачи данных, особенно если процесс дочерний. Требуется поднятие сервера и организция клиента со всеми вытекающими зависимостями и конструкциями
3️⃣ Парсить аутпут дочернего процесса.
✅ быстро, так как пайпы работают через оперативную память
✅ нет обращения к файловой системе и всех действий с этим связанных
✅ пайп привязан к файловым дескрипторам конкретных процессов, и доступ к нему могут получить только те процессы, которые унаследовали этот дескриптор (или получили другим способом)
✅ передача данных в режиме стрима
❌ неудобно если дочерний процесс пишет логи в stdout, нужна какая-то логика выделения только нужного или как-то отключать логи в надежде что никто другой туда ничего не напишет.
❌ нельзя перемещаться через seek
Если у вас взаимодействие с дочерним процессом, то есть самый простой вариант - кастомный пайп!✨
Это как
Для простоты примера сделаем один пайп. Дочерний процесс должен что-то прислать в родительский процесс.
👮♂️РОДИТЕЛЬСКИЙ ПРОЦЕСС
1. Создаем новый пайп
2. Запускаем дочерний процесс передавая ему номер файла
3. Читаем данные
Чтение прекратится когда файл закроется, за это отвечает контекстный менеджер
Стандартные пайпы тоже можно прочитать
👶 Переходим к коду дочернего процесса.
1. Получаем номер дескриптора
Пишем в него данные
Вот и всё, мы сделали коммуникацию между двумя процессами через кастомный пайп ⭐️
Быстро, легко, безопасно!
С помощью двух пайпов можно ораганизовать передачу сообщений между процессами в обе стороны.
Пример с JSON можно глянуть здесь↗️
#tricks
Какие есть варианты это сделать?
1️⃣ Передать дочернему процессу путь к файлу куда и будет записан результат.
После завершение дочернего процесса просто читаем данные из файла.
✅ легко и понятно, все так умеют делать
✅ можно перемещаться по файлу через seek
✅ можно прочитать когда-нибудь потом
❌ обращение к файловой системе, бывает относительно не быстро
❌ какое-то время файл будет доступен любому процессу, небезопасно
❌ только полная запись данных перед чтением (на самом деле есть вариант чтения во время записи, но это не то что мы хотим делать😖)
2️⃣ TCP/UDP сокет
✅ универсально, даже для неродственных процессов
✅ нет обращения в файловой системе (Unix-сокеты это почти файлы но всё равно не совсем)
✅ можно стримить данные
❌ нужна какая-то система авторизация чтобы обезопасить доступ
❌ оверхед для простой передачи данных, особенно если процесс дочерний. Требуется поднятие сервера и организция клиента со всеми вытекающими зависимостями и конструкциями
3️⃣ Парсить аутпут дочернего процесса.
✅ быстро, так как пайпы работают через оперативную память
✅ нет обращения к файловой системе и всех действий с этим связанных
✅ пайп привязан к файловым дескрипторам конкретных процессов, и доступ к нему могут получить только те процессы, которые унаследовали этот дескриптор (или получили другим способом)
✅ передача данных в режиме стрима
❌ неудобно если дочерний процесс пишет логи в stdout, нужна какая-то логика выделения только нужного или как-то отключать логи в надежде что никто другой туда ничего не напишет.
❌ нельзя перемещаться через seek
Если у вас взаимодействие с дочерним процессом, то есть самый простой вариант - кастомный пайп!✨
Это как
stdout или stderr, но только еще один канал в котором не будет никаких логов и сообщений об ошибках.Для простоты примера сделаем один пайп. Дочерний процесс должен что-то прислать в родительский процесс.
👮♂️РОДИТЕЛЬСКИЙ ПРОЦЕСС
1. Создаем новый пайп
import os. subprocess
read_fd, write_fd = os.pipe()
# важный момент! добавляем возможность наследовать дескриптор дочерним процессом. Обязательно после Python 3.4+ (PEP 446)
os.set_inheritable(write_fd, True)
2. Запускаем дочерний процесс передавая ему номер файла
process = subprocess.Popen(
[sys.executable, child_script, str(write_fd)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
close_fds=False # важный момент! это нужно, чтобы дочерний процесс сохранил все открытые дескрипторы, а не только стандартные потоки
)
os.close(write_fd) # закрываем дескриптор чтобы у родителя не висел открытый конец записи, иначе в читающем конце не наступит EOF
3. Читаем данные
with os.fdopen(read_fd, 'r') as data_pipe:
data = data_pipe.read()
print('RECEIVED:', data)
Чтение прекратится когда файл закроется, за это отвечает контекстный менеджер
with в дочернем процессе.Стандартные пайпы тоже можно прочитать
stdout_log, stderr_log = process.communicate()
print(stdout_log)
print(stderr_log)
👶 Переходим к коду дочернего процесса.
1. Получаем номер дескриптора
write_pipe_fd = int(sys.argv[-1])
Пишем в него данные
with os.fdopen(write_pipe_fd, 'w') as data_pipe:
data_pipe.write('Hello!')
data_pipe.flush()
Вот и всё, мы сделали коммуникацию между двумя процессами через кастомный пайп ⭐️
Быстро, легко, безопасно!
С помощью двух пайпов можно ораганизовать передачу сообщений между процессами в обе стороны.
Пример с JSON можно глянуть здесь↗️
#tricks
Gist
custom-pipe-example.py
GitHub Gist: instantly share code, notes, and snippets.
🔥12👍4❤2
Быстрый встроенный профайлинг на Linux с помощью time
Покажет время выполнения процесса
Но это встроенная команда из моей оболочки. Есть такая же GNU-утилита и она может показывать больше информации. Но нужно вызывать по абсолютному пути, так как builtin команда имеет бОльший приоритет.
Кроме времени исполнения будет также показано много другой полезной информации
- эффективность использования CPU (в %)
- максимальный объем занятой памяти
- обращения к файлам
- код выхода
И другие сведения.
#tricks
time python -c 'for i in range(10**7): i**2'
Покажет время выполнения процесса
real 0m2,470s
user 0m2,405s
sys 0m0,074s
real - Общее время, прошедшее с момента запуска до завершения программы. Включая время ожидания I\O или переключения контекста.user - Количество времени, которое CPU потратил на выполнение кода самой программы в пользовательском режиме.sys - Количество времени, которое CPU потратил на выполнение системных вызовов (операций ядра, таких как чтение/запись файлов, управление памятью) от имени программы.Но это встроенная команда из моей оболочки. Есть такая же GNU-утилита и она может показывать больше информации. Но нужно вызывать по абсолютному пути, так как builtin команда имеет бОльший приоритет.
/usr/bin/time -v python -c 'for i in range(10**7): i**2'
Command being timed: "python -c for i in range(10**7): i**2"
User time (seconds): 2.38
System time (seconds): 0.07
Percent of CPU this job got: 100%
...
Кроме времени исполнения будет также показано много другой полезной информации
- эффективность использования CPU (в %)
- максимальный объем занятой памяти
- обращения к файлам
- код выхода
И другие сведения.
#tricks
🔥8❤2👍2
В работе с медиа файлами часто требуется определить не просто расширение, а его, скажем так, "категорию". Тоесть определить это видео, аудио или картинка. Примерно в 10 случаях из 10 в ревью я вижу обычный хардкодинг с большим мапингом и соответствующим поиском по нему.
Для таких случаев есть простой способ - стандартная библиотека
Причём ей не нужен файл, достаточно просто имени строкой.
Первый элемент кортежа это MIME-тип (Multipurpose Internet Mail Extensions Type) - стандартный способ идентификации формата файла.
Формат:
Второй элемент это тип кодировки содержимого, обычно для контейнеров типа gz и аналогичных.
Итого, узнать категорию файла одной строкой:
Конечно при условии, что тип будет распознан, иначе будет
#libs #tricks
file_type_by_ext = {
'video': ['.mp4', '.mov', '.mkv', ...],
'audio': ['.mp3', '.wav', '.ogg', ...],
'image': ['.jpg', '.png', '.exr', ...]
}Для таких случаев есть простой способ - стандартная библиотека
mimetypes.import mimetypes
mimetypes.guess_type("example.txt")
# ('text/plain', None)
Причём ей не нужен файл, достаточно просто имени строкой.
Первый элемент кортежа это MIME-тип (Multipurpose Internet Mail Extensions Type) - стандартный способ идентификации формата файла.
Формат:
type/subtypetype - общая категория данных (text, video, image)subtype - конкретный формат внутри категорииmimetypes.guess_type("photo.jpg")
# ('image/jpeg', None)
mimetypes.guess_type("render.mp4")
# ('video/mp4', None)Второй элемент это тип кодировки содержимого, обычно для контейнеров типа gz и аналогичных.
mimetypes.guess_type("file.tar.gz")
# ('application/x-tar', 'gzip')
mimetypes.guess_type("backup.tar.bz2")
# ('application/x-tar', 'bzip2')Итого, узнать категорию файла одной строкой:
mimetypes.guess_type('myfile.mov')[0].split('/')[0]
# videoКонечно при условии, что тип будет распознан, иначе будет
None а не строка. Но об этом в следующий раз.#libs #tricks
👍19
import mimetypes
mimetypes.guess_type("example.fbx")
# (None, None)
Формат не распознан, так как не зарегистрирован в системе.
Регистрация происходит с помощью функции
mimetypes.init(). Эта функция автоматически вызывается при первом обращении.Для каждой OS работает по-разному. В Windows читает реестр, в Linux достает всё из файла
/etc/mime.types, в MacOS читает из системной БД.На linux можно попробовать распознать тип через вызов
file --mime-type -b <filename>
эта команда попробует прочитать метадату самого файла, то есть должен быть доступ к файлу. Но это не гарантия успеха.
Можно попробовать использовать нестрогое соответствие IANA с помощью флага
strict=False. Тогда будут учтены старые и нестандартные типы. Обычно они с префиксом x-Новые типы можно добавлять самостоятельно.
mimetypes.add_type('application/x-fbx', '.fbx') # с точкой
mimetypes.guess_type("example.fbx")
# ('application/x-fbx', None)Либо вызвать
init() еще раз передав список текстовых файлов с нужными вам типами (без точки)# my-mime-types.txt
application/x-fbx fbx
application/x-ogo ogo
application/x-aga aga
mimetypes.init(['my-mime-types.txt'])
mimetypes.guess_type("example.ogo")
# ('application/x-ogo', None)
Есть и обратная операция - получить расширение файла из mime-типа
mimetypes.guess_extension('image/jpeg')
# .jpgИли все подходящие расширения
mimetypes.guess_all_extensions('image/jpeg')
# ['.jpg', '.jpe', '.jpeg', '.jfif']Советую почитать полную документацию
Также обратите внимание на библиотеку content-types для работы с mime-типами, где больше возможностей.
#libs #tricks
🔥4
Все знают синтаксический сахар с операторами
Где под капотом он превращается в
Останется ли переменная
Конечно нет, это же неизменяемый тип
Теперь провернём тоже самое со списком
Ожидаемо работает так же, ведь мы создали новую переменную.
А теперь попробуем иначе:
И, внезапно, это работает не так как с int, со списками оператор
То же самое будет с
Следует помнить о такой важной разнице!
(Особенно на собесах 😉)
#tricks
+=, -= и тдx += 1
Где под капотом он превращается в
x = x + 1
Останется ли переменная
х той же переменной после +=?Конечно нет, это же неизменяемый тип
x = 1
print(id(x))
# 135373664533280
x += 1
print(id(x))
# 135373664533312
Теперь провернём тоже самое со списком
ls = [1, 2]
print(id(ls))
# 135373622585344
ls = ls + [3]
print(id(ls))
# 135373619036608
Ожидаемо работает так же, ведь мы создали новую переменную.
А теперь попробуем иначе:
ls = [1, 2]
print(id(ls))
# 135373622585344
ls += [3]
print(id(ls))
# 135373622585344
print(ls)
# [1, 2, 3]
И, внезапно, это работает не так как с int, со списками оператор
+= работает как extend()!То же самое будет с
*=, объект останется тем же.ls = [1, 2]
print(id(ls))
# 135373622585344
ls *= 2
print(id(ls))
# 135373622585344
print(ls)
# [1, 2, 1, 2]
Следует помнить о такой важной разнице!
#tricks
👍13❤1
Не запуская код определите, что покажет терминал если выполнить следующее:
Ответ:Несмотря на то, что ваш IDE покажет ошибку, ошибки не будет. Распечатается "c"
Объяснение:
1. Mangling
За это отвечает механизм mangling - искажение имени. Так работают приватные атрибуты классов.
При создании атрибута по правилу: минимум 2 "_" в начале и максимум 1 "_" в конце" имя автоматически становится вида _{classname}{attr}
В нашем случае атрибутов класса не создается, но это не отменяет Mangling при обращении к другим объектам внутри класса.
2. Обращение к атрибуту
Когда внутри класса происходит обращение к любому объекту с именем по указанному выше правилу, его имя на уровне байт кода также преобразуется.
3. Поиск
Далее происходит поиск такой переменной по неймспейсам в порядке LEGB - Local, Enclosing, Global, Built-in.
И не трудно догадаться что мы находим нужный атрибут в Global, В итоге получаем результат!
Проверить можно так:
Либо удалите переменную
Как думаете, это норма или баг?
#tricks
_A__b = 'c'
class A:
def get(self):
return __b
print(A().get())
Ответ:
Объяснение:
1. Mangling
За это отвечает механизм mangling - искажение имени. Так работают приватные атрибуты классов.
При создании атрибута по правилу: минимум 2 "_" в начале и максимум 1 "_" в конце" имя автоматически становится вида _{classname}{attr}
В нашем случае атрибутов класса не создается, но это не отменяет Mangling при обращении к другим объектам внутри класса.
2. Обращение к атрибуту
Когда внутри класса происходит обращение к любому объекту с именем по указанному выше правилу, его имя на уровне байт кода также преобразуется.
3. Поиск
Далее происходит поиск такой переменной по неймспейсам в порядке LEGB - Local, Enclosing, Global, Built-in.
И не трудно догадаться что мы находим нужный атрибут в Global, В итоге получаем результат!
Проверить можно так:
import dis
dis.dis(A.get)
# 4 RESUME 0
#
# 5 LOAD_GLOBAL 0 (_A__b)
# RETURN_VALUE
Либо удалите переменную
_A__b и запустите еще раз, поулчите ошибку:NameError: name '_A__b' is not defined
Как думаете, это норма или баг?
#tricks
❤5😢1