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
У тех, кто часто работает в терминале, есть привычка вызова особо часто используемых команд. например cd, ls, mc...
Вместо команды exit можно использовать Ctrl+D, и это удобно.

И так уж вышло, что эта привычка невольно у меня включается и при работе в REPL. Для быстрого выхода я жму Ctrl+D, и это работает, но только в Linux. В Windows это совсем не работает, так как там надо нажимать Ctrl+Z. И был бы Windows не такой mustdie если бы этого хватило, но требуется еще нажать Enter (если знаете быстрый выход из REPL на винде, то подсказывайте, я не WinUser). Иногда мне быстрей и привычней вбить exit и нажать Enter, как в bash, но и тут подстава - еще нужны скобки вызова🤬.

В общем, настолько высосанную из пальца проблему надо еще поискать 😆, но я нашел для неё решение!

q = type('q', (object,), {'__repr__': lambda *args: exit()})()

Этот код вставляется в стартап скрипт REPL и создаёт новый объект q. Теперь для выхода из REPL достаточно написать символ q и нажать Enter. Работает одинаково на Linux и Windows.

Как это работает?
▫️ динамически создаётся новый тип объекта с помощью конструкции type(NAME, (BASETYPES,), {ATTRS,})
▫️ в атрибутах создаётся оверрайд метода __repr__, который отвечает за распечатку объекта в REPL
▫️ в этом методе вызывается команда exit()

То есть команда выхода срабатывает как только вы пытаетесь распечатать этот объект в консоли. Именно отображение его репрезентации как объекта а не через не print(), который использует метод __str__.

Аналогично работающий код выглядит так:

class Q:
def __repr__(self):
exit()
q = Q()

Из минусов можно выделить следующее:
▫️ имя q занято, но никто не мешает сделать что-то более уникальное
▫️ Нужно как-то добавлять это в стартап. Тут нам поможет startup script

Аналогичным способом можно сделать и другие действия, но стоит помнить что это нестандартное поведение в Python, в прод не оставляйте!
Пару раз я вставлял аналогичные объекты в интерактивную консоль для дебага, они там выполняли роль шорткатов для каких-то наборов действий

PS. Не очень-то это похоже на трик или лайфхак. Это скорей демонстрация гибкости языка в решении надуманных нестандартных проблем.

#tricks
😁7👍3👎1
SQLAlchemy - это один из самых популярных ORM для работы с базами данных из Python.

- поддерживат все популярные базы данных
- не привязана к какому-либо фреймворку (как, например, Django ORM)
- поддерживает асинхрон
- позволяет удобно (питонично) делать довольно сложные SQL запросы

15 июля вышла первая версия из ветки 2.0 и это хорошйи повод изучить эту библиотеку если еще не начали.

Подобрал вам ресурсы для изучения:

- Вебинар и урок про новую SQLAlchemy2.0 от Mike Bayer (автор sqlalchemy и alembic ) с онлайн конференции pythonwebconf:

https://www.youtube.com/watch?v=Uym2DHnUEno

- Для тех кто на английском не очень, есть онлайн книга на руссом от https://t.iss.one/massonnn_yt.
А так же видео версия:

https://www.youtube.com/watch?v=leeC0fpAY-E&list=PLN0sMOjX-lm5Pz5EeX1rb3yilzMNT6qLM

Лично я использую алхимию в связке с FastAPI и пока всё устраивает

#tricks #libs
👍22
Как проверить является ли директория пустой?

Самый простой способ:

if os.listdir(path):
...

Тоже самое с pathlib

p = Path(path)
if list(p.iterdir()):
...

В первом случае функция os.listdir возвращает полный список файлов. Нам остаётся проверить есть ли там что-либо.
Во втором случае мы получаем генератор, который под капотом использует тот же listdir.

Теперь представим что в директории 10к файлов

for i in range(10000):
Path(f'/tmp/test/test{i}.txt').touch()

Не сказать, что при наличии SSD это проблема, но когда таких операций много, мы начинаем терять время, особенно с pathlib.

import timeit
test_path = '/tmp/test'
count = 1000

>>> timeit.timeit('list(os.listdir(p))', setup=f'import os;p="{test_path}"', number=count)
2.281363710993901
>>> timeit.timeit('list(p.iterdir())', setup=f'from pathlib import Path;p=Path("{test_path}")', number=count)
5.6957218300012755

То есть мы получаем список всех 10к файлов просто чтобы узнать что там есть файлы. Хотя нам надо узнать есть ли по указанному пути хотя бы один файл.
Для того чтобы ускорить проверку лучше воспользоваться функцией os.scandir(). Она работает на много быстрей и возвращает итератор с объектами os.DirEntry.
Чтобы узнать есть ли в директории хоть один файл достаточно использовать функцию next()

next(os.scandir(path))

Но если директория пустая, то мы получим ошибку. Поэтому добавляем значение по умолчанию и можно использовать конструкцию в условном операторе

if next(os.scandir(path), None):
...

Либо используем функцию any(), так как она завершится сразу после нахождения первого файла или если итератор пуст.

if any(os.scandir(path)):
...

Сравним скорость
>>> timeit.timeit('next(os.scandir(p), None)', setup=f'import os;p="{test_path}"', number=count)
0.2183076049986994
>>> timeit.timeit('any(os.scandir(p))', setup=f'import os;p="{test_path}"', number=count)
0.21016486900043674

#tricks
👍164
Функция dir() - удобна для получения списка атрибутов у любого объекта.

