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
Что позволяет делать 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
🔥14👍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
👍61
При использовании 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
👍14
Когда требуется быстро расшарить файлы в локальную сеть со своего компа можно использовать дефолтный 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
🔥11👏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
👍15🔥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