Python Заметки
2.22K subscribers
62 photos
2 videos
2 files
229 links
Интересные заметки и обучающие материалы по Python

Контакт: @paulwinex

⚠️ Рекламу на канале не делаю!⚠️

Хештеги для поиска:
#tricks
#libs
#pep
#basic
#regex
#qt
#django
#2to3
#source
#offtop
Download Telegram
Windows 11 становится всё менее дружелюбна к юзерам а порой и вовсе не юзабельной:

▫️ постоянные ломающие апдейты которые не дают загрузить систему, откатить ломающие апдейты, и давно уже удаляют файлы пользователей без спроса. Тенденция в целом уже достаточно давно, включая глобальные сбои и другие неприятности.

▫️ навязчивое продвижение AI шпионов агентов повсюду в системе которых никто не просил.

▫️ всё больше ресурсов ВАШЕГО компа работают не для вас, а в угоду Microsoft. Мелкомягкие официально предлагают купить железо помощней (чтобы они и дальше могли половину мощности использовать по своему усмотрению) а оно что-то не покупается. Рядовой юзер не понимает зачем менять комп который и так норм работает. А глядя на текущие цены на память наступает ощущение что с этим миром что-то не так.

▫️ люди булшитят винду и активно продвигают переход на Linux порой называя винду кучей слопа или даже вирусом, похищающим файлы с целью выкупа (они реально после аплоада и удаления с локала отключают доступ к файлам и требуют купить подписку). А сам Microsoft переименован в Microslop. Появляются даже тулзы для очистки системы от этого слопа.

▫️ Microsoft уже не скрывает, что ваши данные уже не ваши, даже зашифрованные, ибо ваши пароли давно уже хранятся где надо и доступны кому надо.

▫️ При всех этих факапах они закрыли поддержку Windows 10 не давая возможности откатиться на что-то более стабильное.

То есть сами Microsoft стали катализатором поиска альтернатив.
Сам я уже на Linux уже более 7 лет как на основной системе, дома винда есть только в виртуалке для тестов клиентского софта. Расскажите, как у вас обстоят дела на винде? Вы пользуетесь системой или боретесь с ней?

#offtop
👍4
А что происходит на противоположном фронте?
Вы, вероятно, слышали, что 2026 год называют годом Linux на десктопе (в каких-то узких кругах - годом гейминга на Linux). Всё потому, что экосистема Linux постепенно становится более дружелюбной для обычных десктоп-юзеров (в том числе привыкших к Windows), и не только!

▫️ всё чаще появляются Linux дистрибутивы визуально похожие на Windows (или даже лучше), и множество видео с советами какой дистрибутив попробовать новичкам.

▫️ обновления ядра и любых пакетов в экосистеме Linux всегда привносят оптимизацию и удобство и поддержку свежего железа (привет винде с её обратной тенденцией). Например грядущая версия 7.0, опять с множеством приятных мелочей.

▫️ после 10 лет с последего релиза версии 5 окружение KDE Plasma получила мажорный апдейт версии 6 и активно развивается (уже доросла до 6.5). GNOME тоже не спит и готовит версию 50.

▫️ в Wine добавили патч позволяющий устанавливать продукты Adobe на Linux. Для кого-то это был последний рубеж?😏

▫️ Proton активно развивается, да так, что через эту прослойку игры работают даже быстрей чем нативно на винде.

▫️ с каждым релизом Wine и Proton поддерживается всё больше игр, что можно отслеживать на ProtonDB, и даже случаются бусты производительности.

▫️ Я сам на днях на виндобук поставил ChacyOS после чего игры, которые тянули гдето в 5-10 FPS, стали играбельными! Подтверждено личным опытом! Кстати, есть несколько дистрибутивов заточенные именно под игры.

▫️ Valve выпускают новую пачку железок которые (предположительно) порвут рынок гейминга (как и в прошлый раз) и (определнно точно) работают на Linux. Именно Valve вливает ресурсы в Linux в целом и в Proton в частности.

▫️ Госсектор разных стран давно уже мигрирует на opensource, так как нет доверия системе которая может одномоментно неконтролируемо массово рухнуть или быть удаленно заблокированной (в том числе по политическим причинам).

И ниже небольшой опрос - какая у вас операционка основная?

Ни к чему не призываю, ничего не советую! Просто подмечаю тенденцию и хочется узнать мнения из первых рук 😉

