Для Python3.8 в PEP0578 добавили функционал аудита Runtime операций. Это позволяет выполнять хуки (функции) при возникновении определённых событий в интерпретаторе.
Этот функционал разработан, прежде всего, для специалистов по безопасности. Аудит позволяет перехватывать различные действия и проверять их допустимость.
Полный список стандартных аудит-ивентов можно посмотреть здесь. Из названий становится ясно что мы можем перехватить. Например, мы можем перехватить факт открытия файла (
Примеры использования:
▫️Представим, что после разработки и долгих тестирований веб сервиса вы могли где-то оставить функцию ручного ввода данных в консоль. На продакшене такое недопустимо. С помощью аудита можно вызвать исключение перехватив ивент
▫️Ивент
Как добавить свой хук?
Для первого теста выполните такой код в самом начале работы приложения
Теперь давайте посмотрим какие web-коннекты создаются при работе нашего кода. Для запросов используем requests.
А так же:
▫️есть платформозависимые хуки (например взаимодействие с winapi)
▫️можно писать хуки на Си
▫️так как это мера для обеспечения безопасности, нет способа удалить хуки после добавления.
Напоминаю, доступно в Python3.8+
#pep #tricks
Этот функционал разработан, прежде всего, для специалистов по безопасности. Аудит позволяет перехватывать различные действия и проверять их допустимость.
Полный список стандартных аудит-ивентов можно посмотреть здесь. Из названий становится ясно что мы можем перехватить. Например, мы можем перехватить факт открытия файла (
open), импорта модуля (import), копирование файла (shutil.copyfile), запуск процесса (subprocess.Popen) и тд. Как минимум мы можем залогировать данное событие, как максимум, вызвать аварийное завершение программы.Примеры использования:
▫️Представим, что после разработки и долгих тестирований веб сервиса вы могли где-то оставить функцию ручного ввода данных в консоль. На продакшене такое недопустимо. С помощью аудита можно вызвать исключение перехватив ивент
builtins.input
▫️С помощью ивента socket.getaddrinfo можно определить на какие сайты юзер заходил с помощью вашего приложения.▫️Ивент
exec позволит проверить загруженный объект кода перед его исполнением. Например выявить потенциально опасный код, или оставленные API ключи в строковых переменных.Как добавить свой хук?
Для первого теста выполните такой код в самом начале работы приложения
import sysКаждый хук вызывается для всех событий, поэтому мы можем с помощью одного хука увидеть всё что происходит в интерпретаторе.
def hook(event, args):
print(f'EVENT: {event}{args}')
sys.addaudithook(hook)
Теперь давайте посмотрим какие web-коннекты создаются при работе нашего кода. Для запросов используем requests.
import sysВ аутпуте вы увидите домен, на который был сделан запрос.
import requests
def socket_hook(event, args):
if event == 'socket.getaddrinfo':
print(args[0])
sys.addaudithook(socket_hook)
requests.get('https://google.com')
А так же:
▫️есть платформозависимые хуки (например взаимодействие с winapi)
▫️можно писать хуки на Си
▫️так как это мера для обеспечения безопасности, нет способа удалить хуки после добавления.
Напоминаю, доступно в Python3.8+
#pep #tricks
Python.org
PEP 578 -- Python Runtime Audit Hooks
The official home of the Python Programming Language
Кроме стандартных системных ивентов аудита можно вызывать свои ивенты. Для этого используется функция sys.audit()
Такое будет полезно если вы разрабатываете серьезную библиотеку в которой вопрос безопасности стоит не на последнем месте. Дать возможность другим разработчикам добавлять свои проверки будет хорошей практикой!
Хотя никто не запрещает придумать свой способ использования перехвата подобных событий.
Особенно это будет знакомо тем, кто пишет приложения на PyQt. Очень уж похоже на систему сигнал/слот в режиме DirectConnection.
▫️ следует вызывать sys.audit() ПОСЛЕ проверки данных но ДО фактического применения и исполнения логики. Это поможет избежать лишней проверки данных вне основной логики и прервать операцию в нужный момент.
▫️ в имени ивента рекомендуется использовать неймспейс с именем вашего модуля.
#tricks
import sysПросто указываем имя ивента первым аргументом и далее любые аргументы.
def my_hook(event, args):
if event == 'mymodule.myevent':
print('Catched!', args)
sys.addaudithook(my_hook)
sys.audit('mymodule.myevent', 1, 2, 3)
Такое будет полезно если вы разрабатываете серьезную библиотеку в которой вопрос безопасности стоит не на последнем месте. Дать возможность другим разработчикам добавлять свои проверки будет хорошей практикой!
Хотя никто не запрещает придумать свой способ использования перехвата подобных событий.
Особенно это будет знакомо тем, кто пишет приложения на PyQt. Очень уж похоже на систему сигнал/слот в режиме DirectConnection.
▫️ следует вызывать sys.audit() ПОСЛЕ проверки данных но ДО фактического применения и исполнения логики. Это поможет избежать лишней проверки данных вне основной логики и прервать операцию в нужный момент.
▫️ в имени ивента рекомендуется использовать неймспейс с именем вашего модуля.
#tricks
Стандартная Django админка работает стабильно но не могу сказать что она меня устраивает. Да, есть базовый набор стандартного функционала, но чего-то всегда не хватает. Может автокомплитов а может тёмной темы. 😎
Долгое время я использовал DjangoSuit, но проект заглох на невыпущенной альфе 2й версии которая в Django3 и вовсе не поддерживается.
Что же можно сегодня посоветовать?
На сегодняшний день у меня два фаворита:
🚀 DjangoJET
🌎 сайт
🗄 репозиторий
▶️ видео
Крутая и красивая админка с кучей плюшек.
▫️автокомплиты с AJAX подгрузкой
▫️темы
▫️дашборды
▫️респонсив
▫️кастомизация панелей прямо в админке
Стоит денег для коммерческих продуктов. Для опенсорса бесплатно (AGPL)!
🎷 Django Jazzmin
🗄репозиторий
Не такая пафосная но от этого не менее крутая админка.
▫️полностью кастомизируется
▫️много готовых тем и возможность собрать свою прямо в админке
▫️Bootsrtap Model окна вместо всплывающих окон
▫️интегрирован Select2
▫️респонсив
Знаете еще крутые админки? Напишите в комментах!
#django #libs
Долгое время я использовал DjangoSuit, но проект заглох на невыпущенной альфе 2й версии которая в Django3 и вовсе не поддерживается.
Что же можно сегодня посоветовать?
На сегодняшний день у меня два фаворита:
🚀 DjangoJET
🌎 сайт
🗄 репозиторий
▶️ видео
Крутая и красивая админка с кучей плюшек.
▫️автокомплиты с AJAX подгрузкой
▫️темы
▫️дашборды
▫️респонсив
▫️кастомизация панелей прямо в админке
Стоит денег для коммерческих продуктов. Для опенсорса бесплатно (AGPL)!
🎷 Django Jazzmin
🗄репозиторий
Не такая пафосная но от этого не менее крутая админка.
▫️полностью кастомизируется
▫️много готовых тем и возможность собрать свою прямо в админке
▫️Bootsrtap Model окна вместо всплывающих окон
▫️интегрирован Select2
▫️респонсив
Знаете еще крутые админки? Напишите в комментах!
#django #libs
Иногда хочется чтобы в качестве объекта передачи данных был удобный класс но не хочется (или нет возможности) писать сераиализатор в JSON для него. Идеально было бы сделать класс, который сам умел бы сериализоваться в JSON дефолтным модулем без указания дополнительных сериализаторов.
Как это сделать?
Стандартный модуль JSON умеет правильно сериализовать стандартные типы. Но нам нужен кастомный класс с удобными методами и свойствами. Ответ очевиден - наследуемся от стандартного типа!
В качестве базового типа возьмем словарь. Например, мне нужен класс некоего абстрактного элемента списка с именем и индексом. Тогда реализация может быть такой.
Для более сложных случаев можно посмотреть на библиотеку dataclasses-json, которая поможет сериализовать dataclasses.
#tricks #libs
Как это сделать?
Стандартный модуль JSON умеет правильно сериализовать стандартные типы. Но нам нужен кастомный класс с удобными методами и свойствами. Ответ очевиден - наследуемся от стандартного типа!
В качестве базового типа возьмем словарь. Например, мне нужен класс некоего абстрактного элемента списка с именем и индексом. Тогда реализация может быть такой.
class Item(dict):Я добавил два свойства с простым переназначением данных в словарь. Но предполагается, что там будут проверки значений и иные вычисления. Еще хорошо бы добавить методы
def __init__(self, name, index=0):
super().__init__(name=name, index=index)
@property
def name(self):
return self['name']
@name.setter
def name(self, value):
self['name'] = value
@property
def index(self):
return self['index']
@index.setter
def index(self, value):
self['index'] = value
def __setitem__(self, key, value):
if key not in ['index', 'name']:
raise KeyError
super().__setitem__(key, value)
__str__ и __repr__.>>> it = Item('item name')
>>> it.name
'item name'
>>> it.index
0
Также можно добавить любые методы классу. Например я переопределил __setitem__ чтобы нельзя было задать иные ключи.>>> it['key'] = 123Хотя, можно сделать как-то иначе. К примеру, все отличные от основных ключи записывать в отдельный ключ.
KeyError
def __setitem__(self, key, value):Ну а главная особенность этого класса в том, что он легко понимается стандартным модулем json (конечно, если значения ключей это тоже стандартные типы или наследованные от них)
if key not in ['index', 'name']:
self['meta'][key] = value
else:
super(Item, self).setitem(key, value)
>>> json.dumps(item)Самый очевидный пример, использование в библиотеке requests. Передавая объект в аргумент json, мы не можем повлиять на его сериализацию, то есть добавить класс-сериализатор. Там ожидается готовый словарь или иной совместимый тип.
'{"index": 2, "name": "item name"}'
import requestsА вот так выглядит альтернатива с методом toJson()
requests.post(url, json=item)
requests.post(url, data=item.toJson(),А ещё ничто не мешает нам в любой момент конвертнуть наш класс в обычный словарь
headers={'Content-Type': 'application/json'})
>>> dict(item)Не спорю, возможно есть более "умные" решения этой задачи 🤓 Но, тем не менее, аналогичным образом работает стандартный collections.namedtuple. Он тоже наследуется от стандартного tuple, расширяет его функционал и сериализуется без дополнительных средств.
{"index": 2, "name": "item name"}
Для более сложных случаев можно посмотреть на библиотеку dataclasses-json, которая поможет сериализовать dataclasses.
#tricks #libs
Порой админу проще написать огромный bash-скрипт с запросами в БД и парсингом через регулярки чем пытаться тоже самое изобразить в Python. Тоже самое верно и в обратном случае. Даже простые shell команды порой удобней записать в Python скрипт чем на bash. Правда, в обоих случаях выглядит это всё не очень.
Если сталкивались с таким, то поймёте о чем я😢
Специально для вас сделал подборку библиотеки для работы с shell из Python. Чтобы ваш код оставался красивым и читабельным!
➡️ sh
Переносим bash синтаксис в Python. Удобно и немногословно.
Ранее я уже рассказывал про этот модуль, можно глянуть в этом посте.
➡️ plumbum
Аналогичен sh но со своими плюшками. Например встроенная колоризация вывода
➡️ pexpect
Библиотека для интерактивного взаимодействия с процессом через stdin/stdout.
Нашел небольшой гайд на русском.
➡️ shell_command
Еще одна библиотека для вызова shell-команд. Ориентируется на безопасность вызываемого кода и заточена на удобство для админов.
Бонусом
➡️ envoy
Обёртка вокруг subprocess делающая его использование еще проще и минималистичней.
_______________
Кстати, до Python 2.6 вместо subprocess была библиотека commands. Выглядит достаточно аскетично)
#libs
Если сталкивались с таким, то поймёте о чем я😢
Специально для вас сделал подборку библиотеки для работы с shell из Python. Чтобы ваш код оставался красивым и читабельным!
➡️ sh
Переносим bash синтаксис в Python. Удобно и немногословно.
Ранее я уже рассказывал про этот модуль, можно глянуть в этом посте.
➡️ plumbum
Аналогичен sh но со своими плюшками. Например встроенная колоризация вывода
➡️ pexpect
Библиотека для интерактивного взаимодействия с процессом через stdin/stdout.
Нашел небольшой гайд на русском.
➡️ shell_command
Еще одна библиотека для вызова shell-команд. Ориентируется на безопасность вызываемого кода и заточена на удобство для админов.
Бонусом
➡️ envoy
Обёртка вокруг subprocess делающая его использование еще проще и минималистичней.
_______________
Кстати, до Python 2.6 вместо subprocess была библиотека commands. Выглядит достаточно аскетично)
#libs
Telegram
Python Заметки
Python + bash
Если вам часто требуется запускать shell команды из Python-кода, какой способ вы используете?
Самый низкоуровневый это функция os.system(), либо os.popen(). Рекомендованный способ это subprocess.call(). Но это всё еще достаточно неудобно.
Советую…
Если вам часто требуется запускать shell команды из Python-кода, какой способ вы используете?
Самый низкоуровневый это функция os.system(), либо os.popen(). Рекомендованный способ это subprocess.call(). Но это всё еще достаточно неудобно.
Советую…
https://devdocs.io/python~3.9/
Интерактивный поиск по документации Python. Просто наберите имя функции, класса или модуля и сразу получите статью.
Поиск умный, может найти неточное совпадение. Например когда допустили опчепятку в слове. Попробуйте поискать по слову "mltprcsng"
#basic
Интерактивный поиск по документации Python. Просто наберите имя функции, класса или модуля и сразу получите статью.
Поиск умный, может найти неточное совпадение. Например когда допустили опчепятку в слове. Попробуйте поискать по слову "mltprcsng"
#basic
import sys
print(sys.implementation)
Выполнив этот код, вы увидите некий объект namespace с данными о текущей имплементации интерпретатора.Помимо того что эта информация может быть как-либо полезна давайте обратим внимание на то, что это за объект вообще?
Узнаем что это за тип
>>> type(sys.implementation)
<class 'types.SimpleNamespace'>
SimpleNamespace это простой тип для реализации неймспейса.Если не знаете что такое неймспейс, то представьте себе некий объект-контейнер, куда можно складывать другие объекты. После чего обращаться к ним через имя контейнера. То есть, к имени объекта добавляется дополнительный уровень имени, что и является пространством имён.
По сути, любой модуль и класс является неймспейсом для своего содержимого (если не импортить это содержимое из модуля через "*").
Класс SimpleNamespace позволяет легко добавлять и удалять атрибуты. А также можно передать в конструктор keyword аргументы чтобы сразу создать нужные атрибуты
>>> from types import SimpleNamespace
>>> conf = SimpleNamespace(key1=1, key2=2)
>>> conf.key1
1
>>> setattr(conf, 'key3', 3)
или>>> conf.key3 = 3
>>> conf.key3
3
>>> delattr(conf, 'key2')
или>>> del conf.key2
>>> conf
namespace(key1=1, key3=3)
Вот так можно преобразовать словарь в неймспейс:>>> keys_dict = {'k1': 1, 'k2': 2}
>>> keys = SimpleNamespace(**keys_dict)
>>> keys
namespace(k1=1, k2=2)
Обратное преобразование через vars>>> vars(conf)
{'key1': 1, 'key3': 3}
Как это можно использовать?▫️ красиво оформить "константы" или конфиг. В этом случае, кстати, я бы делал ключи аперкейсом.
>>> conf = SimpleNamespace(
**json.load(config_file.open())
)
>>> conf.API_HOST
"192.168.10.20"
▫️ быстро преобразовать словарь в подобие объекта с доступом к ключам через атрибуты. То есть вместо такой записи: shape['width']
можно будет писать так: shape.width
▫️альтернатива для namedtuple, в которой доступно изменение значения атрибутов.#tricks
🤩 Разбираем полезные исходники!
Функция, возвращающая словарь с данными о панели задач.
🔸 Как может пригодиться?
Я использую для открытия виджета по клику на значке в трее с выравниванием по таскбару. Аналогично работает попап у Dropbox клиента.
🔸 Тестил на Windows10 и Debian10.
🔸 В комплекте проверочный виджет который при создании точно перекрывает таскбар. Проверка правильности определения геометрии таскбара. Закрывается по клику.
🔸 Ещё в комплекте пример окошка, которое появляется в районе часов над таскбаром.
🔸 Есть один баг. При перекрытии тасбара в Gnome (linux) виджет не получает событий от мыши.
Решения пока не нашел😕
Код забираем здесь ↗️
#source #qt
Функция, возвращающая словарь с данными о панели задач.
screen : номер монитораlocation : расположение на экране (внизу, слева и тд)geometry : QRect с координатами таскбараsystem_tray : доступен ли системный трей🔸 Как может пригодиться?
Я использую для открытия виджета по клику на значке в трее с выравниванием по таскбару. Аналогично работает попап у Dropbox клиента.
🔸 Тестил на Windows10 и Debian10.
🔸 В комплекте проверочный виджет который при создании точно перекрывает таскбар. Проверка правильности определения геометрии таскбара. Закрывается по клику.
🔸 Ещё в комплекте пример окошка, которое появляется в районе часов над таскбаром.
🔸 Есть один баг. При перекрытии тасбара в Gnome (linux) виджет не получает событий от мыши.
Решения пока не нашел😕
Код забираем здесь ↗️
#source #qt
Вы используете свойства (
Удобная штука, скажу я вам! Но работают они только для инстансов класса. Вот простой пример:
Как сделать подобие property для класса?
Есть несколько способов:
🔸 Написать свой декоратор в точности повторяющий функционал property. Это ведь не сложно🤓
Я тоже когда-то писал свою версию подобного функционала.
Примеры смотрим здесь↗️ и здесь↗️.
Всё это здорово, но не очень-то хочется изобретать велосипед. Есть ли что-то готовое из стандартных средств?
🔸 Если вы уже перешли на Python 3.9 то можете написать вот так:
Плохая новость — не получится сделать setter, по крайней мере лаконично и красиво.
Например, вот так не работает:
#tricks
@property) в классах?Удобная штука, скажу я вам! Но работают они только для инстансов класса. Вот простой пример:
class A:Создаём класс и получаем значение свойства
@property
def prop(self):
return 10
>>> a = A()А что будет если вызвать свойство у класса
>>> a.prop
10
>>> A.propЭх, несвезло😢
<property object at 0x000...318>
Как сделать подобие property для класса?
Есть несколько способов:
🔸 Написать свой декоратор в точности повторяющий функционал property. Это ведь не сложно🤓
Я тоже когда-то писал свою версию подобного функционала.
Примеры смотрим здесь↗️ и здесь↗️.
Всё это здорово, но не очень-то хочется изобретать велосипед. Есть ли что-то готовое из стандартных средств?
🔸 Если вы уже перешли на Python 3.9 то можете написать вот так:
class С:Например, вот так можно динамический докстринг сделать
@classmethod
@property
def x(cls):
return 2+2
>>> C.x
4
class C:Хорошая новость в том, что это работает из коробки, ничего дописывать не надо.
@classmethod
@property
def __doc__(cls) -> str:
return f"A doc for {cls.__name__!r}"
>>> help(C)
class C(builtins.object)
| A doc for 'C'
...
Плохая новость — не получится сделать setter, по крайней мере лаконично и красиво.
Например, вот так не работает:
class C:Выражение присвоения просто перезаписывает атрибут
_x = 0
def x_get(cls):
return cls._x
def x_set(cls, value):
cls._x = value
x = classmethod(property(x_get, x_set))
x, а не вызывает setter.>>> C.x = 1Чтобы setter тоже работал, нужно сделать иначе. Но об этом смотрите в следующем посте.
>>> C._x
0
#tricks
GitHub
GitHub - mammo0/py-simple-classproperty
Contribute to mammo0/py-simple-classproperty development by creating an account on GitHub.
Продолжаем со свойствами классов. Теперь мы хотим иметь рабочий setter.
🔸 Вместо класса создаем свойства для его мета-класса
🔸 Динамический атрибут класса. Похож на прошлый пример но с дополнительной фишкой.
Что он делает? Этот декоратор не только позволяет добавить свойство класса но и разделить функционал для свойства класса и свойства инстанса.
Работает это через дополнительный вызов
🔸 Вместо класса создаем свойства для его мета-класса
class CMeta(type):При этом можем изменить дефолтное значение для унаследованного класса. Плюс, как и в обычном property можно сделать getter, setter и deleter.
_x = 0
@property
def x(cls):
return cls._x
@x.setter
def x(cls, value):
cls._x = value
class C(metaclass=CMeta):
_x = 2
>>> C.xПроверим, действительно ли сработал setter или мы просто перезаписали атрибут
2
>>> C.x = 34
>>> C.x
34
x
>>> C._xДа, всё верно! 😎
34
🔸 Динамический атрибут класса. Похож на прошлый пример но с дополнительной фишкой.
Что он делает? Этот декоратор не только позволяет добавить свойство класса но и разделить функционал для свойства класса и свойства инстанса.
Работает это через дополнительный вызов
__getattr__ и __setattr__ мета-класса, где требуется проверить имя атрибута и сделать соответствующие выводы.from types import DynamicClassAttribute#tricks
class CMeta(type):
def __getattr__(self, item):
if item == 'x': # проверка имени
return 'x from class'
raise AttributeError
def __setattr__(self, key, value):
print('set class', key, '=', value)
class C(metaclass=CMeta):
@DynamicClassAttribute
def x(self):
return 'x from instance'
@x.setter
def x(self, value):
print('set instance x =', value)
>>> C.x
'from class'
>>> C().x
'from instance'
>>> C.x = 2
'set class x = 2'
C().x = 2
'set instance x = 2'
А как вам идея сделать свойство для модуля?!
К сожалению, тут без внешних библиотек не обойтись. Но выглядит интересно!
Теперь обращаемся к функции как к объекту модуля:
К сожалению, тут без внешних библиотек не обойтись. Но выглядит интересно!
# my_module.pyПо аналогии со свойствами класса и инстанса в функцию первым аргументом прилетает объект текущего модуля.
from mprop import mproperty
@mproperty
def x(mod):
print(f"Prop from '{mod.__name__}'")
return 2+2
Теперь обращаемся к функции как к объекту модуля:
>>> import my_module#tricks #libs
>>> my_module.x
Prop from 'my_module'
4
GitHub
GitHub - josiahcarlson/mprop: Module properties for Python
Module properties for Python. Contribute to josiahcarlson/mprop development by creating an account on GitHub.
В PyCharm есть окно со статистикой продуктивности (Help / Productivity Guide), где отображена интересная информация об использовании IDE.
В частности, можно узнать сколько нажатий на кнопки вам сэкономил автокомплит.
У меня 231К за 14 месяцев !
Не плохо конечно, но клавиатура всё равно стёрлась)))
🙌⌨️😵
#offtop
В частности, можно узнать сколько нажатий на кнопки вам сэкономил автокомплит.
У меня 231К за 14 месяцев !
Не плохо конечно, но клавиатура всё равно стёрлась)))
🙌⌨️😵
#offtop
Один из самых удобных способов записать данные это использование готовых форматов, такие как JSON или YAML.
Из плюсов такого подхода стоит отметить вот что:
🔸 готовый, повсеместно используемый и поддерживаемый формат
🔸 простой и понятный файл, удобочитаемый для человека
🔸 можно легко редактировать в любом текстовом редакторе без специальных программ и библиотек
Но есть и минусы
🔹 затраты времени при записи файла (кодирование данных в нужный формат строки)
🔹 затраты времени при чтении файла (декодирование данных в Python объекты)
🔹 размер файла увеличивается из-за разметки данных (скобки, запятые, переносы, отступы...)
🔹 перед записью все данные должны быть помещены в память в полном объёме (не всегда)
🔹 при чтении необходимо считать весь файл в память и только потом декодировать данные
Если нужно писать немного данных в несколько файлов, то затраты по времени не ощутимы. Обычно это файлы конфига или какие-либо метаданные. Это отличный вариант под такие задачи.
Есть и другой поход к записи файлов - это бинарные файлы. Используется, когда данных достаточно много и никто их не собирается читать глазками😳.
🔸 очень быстрая запись
🔸 чтение значительно быстрей чем JSON, YAML итд
🔸 размер файла значительно меньше, так как нет разметки
🔸 можно записывать данные по мере поступления не загружая всё в память
🔸 можно извлечь любую часть данных независимо
Из минусов
🔹 нужно определить свой формат записи данных (если не используете готовую спецификацию определённого формата)
🔹 не получится открыть файл и визуально понять что там записано, а для чтения файла потребуется знать его спецификацию.
🔹 не так-то просто создать такой файл без специальной библиотеки
В таком виде удобно записывать большой массив любых однородных данных. Например, мониторинг валютной биржи или кэшированная анимация 3D геометрии.
(Это не означает что нельзя записать данные разного типа, просто это будет не так удобно)
Представьте себе JPG-картинку. По сути это немного мета-информации и большой массив пикселей. Тоже самое со звуком или видео файлом. Поэтому, если вы попробуете открыть картинку в текстовом редакторе вы увидите что-то вроде такого
К чему это я? Читайте в следующем посте...
#tricks #basic
Из плюсов такого подхода стоит отметить вот что:
🔸 готовый, повсеместно используемый и поддерживаемый формат
🔸 простой и понятный файл, удобочитаемый для человека
🔸 можно легко редактировать в любом текстовом редакторе без специальных программ и библиотек
Но есть и минусы
🔹 затраты времени при записи файла (кодирование данных в нужный формат строки)
🔹 затраты времени при чтении файла (декодирование данных в Python объекты)
🔹 размер файла увеличивается из-за разметки данных (скобки, запятые, переносы, отступы...)
🔹 перед записью все данные должны быть помещены в память в полном объёме (не всегда)
🔹 при чтении необходимо считать весь файл в память и только потом декодировать данные
Если нужно писать немного данных в несколько файлов, то затраты по времени не ощутимы. Обычно это файлы конфига или какие-либо метаданные. Это отличный вариант под такие задачи.
Есть и другой поход к записи файлов - это бинарные файлы. Используется, когда данных достаточно много и никто их не собирается читать глазками😳.
🔸 очень быстрая запись
🔸 чтение значительно быстрей чем JSON, YAML итд
🔸 размер файла значительно меньше, так как нет разметки
🔸 можно записывать данные по мере поступления не загружая всё в память
🔸 можно извлечь любую часть данных независимо
Из минусов
🔹 нужно определить свой формат записи данных (если не используете готовую спецификацию определённого формата)
🔹 не получится открыть файл и визуально понять что там записано, а для чтения файла потребуется знать его спецификацию.
🔹 не так-то просто создать такой файл без специальной библиотеки
В таком виде удобно записывать большой массив любых однородных данных. Например, мониторинг валютной биржи или кэшированная анимация 3D геометрии.
(Это не означает что нельзя записать данные разного типа, просто это будет не так удобно)
Представьте себе JPG-картинку. По сути это немного мета-информации и большой массив пикселей. Тоже самое со звуком или видео файлом. Поэтому, если вы попробуете открыть картинку в текстовом редакторе вы увидите что-то вроде такого
f15d cd29 a564 4578 ...
09e2 9bc4 a696 1253 ...
84e9 4de1 3b23 c24a ...
2534 5161 28e0 709d ...
...
Это и есть записанные байтики. И для их чтения требуется определённый софт который знает что с ними делать. Под каждый тип файла.К чему это я? Читайте в следующем посте...
#tricks #basic
⭐️ Вышел первый бета-релиз Python 3.10
Это значит что:
- до стабильного релиза осталось примерно пол года
- ждем информацию по ветке 3.11
#offtop
Это значит что:
- до стабильного релиза осталось примерно пол года
- ждем информацию по ветке 3.11
#offtop
Python.org
Python Release Python 3.10.0b1
The official home of the Python Programming Language
Можно ли в Python создавать бинарные файлы? Конечно можно.
Для этого в Python есть следующие инструменты:
▫️ тип данных bytes и bytearray
▫️ открытие файла в режиме wb (write binary) или rb (read binary)
▫️ модуль struct
Про модуль struct поговорим в первую очередь.
Файл в формате JSON или Yaml внутри себя содержит разметку данных. Всегда можно определить где список начался а где закончился. Где записана строка а где словарь. То есть формат записи данных содержит в себе элементы разметки данных.
В binary-файле данные не имеют визуальной разметки. Это просто байты, записанные один за другим. Правила записи и чтения находятся вне файла.
Модуль struct как раз и помогает с организацией данных в таком файле с помощью определения форматов записи для разных частей файла.
Модуль struct преобразует Python-объекты в массив байт, готовый к записи в файл и имеющий определённый вид.
Для этого всегда следует указывать формат преобразования (или, как оно здесь называется - запаковки).
Формат нужен для того, чтобы выделить достаточное количество байт для записи конкретного типа объекта. В последствии с помощью того же формата будет производиться чтение.
При этом следует помнить что мы говорим о типах языка С а не Python.
Именно формат определяет, что записано в конкретном месте файла, число, строка или что-то еще.
Вот какие токены формата у нас есть.
Помимо этого, первым символом можно указать порядок байтов. На разных системах одни и те же типы данных могут записываться по-разному, поэтому желательно указать конкретный способ из доступных. Если этого не сделать, то используется символ '@', то есть нативный для текущей системы.
В строке формата мы пишем в каком порядке и какие типы собираемся преобразовать в байты.
Запакуем в байты простое число, токен "i".
#lib #basic
Для этого в Python есть следующие инструменты:
▫️ тип данных bytes и bytearray
▫️ открытие файла в режиме wb (write binary) или rb (read binary)
▫️ модуль struct
Про модуль struct поговорим в первую очередь.
Файл в формате JSON или Yaml внутри себя содержит разметку данных. Всегда можно определить где список начался а где закончился. Где записана строка а где словарь. То есть формат записи данных содержит в себе элементы разметки данных.
В binary-файле данные не имеют визуальной разметки. Это просто байты, записанные один за другим. Правила записи и чтения находятся вне файла.
Модуль struct как раз и помогает с организацией данных в таком файле с помощью определения форматов записи для разных частей файла.
Модуль struct преобразует Python-объекты в массив байт, готовый к записи в файл и имеющий определённый вид.
Для этого всегда следует указывать формат преобразования (или, как оно здесь называется - запаковки).
Формат нужен для того, чтобы выделить достаточное количество байт для записи конкретного типа объекта. В последствии с помощью того же формата будет производиться чтение.
При этом следует помнить что мы говорим о типах языка С а не Python.
Именно формат определяет, что записано в конкретном месте файла, число, строка или что-то еще.
Вот какие токены формата у нас есть.
Помимо этого, первым символом можно указать порядок байтов. На разных системах одни и те же типы данных могут записываться по-разному, поэтому желательно указать конкретный способ из доступных. Если этого не сделать, то используется символ '@', то есть нативный для текущей системы.
В строке формата мы пишем в каком порядке и какие типы собираемся преобразовать в байты.
Запакуем в байты простое число, токен "i".
>>> import structТеперь несколько
>>> struct.pack('=i', 10)
b'\n\x00\x00\x00'
float, при этом нужно передавать элементы не массивом а последовательностью аргументов.>>> struct.pack('=fff', 1.0, 2.5, 4.1)
b'\x00\x00\x80?\x00\x00 @33\x83@'
Вместо нескольких токенов можно просто указать нужное количество элементов перед одним токеном, результат будет тот же.>>> struct.pack('=3f', 1.0, 2.5, 4.1)
b'\x00\x00\x80?\x00\x00 @33\x83@'
Теперь запакуем разные типы>>> data = struct.pack('=fiQ', 1.0, 4, 100500)
я запаковал типы float, int и unsigned long long (очень большой int, на 8 байт)b'\x00\x00\x80?\x04\x00\x00...'Распаковка происходит аналогично, но нужно указать тот же формат, который использовался при запаковке. Результат возвращается всегда в виде кортежа.
>>> struct.unpack('=fiQ', data)
(1.0, 4, 100500)
Как видите, ничего страшного!#lib #basic
Теперь запакуем строку.
В этом случае следует передавать тип данных bytes.
#libs #basic
В этом случае следует передавать тип данных bytes.
>>> struct.pack('=s', b'a')
b'a'
Для записи слова следует указывать количество символов.>>> struct.pack('=5s', b'hello')
b'hello'
Кстати, запакованный вид соответствует исходному тексту. Всё верно, символ есть в таблице ASCII, то есть его код попадает в диапазон 0-127, он может быть записан одним байтом и имеет визуальное представление. А вот что будет если добавить символ вне ASCII>>> struct.pack(f'=s', b'ё')
SyntaxError: bytes can only contain ASCII literal characters.
Ошибка возникла еще на этапе создания объекта bytes, который не может содержать такой символ. Поэтому надо кодировать эти байты из строки.>>> enc = 'ёжик'.encode('utf-8')
>>> struct.pack(f'={len(enc)}s', enc)
b'\xd1\x91\xd0\xb6\xd0\xb8\xd0\xba'
Заметьте, длина такой строки в байтах отличается от исходной длины, так как символы вне ASCII записываются двумя байтами и более. Поэтому здесь формат создаём на лету, используя получившуюся длину как каунтер токена.#libs #basic
Есть один интересный момент с запаковкой строк.
Если вам требуется записать просто одну строку какой-то рандомной длины, то паковка тут не нужна. Можете писать любые байты в файл как угодно без запаковки.
А смысл паковки в том, что с помощью формата мы гарантируем правильную длину всех частей записываемых данных.
Если данных слишком много, они обрезаются, если мало, то лишнее заполнится нулевым байтом. Ведь мы читаем данные, ориентируясь на заведомо установленные и предсказуемые позиции байтов в файле.
Вот абстрактный пример спецификации файла:
◽️
Если мы хоть на один байт сместимся, то данные распакуются некорректно.
Когда размер данных заранее неизвестен, то, обычно, перед непосредственно данными пишут сколько они занимают места до следующего блока данных, как в моём примере.
В более простых случаях это необязательно.
❗️Еще раз, помимо преобразования разных типов в байты, модуль struct занимается форматированием или разметкой данных в файле. Именно это и означает что разметка находится вне файла с данными. И это критически важный момент при создании бинарных файлов!
#libs
>>> struct.pack(f'=6s', b'python')
b'python'
>>> 'python'.encode()
b'python'
Хммм..., struct создает тип bytest но при этом для строки просит тоже bytes. На выходе получаем опять bytes без изменений. В чем логика? Ведь ничего же не поменялось! Зачем тогда нам вообще нужен struct если encode делает тоже самое?Если вам требуется записать просто одну строку какой-то рандомной длины, то паковка тут не нужна. Можете писать любые байты в файл как угодно без запаковки.
А смысл паковки в том, что с помощью формата мы гарантируем правильную длину всех частей записываемых данных.
Если данных слишком много, они обрезаются, если мало, то лишнее заполнится нулевым байтом. Ведь мы читаем данные, ориентируясь на заведомо установленные и предсказуемые позиции байтов в файле.
>>> struct.pack(f'=3s', b'python')
b'pyt'
>>> struct.pack(f'=10s', b'python')
b'python\x00\x00\x00\x00'
Формат запаковки нужен как раз для того, чтобы фиксировать разметку файла на основе размера используемых типов данных.Вот абстрактный пример спецификации файла:
◽️
128 байт : какой-то заголовок (str)
◽️8 байт : количество элементов (int)
◽️[4 байта] : массив данных (тип float до конца файла по 4 байта)
Теперь мы знаем как записывать и считывать этот файл, у нас есть его спецификация. Просто берëм нужный диапазон байт и распаковываем в нужный тип данных.Если мы хоть на один байт сместимся, то данные распакуются некорректно.
Когда размер данных заранее неизвестен, то, обычно, перед непосредственно данными пишут сколько они занимают места до следующего блока данных, как в моём примере.
В более простых случаях это необязательно.
data1 = [...]
data2 = [...]
struct.pack(f'=Q{len(data1)}i', len(data1), data1)
struct.pack(f'=Q{len(data2)}i', len(data2), data2)
Начиная с начала файлы мы точно знаем что следующие 8 байт (Q = unsigned long long) это число с количеством элементов, записанных сразу после него. И точно знаем где находится следующий блок данных.❗️Еще раз, помимо преобразования разных типов в байты, модуль struct занимается форматированием или разметкой данных в файле. Именно это и означает что разметка находится вне файла с данными. И это критически важный момент при создании бинарных файлов!
#libs
Давайте посмотрим что со скоростью записи в байты.
Написал тестовый скрипт который пишет 10к значений в 3к файлов с помощью JSON и через struct.
Код берём здесь↗️
Вот результаты на моём железе.
Теперь в функцию json.dump() добавим аргумент indent=4, разница станет еще больше.
Запись х35, чтение х3.1, размер файла х3.2.
И чем больше данных, тем больше разница.
4к файлов по 15к значений, indent=4:
Запись х40.4, чтение х3.3, размер файла х3.3
Очевидно, что при записи в JSON много времени уходит на преобразование данных в строку в нужном формате. И обратная операции во время чтения. Удобство имеет свою цену)
В свою очередь байты пишутся как есть без изменений и лишних знаков форматирования. Нужно лишь преобразовать каждый тип данных в массив байт.
Формат находится вне файла, то есть никакой разметки, в отличие от JSON файла. Поэтому файл на много меньше по размеру.
#libs
Написал тестовый скрипт который пишет 10к значений в 3к файлов с помощью JSON и через struct.
Код берём здесь↗️
Вот результаты на моём железе.
JSON:Через байты скорость записи х25.8 быстрей, чтение х1.7. Размер файла в 2 раза меньше.
Array Size: 85176
File Size: 80560
Time: W:41.9381s R:24.909s
BYTES:
Array Size: 40033
File Size: 40000
Time: W:1.6251s R:14.5471s
Теперь в функцию json.dump() добавим аргумент indent=4, разница станет еще больше.
Запись х35, чтение х3.1, размер файла х3.2.
И чем больше данных, тем больше разница.
4к файлов по 15к значений, indent=4:
Запись х40.4, чтение х3.3, размер файла х3.3
Очевидно, что при записи в JSON много времени уходит на преобразование данных в строку в нужном формате. И обратная операции во время чтения. Удобство имеет свою цену)
В свою очередь байты пишутся как есть без изменений и лишних знаков форматирования. Нужно лишь преобразовать каждый тип данных в массив байт.
Формат находится вне файла, то есть никакой разметки, в отличие от JSON файла. Поэтому файл на много меньше по размеру.
#libs
Gist
struct_vs_json.py
GitHub Gist: instantly share code, notes, and snippets.
Исследуем бинарный файл с изображением. Файл примера можно забрать сразу после этого поста.
Для простоты эксперимента, я сделал BMP файл размером 1х1 пиксель, сохранённый без сжатия.
Наша задача — достать RGB информацию этого единственного пикселя.
Файл я сделал в Photoshop и закрасил пиксель цветом [255, 128, 50]. Сохранил с глубиной цвета 24 бит (по 8 бит на канал, то есть 1 байт).
Вооружившись спецификацией формата BMP мы можем рассчитать где что записано в таком файле.
В начале файла записаны заголовки с различной информацией. Для её отображения можете использовать этот код.
Эти данные активно используют программы-просмотрщики. Например, первые два байта это сигнатура файла (вспоминаем посты по этой тему).
Полезное для нас поле - DataOffset, которое говорит где начинаются данные относительно начала файла.
Открываем файл
Проверяем
PS:
Кто в теме, может покопаться в скрипте для парсинга бинарника mb-файла (файл сцены Autodesk Maya)
https://github.com/westernx/mayatools/blob/master/mayatools/binary.py
#libs
Для простоты эксперимента, я сделал BMP файл размером 1х1 пиксель, сохранённый без сжатия.
Наша задача — достать RGB информацию этого единственного пикселя.
Файл я сделал в Photoshop и закрасил пиксель цветом [255, 128, 50]. Сохранил с глубиной цвета 24 бит (по 8 бит на канал, то есть 1 байт).
Вооружившись спецификацией формата BMP мы можем рассчитать где что записано в таком файле.
В начале файла записаны заголовки с различной информацией. Для её отображения можете использовать этот код.
Эти данные активно используют программы-просмотрщики. Например, первые два байта это сигнатура файла (вспоминаем посты по этой тему).
Полезное для нас поле - DataOffset, которое говорит где начинаются данные относительно начала файла.
Offset: 54
То есть, с начала файла надо пропустить 54 байта, после чего пойдут пиксели! Так и сделаем.Открываем файл
file = open('one_pixel.bmp', 'rb')
Пропускаем 54 байтаfile.seek(54)
В разделе Additional Info в спецификации написано, что порядок записи каналов такой: BGR. Поэтому забираем наши данные о каналах в таком же порядкеb, g, r = struct.unpack('BBB', file.read(3))
file.close()
Почему "B"? Потому что в спецификации указано, что на канал использовано по 1 байту.Проверяем
print(f'R:{r} G:{g} B:{b}')
R:255 G:128 B:50
Отлично, мы добыли то что нам требовалось 😊PS:
Кто в теме, может покопаться в скрипте для парсинга бинарника mb-файла (файл сцены Autodesk Maya)
https://github.com/westernx/mayatools/blob/master/mayatools/binary.py
#libs