Ранее я писал про функцию __dir__() в модуле (не путайте её с переменной __all__(), которая указывает список объектов для импорта если встречается конструкция from module import *).

Скорее всего вы уже знаете как использовать функцию dir(). Любой объект может реализовать метод __dir__() чтобы указать список имеющийхся и динамических атрибутов. И функция dir() поможет получить список этих атрибутов.

>>> dir(str)
['__add__', '__class__', '__contains__', ...]

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

>>> dir()
['__builtins__', '__doc__', '__file__', ...]

>>> def test():
>>> x = 1
>>> print(dir())
>>> test()
['x']

#basic #tricks
👍7
Что позволяет делать f-strings в 3.12.

▫️можно использовать одинаковые кавычки во всём выражении
▫️можно добавлять переносы для многострочного выражения
▫️можно использовать символ новой строки (эта проблема неактуальна)
>>> print(f"{"\n".join(
>>> ["1","2","3",
>>> f"{
>>> f"{2+2}"
>>> *(2+2)
>>> }"
>>> ]
>>> )}")
1
2
3
4444

#tricks #libs
👍12
Варианты распаковки контейнеров по отдельным переменным

Обычная распаковка по точному количеству
data = [1, 2, 3, 4, 5]
v1, v2, v3, v4, v5 = data

Распаковка с неизвестным количество но не меньше чем N
v1, *_ = data
v1, *_, v4, v5 = data

Если точно знаете позицию нужного объекта в списке, включая вложенные списки, то достать его можно двумя способами
Через индекс:
data = [[1]]
v1 = data[0][0]

Через распаковку со скобками:
data = [[1]]
(v1, ), = data

data = [[[1]]]
((v1,), ), = data

Еще примеры распаковки вложенных объектов
data = [[1, 2], [3, 4], [5, 6]]
(v1, v2), (v3, v4), (v5, v6) = data
(v1, v2), *_, (v5, *_) = data

#tricks
🔥16👍6
Когда пишешь асинхронный код нужно учитывать особенности такого подхода. Всегда требуется держать в уме, когда возвращается корутина а когда реальный результат. Между этими двумя сущностями должен быть вызов через await.
Вот пример синхронного запроса в базу данных с помощь sqlalchemy. Query пишу инлайном для компактности.
entities = session.execute(select(EntityModel)).scalars().all()

Всё ясно и линейно. А вот он же асинхронный.
result = await session.execute(select(EntityModel))
entities = result.scalars().all()

Это значит что session.execute возвращает корутину, или awaitable объект. Сначала его нужно выполнить через await, тогда получишь объект с которым можно дальше работать.
Не хочу сказать что это мастхэв практика, но простые асинхронные запросы тоже можно сократить до одной строки. Просто использовать скобки.
entities = ( await session.execute(select(EntityModel)) ).scalars().all()

На самом деле я использую такую конструкцию только в прототипах тестов или вспомогательных функциях тестов. В продакшн такое обычно не попадает.

#tricks
👍9
Функция subprocess.check_output() удобна, когда нужно просто получить аутпут процесса.
info = subprocess.check_output(cmd, text=True)

Но вы не сможете таким образом получить аутпут процесса который завершился с ненулевым кодом выхода. Вместо этого у вас выбрасывается исключение
CalledProcessError: Command '[...]' returned non-zero exit status 1.

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

За генерацию исключения отвечает аргумент check, который по умолчанию равен False но именно в check_output он равен True и не может быть переопределён при вызове.

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

Классы TimeoutExpired и CalledProcessError имеют ряд атрибутов, которые хранят всю нужну инфу. Например, вызванная команда (cmd), код выхода (returncode) и то что мы ищем - аутпут процесса (output)

Итого, базовая фукнция для захвата аутпута для любого кода выхода будет выглядеть как-то так:
def get_proc_output(cmd):
try:
return subprocess.check_output(cmd, text=True)
except subprocess.CalledProcessError as e:
return e.output

#tricks
👍13😁3🔥1
Нередко требуется удалять дубликаты инстансов класса. Для этого обычно используется либо циклы со сравнением некоторых атрибутов, либо тип данных 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
👍7🔥1
Три способа создать декоратор для метода класса.

▫️Способ 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
👍7
Объекты 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
🔥15👍3
Недавно была задача форматировать строки по шаблону. Шаблон обычный для метода 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
👍71
При использовании 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
👍15
Когда требуется быстро расшарить файлы в локальную сеть со своего компа можно использовать дефолтный 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
🔥12👏2👍1
Регулярно приходится писать и ревьюить код, где используется 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.iss.onetaObject()
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
👍16🔥7
Установить свойства виджета в 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
👍1
Как добавить директорию в игнор git репозитория.

1. Те, кто работает JetBrains-продуктах уже на автомате добавляют в .gitignore строчку: .idea/. Это самый простой способ.

2. Чтобы не добавлять в каждом проекте можно добавить в глобальный игнор файл. По умолчанию он лежит здесь:

~/.config/git/ignore


Либо указать другой путь через конфиг

git config --global core.excludesfile ~/.gitignore


Кстати, библиотека venv в Python 3.13 по умолчанию в корень вирутального окружения добавляет файл .gitignore с одним символом *, что означает исключение всего в текущей директории. Таким образом папка с venv автоматически исключается из репозитория. Удобно.

#tricks
👍8
Недавно возникла такая задача: требовалось из 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
🔥11👍42
Быстрый встроенный профайлинг на 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
🔥72👍2