Знаю, что Linux тоже не идеален, знаю что каждой задаче - свой инструмент. Но это не тема поста, так что можно без холиваров)


#offtop #linux
Вы до сих пор используете в проекте "магические" строки?😖
@dataclass
class Task:
status: str
...

def create_pending_task(data: dict) -> Task:
task = Task(**data)
task.status = "pending" # < магическая строка
return task

Где тут проблема?

🔸 Если "pending" изменится на "wait", вам придется искать это слово по всему проекту
🔸 Напишете panding вместо pending и баг вылезет только в рантайме в непредсказуемом месте
🔸 Вам очень повезет, если в проекте нет такой же строки но с другим смыслом

Как делать правильно?

Используем модуль enum
from enum import StrEnum

class TaskStatus(StrEnum):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"

@dataclass
class Task:
status: TaskStatus
...

def create_pending_task(data: dict) -> Task:
task = Task(**data)
task.status = TaskStatus.PENDING
return task


Почему это лучше:
▫️Теперь это не строка а объект
▫️ IDE сможет подсказать какие статусы существуют, вам не нужно лезть в документацию или базу
▫️ Единый источник истины. Изменяем в одном месте вместо поиска на всему проекту
▫️ Типизация - наше всё, mypy умеет с этим работать
▫️ Читаемость кода повышается. Ведь читаем мы его чаще чем пишем
▫️ Автоматическая валидация допустимых значений в моделях Pydantic

#tricks
👍145👎2
Почему в прошлом посте я использовал StrEnum а не Enum?
Всё просто, дефолтный Enum не поддерживает нативное сравнение с нужным нам типом.
from enum import Enum

class DefaultEnum(Enum):
KEY = "value"

"value" == DefaultEnum.KEY # False
"value" == DefaultEnum.KEY.value # True

Как видите, приходится вызывать .value, что неудобно в некоторых случаях и более многословно. StrEnum это исправляет:
from enum import StrEnum

class StringEnum(StrEnum):
KEY = "value"

"value" == StringEnum.KEY # True

Для примера из прошлого поста это выглядело бы так:
if task.status == TaskStatus.PENDING:
...

Точно так же работает и IntEnum.

StrEnum появился в версии 3.11, для более ранних использовали комбинацию MyEnum(str, Enum), что не тоже самое.
StrEnum правильно создает значения с функцией auto(). Сочетание str+Enum создает числа, но в виде строк. Приходится явно писать строки. Сделал пару примеров для сравнения↗️

Когда не стоит использовать StrEnum:
- когда нужно явное отличие значений энума от строки
- когда в проекте уже используется обычный Enum

#tricks
👍9
💐❤️🌼⭐️
4😁3
Оператор pipe позволяет писать более компактный код, реализуя логику объединения данных (Union).
Важно помнить, что его поведение зависит от контекста.

Побитовые операции (логическое OR)
result = 5 | 3
# 5 (0101) | 3 (0011) = 7 (0111)

Самое главное - не путать с оператором or, это другое!

Объединение множеств
set_a = {1, 2, 3}
set_b = {3, 4, 5}
set_c = set_a | set_b
# {1, 2, 3, 4, 5}
set_c |= {5, 6}
# {1, 2, 3, 4, 5, 6}


Слияние словарей
dict_1 = {"a": 1, "b": 2}
dict_2 = {"b": 3, "c": 4}
merged = dict_1 | dict_2
# {'a': 1, 'b': 3, 'c': 4}
merged |= {"d": 5}
# {'a': 1, 'b': 3, 'c': 4, 'd': 5}


Аннотации типов, заменяет Union
def process_data(value: int | str) -> None:
print(value)


Допустимо использовать в isinstance или issubclass
isinstance(3, int | float)
# True


Паттерн-матчинг
status_code = 404
match status_code:
case 200 | 201 | 204:
print("OK")
case 400 | 404 | 500:
print("ERROR")

Для использования в своих классах требуется переопределить метод __or__


Так же нашел библиотеку pipe которая добавляет еще много возможностей. Рекомендую ознакомиться ;)

#basic
4
Еще одно применение пайпов - в контексте с Enum.
Но для этого нужен специальный Enum основанный на типе Flag.
В связке с auto он генерирует битовые маски, которые впоследствии можно использовать с оператором |

from enum import Flag, auto

class Perm(Flag):
READ = auto() # 1 (0001)
WRITE = auto() # 2 (0010)
EXECUTE = auto() # 4 (0100)
DELETE = auto() # 8 (1000)


