TGINSIGHT CHAT
Python Заметки
@pythonotes
EducationИнтересные заметки и обучающие материалы по Python Контакт: @paulwinex ⚠️ Рекламу на канале не делаю!⚠️ Хештеги для поиска: #tricks #libs #pep #basic #regex #qt #django #2to3 #source #offtop
Неодамнешни објави
Ознака: #tricks · 178 објави
Објавено 5 јан.
В работе с медиа файлами часто требуется определить не просто расширение, а его, скажем так, "категорию". Тоесть определить это видео, аудио или картинка. Примерно в 10 случаях из 10 в ревью я вижу обычный хардкодинг с большим мапингом и соответствующим поиском по нему. 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/subtype type - общая категория данных (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
Објавено 6 окт.
Быстрый встроенный профайлинг на Linux с помощью time 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
Hashtags
Објавено 15 авг.
Недавно возникла такая задача: требовалось из Python скрипта запустить дочерний процесс, тоже Python скрипт, и получить от него некоторые данные. В моём случае это был некий словарь который мог быть сериализован в JSON формат, но это не так важно. Какие есть варианты это сделать? 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
Hashtags
Објавено 11 ное.
Как добавить директорию в игнор git репозитория. 1. Те, кто работает JetBrains-продуктах уже на автомате добавляют в .gitignore строчку: .idea/. Это самый простой способ. 2. Чтобы не добавлять в каждом проекте можно добавить в глобальный игнор файл. По умолчанию он лежит здесь: ~/.config/git/ignore Либо указать другой путь через конфиг git config --global core.excludesfile ~/.gitignore Кстати, библиотека venv в Python 3.13 по умолчанию в корень вирутального окружения добавляет файл .gitignore с одним символом *, что означает исключение всего в текущей директории. Таким образом папка с venv автоматически исключается из репозитория. Удобно. #tricks
Hashtags
Објавено 23 окт.
Установить свойства виджета в PySide можно не только через соответствующие методы и конструктор класса. Можно их изменять с помощью метода setProperty по имени. btn = QPushButton("Click Me") btn.setProperty("flat", True) Это аналогично вызову btn.setFlat(True) Если указать несуществующее свойство, то оно просто создается btn.setProperty("btnType", "super") Получить его значение можно методом .property(name) btn_type = btn.property("btnType") Когда это может быть полезно? ▫️Можно просто хранить какие то данные в виджете и потом их доставать обратно widget = QWidget() widget.setProperty('my_data', 123) print(widget.property('my_data')) ▫️ Назначая эти свойства разным виджетам можно потом отличить виджеты во время итераци по ним. Например, найти все кнопки со свойством my_data="superbtn". Но ведь вместо кастомного свойства можно использовать objectName, будет тот же результат. Да, но y ObjectName есть ограничение - только строки. ▫️ Если нам потребуется не просто поиск а, например, сортировка по числу, то свойства позволяют нам это сделать. Поддерживается любой тип данных widget.setProperty('my_data', {'Key': 'value'}) widget.setProperty('order', 1) all_widgets.sort(key=w: w.property('order')) Но ведь Python позволяет всё вышеперечисленное сделать простым созданием атрибута у объекта widget.order = 1 widget.my_data = 123 Да, но я думаю что не надо объяснять почему не стоит так делать. К тому же, если у виджета нет свойства то метод .property(name) вернет None, а отсутствующий атрибут выбросит исключение. ▫️ Действительно полезное применение кастомным свойствам - контроль стилей. Здесь атрибутами не обойтись, нужны именно свойства. Дело в том, что в селекторах стилей можно указывать конкретные свойства виджетов на которые следует назначать стиль. Просто запустите этот код from PySide2.QtWidgets import * if __name__ == "__main__": app = QApplication([]) widget = QWidget(minimumWidth=300) layout = QVBoxLayout(widget) btn1 = QPushButton("Action 1") btn2 = QPushButton("Action 2") btn3 = QPushButton("Action 3", flat=True) layout.addWidget(btn1) layout.addWidget(btn2) layout.addWidget(btn3) # добавим кастомное свойство одной кнопке btn1.setProperty("btnType", "super") # добавляем стили widget.setStyleSheet( """ QPushButton[btnType="super"] { background-color: yellow; color: red; } QPushButton[flat="true"] { color: yellow; } """ ) widget.show() app.exec_() С помощью селектора мы избирательно назначили стили на конкретные кнопки. Как получить список всех кастомный свойств? Функция получения списка кастомных свойств отличается от получения дефолтных. def print_widget_dyn_properties(widget): for prop_name in widget.dynamicPropertyNames(): property_name = prop_name.data().decode() property_value = widget.property(property_name) print(f"{property_name}: {property_value}") #tricks#qt
Објавено 21 окт.
Регулярно приходится писать и ревьюить код, где используется PySide2-6. Заметил, что в подавляющем большинстве случаев настройка создаваемых базовых виджетов происходит через методы. Думаю, всем знаком такой способ. Простой пример с кнопкой: button = QPushButton("Click Me") button.setMinimumWidth(300) button.setFlat(True) button.setStyleSheet("font-size: 20pt") button.setToolTip("Super Button") button.clicked.connect(lambda: print("Button clicked")) Но есть и альтернативный способ - настройка через свойства. Это просто ключевые аргументы конструктора класса. Хоть они и не указаны в документации как аргументы, но они есть) Этот код делает тоже самое но с помощью Property button = QPushButton( "Click Me", minimumWidth=300, flat=True, styleSheet="font-size: 20pt", toolTip="Super Button", clicked=lambda: print("Button clicked"), ) Где это может быть полезно ▫️ Это выглядит более аккуратно и коротко, уже повод использовать ▫️ Может использоваться в заполнении лейаута, когда нам не нужно никакое другое взаимодействие с виджетом и поэтому сохранять его в переменную не требуется. Например, лейбл или кнопка. widget = QWidget(minimumWidth=400) layout = QHBoxLayout(widget) layout.addWidget(QLabel("Button >", alignment=Qt.AlignRight)) layout.addWidget(QPushButton("Click Me", clicked=lambda: print("Button clicked"))) widget.show() Либо так widget = QWidget(minimumWidth=400) layout = QHBoxLayout(widget) for wd in ( QLabel("Button >", alignment=Qt.AlignRight), QPushButton("Click Me", clicked=lambda: ...) ): layout.addWidget(wd) widget.show() ▫️ Можно хранить настройки в каком-то конфиге или генерировать на лету, после чего передавать как kwargs. kwargs = {"text": "Hello " * 30, "wordWrap": True} my_label = QLabel(**kwargs) Как получить полный список доступных свойств? Эта функция распечатает в терминал все свойства виджета и их текущие значения def print_widget_properties(widget): meta_object = widget.metaObject() for i in range(meta_object.propertyCount()): property_ = meta_object.property(i) property_name = property_.name() property_value = property_.read(widget) print(f"{property_name}: {property_value}") #tricks#qt
Објавено 7 окт.
Когда требуется быстро расшарить файлы в локальную сеть со своего компа можно использовать дефолтный python-сервер. Все решается одной командой. python3 -m http.server Но это бывает неудобным если нужно скачать папку или залить файлы. В этом случае более удобным будет быстрый FTP сервер. Я себе сделал шорткат для поднятия простого FTP сервера без авторизации на базе библиотеки pyftpdlib. Варианты запуска: # на рандомном порту read only python3 -m pyftpdlib # на указанном порту python3 -m pyftpdlib -p 22222 # с доступом на запись python3 -m pyftpdlib -w # с авторизацией python3 -m pyftpdlib -w --user=name --password=123 # полный список аргументолв python3 -m pyftpdlib -h Мой алиас для расшаривания в текущей директории alias ftp="python3 -m pyftpdlib -w -p 22222" Теперь можно подключть FTP соединение как удалённую директорию стандартными средствами OS. В Windows это Add Network Location, в Linux - зависит от дистрибутива. Ищите в разделе Network вашего файлового браузера. Также можно использовать сторонние клиенты, например FileZilla. А здесь подробней про http.server #libs#tricks
Објавено 5 авг.
При использовании PNG файлов в PySide/PyQt может появляется такой ворнинг libpng warning: iCCP: known incorrect sRGB profile ICC Profile — это файлы, которые содержат информацию о том, как преобразовывать цвета из одного цветового пространства в другое. Они используются для обеспечения точного цветового соответствия между различными устройствами и программами, такими как сканеры, принтеры, мониторы и графические редакторы. sRGB (standard Red Green Blue) — стандартное цветовое пространство, которое используется в цифровой фотографии и веб-дизайне. Указанная выше ошибка говорит о том, что профиль устарел или повреждён. Qt-движок не может его считать. Если у вас множество иконок и иных картинок оформления, то ворнингов будет сыпаться довольно много. Решение — пересохранить файл без профиля, то есть будет использоваться цветовое пространство на усмотрение приложения. from PIL import Image Image.open(input_path).save(output_path, icc_profile=None) from PySide2.QtGui import QImage, QImageWriter image = QImage(input_path) image.setText("icc", "") writer = QImageWriter(output_path) writer.write(image) #tricks
Hashtags
Објавено 29 јул.
Недавно была задача форматировать строки по шаблону. Шаблон обычный для метода format() /path/to/app{version}/bin или /opt/{app_name}/bin:{DEFAULT_PATH}:/usr/bin Проблема состояла в том, что некоторые переменные следует игнорировать, заменять только те, что у меня есть на данный момент (дальше идет ещё один обработчик). Если в метод строки format() не передать все переменные то будет ошибка KeyError "/opt/{app_name}:{DEFAULT_PATH}".format(app_name="my_app") # KeyError: 'DEFAULT_PATH' Какие варианты решения есть? ▫️ переопределить класс srt и метод format ▫️ написать отдельную функцию парсинга строки с использованием regex ▫️ сделать словарь, который возвращает исходную переменную при отсутствии ключа Третий вариант и рассмотрим, он самый краткий, буквально 2 строки включая вызов! class SkipDict(dict): def __missing__(self, key): return f"{{{key}}}" "/opt/{app_name}:{DEFAULT_PATH}".format_map(SkipDict(app_name='my_app')) # "/opt/my_app:{DEFAULT_PATH}" 1. Мы создаем кастомный класс наследуя его от dict и переопределяем __missing__. Этот метод вызывается когда в словаре ключ не найден. По умолчанию он выбрасывает исключение KeyError. Всякий раз когда ключ не найден, мы возвращаем исходный вид этой переменной и ошибки теперь не будет. 2. Это не сработает если переменные указаны в формате ${name}. Это совсем другой синтаксис из bash и подобных сред. 3. Переменные можно передать и просто готовым словарём. Это же обычный конструктор объекта dict "...".format_map(SkipDict(kwargs)) 4. Вместо format() используется format_map(), просто удобней в данном случае. 5. Ну да, не две строки. Просто класс нужно создать и в одну строку. Если кому надо именно 2х-строчное решение - забирайте: SkipDict = type('SkipDict', (dict, ),{'__missing__': lambda self, key: f"{{{key}}}"}) 6. Из минусов: вы не сможете так просто определить все ли вам нужные переменные заполнены, так как пропускаются ВСЕ отсутствующие. #tricks
Hashtags
Објавено 15 јул.
Объекты datetime.timedelta поддерживают операторы деления и умножения from datetime import timedelta td1 = timedelta(hours=1) # увеличим интервал в 2.5 раза print(td1*2.5) # 2:30:00 # разделим интервал на 2 print(td1/2) # 0:30:00 Можно разделить один интервал на другой, включая целочисленное деление. Так мы узнаем сколько раз один период помещается в другой. td2 = timedelta(minutes=25) print(td1/td2) # 2.4 print(td1//td2) # 2 А так же остаток от делния. print(td1%td2) # 0:10:00 Объекты datetime.timedelta поддерживают отрицательные значения. Эти две записи идентичны. datetime.now() - timedelta(hours=1) datetime.now() + timedelta(hours=-1) И, что очевидно, операторы сравнения td1>td2 # True А еще можно почитать про форматирование даты и времени здесь и здесь. #tricks
Hashtags
Објавено 10 јул.
Три способа создать декоратор для метода класса. ▫️Способ 1. Обычная функция. Единственное отличие от простого декоратора функции в том, что нужно учитывать аргумент self. Если же он не нужен то просто пробрасываем его через *args def decorator_func(func): def wrapped(*args, **kwargs): print('decorator_func') return func(*args, **kwargs) return wrapped class MyClass: @decorator_func def method(self): print('call method') MyClass().method() # decorator_func # call method ▫️Способ 2. Методы класса. Но что, если декоратор жестко привязан к классу и используется только в нём. И стоит задача закрепить декоратор именно за этим классом и расположить внутри него. В таком случае можно сделать staticmethod. Это будет выглядеть страшно, но работать будет (тестировано на 3.11) Очевидно, что декоратор должен быть объявлен раньше метода. class MyClass: @staticmethod def decorator(func): def wrapper(*args, **kwargs): print('decorator from staticmethod') return func(*args, **kwargs) return wrapper @decorator.__func__ def method(self): print('method called') MyClass().method() # decorator from staticmethod # method called Тоже самое будет и с classmethod, но еще хуже. class MyClass: @classmethod def decorator(func): def wrapper(self, *args, **kwargs): print('decorator from classmethod') return func(self, *args, **kwargs) return wrapper @decorator.__func__ def method(self): print('method called') MyClass().method() # decorator from classmethod # method called Где-то потерялся аргумент cls. Скорее всего это можно решить но лучше не надо. Оба варианта выглядят страшненько 🫣 ▫️Способ 3. Вложенный класс и staticmethod class MyClass: class deco: @staticmethod def my_decorator(func): def wrapper(*args, **kwargs): print('decorator from subclass') return func(*args, **kwargs) return wrapper @deco.my_decorator def method(self): print('method called') MyClass().method() # decorator from subclass # method called Получаем чтото вроде микса способов 1 и 2: функция вложена в отдельный класс. Лучшей практикой является способ 1 - обычные функции. Всего пару раз за практику я использовал 3й способ, когда декоратор был намертво привязан к классу и нигде больше не мог использоваться (например, отправлял вызов метода на воркера в другой процесс, не спрашивайте почему так, просто так было нужно 🤪) Способ 2 не советую. Это, скорей, разминка для ума чем практический пример. PS - wraps пропустил для краткости - в коментах дополнительная инфа #tricks
Hashtags
Објавено 19 јун.
Нередко требуется удалять дубликаты инстансов класса. Для этого обычно используется либо циклы со сравнением некоторых атрибутов, либо тип данных set(). При добавлении элемента в set происходит сравнение этого объекта по хешу. Если хеш совпадает с хешем уже существующего объекта, то происходит сравнение объектов на равенство. Если объекты равны, то новый объект не добавляется. class A: def __init__(self, pk: int): self.pk = pk def __repr__(self): return f"{self.__class__.__name__}(pk={self.pk})" set([A(pk=1), A(pk=2), A(pk=2)]) >>> {A(pk=1), A(pk=2), A(pk=2)} Далее для краткости метод `__repr__()` я буду пропускать По умолчанию в расчёте хеша, помимо прочего, используется адрес в памяти, который можно получить с помощью функции id(), поэтому все объекты считаются разными. Чтобы изменить способ сравнения объектов нам требуется переопределить метод __eq__() class A: def __init__(self, pk: int): self.pk = pk def __eq__(self, other): return self.pk == other.pk set([A(pk=1), A(pk=2), A(pk=2)]) >>> TypeError: unhashable type: 'A' Теперь в дело вступает логика, описаная в документации. Если вы переопределили __eq__() то следует переопределить и __hash__(). class A: def __init__(self, pk: int): self.pk = pk def __eq__(self, other): return self.pk == other.pk def __hash__(self): return hash(self.pk) set([A(pk=1), A(pk=2), A(pk=2)]) >>> {A(pk=1), A(pk=2)} Отлично, теперь всё работает. Этот же принцип действует и при наследовании. Допустим, вы создали дочерний класс class B(A): pass set([B(pk=1), B(pk=2), B(pk=2)]) >>> {B(pk=1), B(pk=2)} Теперь следует учитывать вот такое поведение hash(A(1)) == hash(B(1)) >>> True set([A(1), B(1)]) >>> {A(pk=1)} Инстансы А и В могут считаться идентичными, если они имеют одинаковые значения атрибутов и хеш, что может привести к неожиданным результатам при использовании множеств. Нужно учесть это в методах: class A: ... def __eq__(self, other): return isinstance(other, self.__class__) and self.pk == other.pk def __hash__(self): return hash((self.pk, self.__class__)) ... Но если вдруг решите как-то изменить способ сравнения в классе В... class B(A): def __eq__(self, other): return abs(self.pk) == abs(other.pk) set([B(pk=1), B(pk=2), B(pk=2)]) >>> TypeError: unhashable type: 'B' Снова получите ошибку. Та же логика - при переопределении метода __eq__() в новом классе метод __hash__() автоматически становится None и его тоже требуется переопределить. #tricks
Hashtags