Теперь мы можем комбинировать их через пайп
admin_perms = Perm.READ | Perm.WRITE | Perm.EXECUTE
user_perms = Perm.READ | Perm.EXECUTE
print(admin_perms)
# <Perm.READ|WRITE|EXECUTE: 7>


Можно делать проверки через in (возвращает bool)
if Perm.READ in admin_perms:
print("Success!")


Либо через & (возвращает совпадение либо 0)
print(Perm.READ & admin_perms)
# <Perm.READ: 1>
print(Perm.WRITE & user_perms)
# <Perm: 0>


Оператор ~ инвертирует все флаги
print(~admin_perms)
#<Perm.DELETE: 8>


Можно заранее создать комбинацию.
class Perm(Flag):
READ = auto() # 1 (0001)
WRITE = auto() # 2 (0010)
EXECUTE = auto() # 4 (0100)
DELETE = auto() # 8 (1000)
RW = READ | WRITE

mode = Perm.READ
print(mode & Perm.RW)
# <Perm.READ: 1> (True)
print(mode & Perm.EXECUTE)
# <Perm: 0> (False)


Flag более изолирован. Он не равен числу напрямую, что защищает от случайных ошибок в логике.

#tricks
7👍5👎1
Все паблики облетела новость о покупке Astral. Мнения бытуют разные, так что мне сложно даже предполагать к чему это приведёт.
Сегодня всё так быстро меняется и происходит то, во что раньше бы никто не поверил! Вобщем, будем надеятся.

https://openai.com/index/openai-to-acquire-astral/

#offtop
🔥1
Мы используем Makefile думая, что нет альтернатив, что это стандарт и всё такое.
Но make это не запускалка команд, а система сборки. Мы фактически используем его не по назначению.
И на самом деле альтернатива есть! Некоторое время назад я открыл для себя прекрасный инструмент - just. Он решает все проблемы make.

just - это не система сборки как make, это именно исполнитель команд!
Больше никаких Phony Targets и табуляций, привет нормальный синтаксис и передача аргументов!!! 😎

⭐️ Что умеет just:

Автодокументирование команд
Не нужно делать отдельную команду с докой, просто добавь комментарий
# команда сборки
build:
...


$ just --list
Available recipes:
build # команда сборки


Команда с именем default запускается по умолчанию если не указано другое, так что я обычно делаю так:
default:
just --list


Теперь просто выполняем just и получаем доку из текущего файла.

Удобная работа с переменными окружения
# загрузить из .env
set dotenv-load

# глобальная переменная
export PYTHONPATH := "./src"

# переменная для команды
test $TESTUNG="true":
pytest


Передача аргументов
build target:
@echo 'Build {{target}}...'


команда запуска
$ just build dev
# Build dev...


Выбор интерпретатора прямо в команде
Пример с инлайн-скриптом на python:
system:
#!/usr/bin/env python3
import platform
print(platform.system())


Эта же функция позволит выполнить скрипт как одну команду вместо перезапуска шела для каждой строки
foo:
#!/usr/bin/env sh
for file in ls .; do
echo $file
done


Выполнение команды в определенной директории. Можно указать как релятивный путь так и абсолютный
[working-directory: 'backend']
build:
docker compose build


Также можно задать рабочую директорию глобально

Там еще много интересного:
- поддержка функций
- автокомплиты и интеграции
- экспрешены
- алиасы команд
- группировка команд
- альтернативы команды под разные ОС
- импорт других just-файлов
- цветной вывод
- ... и другие штуковины!

Так что вперёд - ➡️ читать доку!
Репозиторий: ➡️ https://github.com/casey/just
Статья: ➡️ https://www.chicks.net/reference/file_formats/just/

ЗЫ. Кажется, на Makefile я уже не вернусь)

#tools
🔥95
Если запустить REPL с модулем asyncio, то вы входите в особый асинхронный REPL.

user@host:~$ python -m asyncio
asyncio REPL 3.12.7 ...
Use "await" directly instead of "asyncio.run()".
>>> import asyncio
>>>


В этом режиме
- создаётся и настраивается event loop
- уже импортирован asyncio
- работает await на верхнем уровне

То есть такая команда сработает без ошибок!
await asyncio.sleep(3)


Удобно для тестирования асинхронных функций без создания ивентлупов и остальной обвязки.
Работает в: 3.8+


#tricks #async
🔥14😁2👏1
Сгеодня перестал работать Telegram в РФ. Жаль, что вы уже не прочитаете это сообщение. Но если вдруг прочитаете то знайте - канал переехал в MAX!
Подписывайтесь чтобы быть на связи 😎
😁38👏3👎1🔥1🤔1
Стандартная библиотека asyncio это стандарт (начиная с Py3.4) для работы с асинхронным кодом. Но эта библиотека достаточно низкоуровневая, со своими проблемами, устаревшими подходами.
Чтобы исправить это, были созданы разные обертки и альтернативы с реализацией популярных инструментов и паттернов асинхронного программирования. Это такие библиотеки как:

- trio: улучшает корректность выполнения, не оставляя потерянных корутин при ошибках, то есть предлагает Structured Concurrency из коробки.

- curio: упрощение синтаксиса и читаемости кода, больше похоже на работу с потоками.

- anyio: универсальная обертка над asyncio или trio плюс множество вспомогательных инструментов.

anyio используется в FastAPI как основная библиотека для работы с асинхронным кодом и вызовом синхронного кода из асинхронного.


В общем, рекомендую почитать про возможности anyio, возможно вы более не будете использовать чистый asyncio в своих проектах)

Это совсем не значит что дефолтный asyncio плох, он тоже даёт достаточный для работы функционал и продолжает развиваться. Например, в версии 3.11 появились TaskGroup, с похожим на trio функционалом. Так что он тоже актуален, просто придется больше написать кода самостоятельно.

#libs #async
5👍2
Недавно делал быстрый прототип асинхронного приложения в котором требовалось вызывать много синхронного кода. Да, я знаю, что это не лучший дизайн, но нужно было быстрое решение на один процесс и без очередей. Поэтому я выполнял код в потоках.
Выглядело это примерно так:

from fastapi.concurrency import run_in_threadpool

async def execute(data: DataRequest) -> DataResponse:
    try:
        result = await run_in_threadpool(sync_function, data)
        return DataResponse(data=result)
    except Exception as e:
        return DataResponse(
            error=str(e),
            success=False,
        )


В общем работает нормально. Для всех вызовов под капотом используется общий тредпул, всё работает предсказуемо.
Но потребовалось изменить количество запускаемых в пуле потоков (по умолчанию создается 40 воркеров).
Так как дело происходит с FastAPI, делается это через lifespan используя настройки anyio:

import anyio

@asynccontextmanager
async def lifespan(app: FastAPI):
    limiter = anyio.to_thread.current_default_thread_limiter()
    limiter.total_tokens = 100
    yield
    # если вдруг нужно вернуть обратно
    limiter.total_tokens = 40


Зачем менять количество воркеров?
- уменьшить, если оперативки мало (один тред занимает ~8мб)
- увеличить чтобы выдержать нагрузку

Если есть предложения получше при тех же вводных - предлагайте😉

#async
5👍3🔥1
Nuitka 4.0

Библиотека для компиляции python-кода в исполняемые файлы получила мажорный апдейт.

Ключевые изменения:
- ускорение сборки бинарника в 15 раз
- экспериментальная поддержка компилятора Zig
- возможность выбранные функции оставлять как есть, в виде байт кода через декоратор @nuitka_ignore
- бинарники работают до 30% быстрей (CPU-bound задачи)
- теперь можно вместо отдельного скрипта сборки использовать pyproject.toml. Весь конфиг в одном файле!
- улучшен контроль подключения DLL библиотек для Windows
- улучшена совместимость с рядом популярных и не очень пакетами

И в целом выбран путь на повышение производительности бинарной сборки.

Полная информация ➡️ здесь.

#libs
9👍2👏1😱1
В асинхронных приложениях есть один не всегда очевидный момент, который приводит к неявным багам - это общие глобальные объекты. Да, это в целом антипаттерн, но иногда такие объекты действительно нужны и вполне уместны. Например, когда вы завязаны на уже существующем коде и не можете его поменять, но переменную подставлять надо, при этом явно пробросить её не получится. Для этого используется ContexctVar.
Случаи бывают разные но я приведу самый понятный пример - логирование.
Допустим, мы не можем передать id юзера куда-то внутрь фреймворка, но можем использовать его в своем хендлере подставляя как переменную. В примере будет без хендлера но суть та же.
import asyncio
from contextvars import ContextVar
import random

# это переменная, которая будет разной для каждой корутины
user_id_ctx = ContextVar("user_id", default=-1)

async def handle_request(user_name, user_id):
# устанавливаем значение для текущей корутины и её дочерних корутин
user_id_ctx.set(user_id)
print(f"Create {user_name} == {user_id}")
await asyncio.sleep(random.random())
# id не передаётся в вызов
await process_order(user_name)

async def process_order(user_name):
# получаем значение из локального контекста
current_user_id = user_id_ctx.get()
print(f"Log {user_name}: {current_user_id}")

async def main():
await asyncio.gather(
*[handle_request(f"user {i}", i) for i in range(10)],
)

if __name__ == "__main__":
asyncio.run(main())

Во всех выводах id должен совпадать.

- Точно так же можно подставлять атрибуты у инстансов синглтонов.
- Контекст наследуется дочерними корутинами.
- Не стоит увлекаться этим способом, он прилично усложняет логику. Явное лучше неявного.

@asyncio @tricks
👍52
Теперь аналогичная история с тредами. Для тредов используется объект threading.local.
Он позволяет создать локальный динамический атрибут (да, вот так костыльно) для треда.

Вот базовый пример:

import threading
import time
import random

# глобальная переменная
thread_data = threading.local()

def execute():
# поулчаем локальное значение для текущего треда
current_user_id = getattr(thread_data, "user_id", -1)
print(f"Log {threading.current_thread().name}: {current_user_id}")

def thread_task(user_id):
# устанавливаем значение для текущего треда
time.sleep(random.random())
thread_data.user_id = user_id
print(f"Create {threading.current_thread().name} == {user_id}")
execute()

threads = [
threading.Thread(
target=thread_task,
args=(i,),
name=f"Thread-{i}")
for i in range(10)
]
for t in threads:
t.start()
for t in threads:
t.join()

Вывод должен быть аналогичным, с соотетстивем номера треда и id юзера.

Есть еще один пример здесь


#tricks
2👍1
Мы рассмотрели два способа управления конеткстом переменных. Если вам показалось, что это выглядит излишне и можно было бы оставить один, то вам не показалось.
Способ с threading.local придуман для разделения переменных между потоками. CоntextVar был добавлен как новый метод для асинхронного кода, но оказался настолько универсальным, что его можно использовать и с потоками.
После появления ContextVar в PEP567 его рекомендовано использовать вместо threading.local.
И даже был сделан бекпорт для версия ниже 3.7.1.

Теперь, если совместить ContextVar и Proxy-класс из прошлого примера то получим такой класс↗️.

Но у этого класса есть две проблемы:

1️⃣ Нигде не вызывается reset для сброса переменной, что может приводить проблемам

- утечка памяти
- "грязный" конеткст при переиспользовании потоков
- невозможность вернуться к дефолту

Решим это с помощью конектстного менеджера:
@contextlib.contextmanager
def configure_context(self, *args, **kwargs):
"""Синхронный контекстный менеджер (для `with`)"""
tok_cfg = self._cv_config.set((args, kwargs))
tok_obj = self._cv_object.set(None)
try:
yield self
finally:
self._cv_object.reset(tok_obj)
self._cv_config.reset(tok_cfg)

@contextlib.asynccontextmanager
async def aconfigure_context(self, *args, **kwargs):
"""Асинхронный контекстный менеджер (для `async with`)"""
tok_cfg = self._cv_config.set((args, kwargs))
tok_obj = self._cv_object.set(None)
try:
yield self
finally:
self._cv_object.reset(tok_obj)
self._cv_config.reset(tok_cfg)


Пример использования:
with proxy.configure_context(val1, val2):
proxy.do_something()


Теперь прокси готов, но...

2️⃣ В асинхронном коде, для которого и придуманы ContextVar, созданием корутин занимается Event Loop, именно он отвечает за наследование контекста дочерними корутинами. В случае с потоками ничего такого нет, мы сами себе "эвентлуп", поэтому приходится прописывать копирование конеткста самстоятельно.

Пример проблемы с отсутствием наследованием конеткста в потоках↗️

Для решения есть функция копирования текущего контекста и метод запуска функции с новым конектстом:
сontextvars.copy_context().run(func, *args, **kwargs)


Здесь сложно придумать универсальное автоматическое копирование контекста, самая простая функция будет выглядеть так:
def run_in_thread_with_context(
func: Callable, *args, **kwargs
) -> threading.Thread:
ctx = contextvars.copy_context()
t = threading.Thread(
target=lambda: ctx.run(func, *args, **kwargs)
)
t.start()
return t


И если вернуться к нашему синхронному ApiClient, то придётся следить за конектстом самостоятельно. И если где-то в коде библиотеки уже есть вызов тредов, то это работать не будет, придется переписывать.

threading.local тоже не наследует конеткст.


Полный пример Proxy с CоntextVar↗️

Пример использования:
client = ContextVarProxy(ApiClient)

def worker_in_thread(token):
with client.configure_context(token=token):
use_client(...)


Еще вариант, это кастомные ThreadExecutor и Thread с поддержкой автокопирования контекста. Забираем здесь↗️

И нет, это не пример как надо делать в проде) Это просто эксперемент для понимания процесса.

#tricks
3👍1
Как-то давно писал трансфер файлов по сети.
В этом проекте требовалось создавать файл, который сразу существует на диске, имеет нужный размер но еще не содержит данных.
Вот примеры как создать такой файл:

length = 1024 * 1024 * 1024 * 100
with open(file_path, "wb") as out:
out.seek(length-1)
out.write(b"\0")

with open(file_path, "wb") as out:
out.truncate(1024 * 1024 * 1024 * 120)

truncate -s 100M test


Файл создается моментально и получается полностью состоящий из нулей. Более того, он не занимает место над диске!
Это называется sparse files - разреженные файлы. На таких файловых системах как ext4, XFS, Btrfs, ZFS файл автоматически становится разреженным если процесс пишет за пределы конца файла. В структуре файла создаются "дырки" которые автоматически при чтении вернут нули.

Если запустить тоже самое на Windows, то результат будет другой. Файл будет создаваться долго и реально займет место на диске.

NTFS умеет создавать разреженные файлы, но это надо активировать явно:

import os
import msvcrt
import ctypes

file_path = r"C:\file"
length = 1024 * 1024 * 1024 * 100 # 100 GB

with open(file_path, "wb") as f:
handle = msvcrt.get_osfhandle(f.fileno())
FSCTL_SET_SPARSE = 0x900C4
bytes_returned = ctypes.c_ulong()
ctypes.windll.kernel32.DeviceIoControl(
handle, FSCTL_SET_SPARSE, None, 0, None, 0,
ctypes.byref(bytes_returned), None
)
f.seek(length-1)
f.write(b"\0")


Таким образом мы делаем преалокацию файла с возможностью писать в любое место, например так работают торренты.
В моем случае было многопоточное скачивание разных кусков файлов с возможностью докачки.

При копировании таких файлов чаще всего копия занимает всё положенное ей место.
Чтобы учитывать такое свойство файла нужно использовать специальные опции

shutil.copyfile(src, dst, follow_symlinks=False)

rsync -S ...

robocopy /SPARSE ...


Для тестирования трансфера требовалось создавать реальные файлы с рандомными данными. Сделать это просто:

import os
with open(file_path, "wb") as out:
for _ in range(1024):
out.write(os.urandom(1024*1024*10))


dd if=/dev/urandom of=file.bin bs=1M count=10


Тут, конечно, никаких разреженных файлов быть не может.

#tricks
🔥83👍2
В Python 3.6 был полностью переработан стандартный dict. Вместо разреженной таблицы данные стали храниться в плотных массивах. Это дало буст к скорости и экономию памяти. И, как сайд эффект, ключи стали упорядочены. В каком порядке добавляешь ключи, в таком можно и забрать.

Но при этом OrderedDict никуда не делся. Это сделано для совместимости?

Нет, между dict и OrderedDict всё ещё большая разница.

▫️ При сравнении в обычном dict проверяется только наличие ключа, а в OrderedDict проверяется их порядок
▫️OrderedDict основан на связном списке и имеет метод move_to_end() для изменения порядка элементов.
А метод popitem() позволяет удалять элемент как с конца так и из начала.
▫️OrderedDict это кастомный класс. Он не так оптимизирован как обычный dict. Работает дольше, места занимает больше.

В версии 3.7 он был переписан на С и стал быстрей, но всё еще уступает обычному dict.

Немного тестов:

Память: в 2.5-3 раза больше
Создание: в ~2 раза дольше
Удаление: в ~3 раза быстрей (popitem против del)
Поиск по ключу: примерно одинаково (хеш таблицы)

Код тестов↗️

Если вы используете OrderedDict, то это предполагает, что порядок ключей важен для логики программы.


#libs
5👍5