Top 100 Questions
Топ-100 найпопулярніших питань⚑
1. Що таке GIL? Які проблеми в нього є?⚑
🔥 96/100 · python/gil_threads_processes.md
Summary
GIL - Global Interpreter Lock - Глобальний блок інтерпретатора - це механізм, який регулює виконання потоків у межах інтерпретатора CPython, запобігаючи одночасному виконанню кількох потоків Python-коду. Він забезпечує безпеку доступу до пам’яті під час виконання інтерпретатора, але водночас створює значні обмеження для паралельного виконання обчислювальних задач. GIL — це не просто обмеження. Він є компромісом між продуктивністю, безпекою пам’яті та простотою реалізації інтерпретатора.
В будь-який момент виконується лише один потік Python. GIL гарантує, що кожен потік має ексклюзивний доступ до змінних інтерпретатора (і відповідні виклики C-розширень працюють правильно). Хоча GIL значно спрощує розробку інтерпретатора та гарантує потокобезпечність багатьох базових операцій, він стає серйозним обмеженням для багатоядерних процесорів, оскільки лише один потік може виконувати Python-код у певний момент часу.
GIL — це блокування, яке має бути захоплено перед будь-яким доступом до Python (не так важливо, чи це виконується Python-код або виклики Python C API).
GIL необхідний для забезпечення безпеки пам’яті в Python та потокобезпечності загалом. Без GIL довелося б додатково синхронізувати доступ до кожного об’єкта у пам’яті, що суттєво ускладнило б код і зменшило б продуктивність однопоточних програм. Тому GIL також покращує продуктивність у сценаріях, де код виконується в одному потоці. Для однопоточних задач відсутність накладних витрат на синхронізацію робить виконання швидшим. Це особливо корисно для багатьох класичних сценаріїв використання Python, таких як обробка скриптів, автоматизація або створення вебсервісів.
Також GIL робить розробку розширень на C набагато простішою. Багато бібліотек на C розраховують на те, що GIL захищає доступ до об’єктів Python. Це дозволяє бібліотекам уникати ручної синхронізації та працювати з об’єктами Python у безпечному середовищі.
Принцип роботи простий. Потоки утримують GIL, поки вони виконуються. Однак, вони звільняють його при блокуванні для операцій введення-виведення. Кожного разу, коли поток змушений чекати, інші готові до виконання потоки отримують шанс запуститися.
PyObject*
_PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag)
{
// local variable declarations, etc.
// evaluation loop
for (;;) {
// eval_breaker signals whether bytecode execution must pause, e.g. another thread requested the GIL
if (_Py_atomic_load_relaxed(eval_breaker)) {
// eval_frame_handle_pending() pauses bytecode execution, releases the GIL, then waits for it again
if (eval_frame_handle_pending(tstate) != 0) {
goto error;
}
}
NEXTOPARG(); // fetch the next bytecode instruction
switch (opcode) {
case TARGET(NOP) {
FAST_DISPATCH(); // next iteration
}
case TARGET(LOAD_FAST) {
FAST_DISPATCH(); // next iteration
}
// 117 more case blocks covering every possible opcode
}
// error handling
}
// cleanup
}
Функція __PyEval_EvalFrameDefault_ відповідає за обробку байт-коду і виконання його інструкцій.
Коли потік починає роботу, він захоплює GIL. Через певний час планувальник процесів вирішує, що поточний потік виконав достатньо роботи і передає управління наступному потоку. Потік №2 бачить, що GIL захоплений, тому він не продовжує роботу, а сам себе переводить у сплячий режим, уступаючи процесор потоку №1.
Але потік не може утримувати GIL нескінченно. До версії Python 3.2 GIL перемикався кожні 100 тіків (байткод-інструкцій Python, sys.setcheckinterval). У пізніших версіях GIL може бути утримано потоком не більше 5 мс. Значення цього інтервалу можна змінити за допомогою функції sys.setswitchinterval(). GIL також звільняється, якщо потік здійснює системний виклик, працює з диском або мережею.
import threading
import time
def run_io_task(name: str):
print(f'{name} started I/O operation')
time.sleep(2)
print(f'{name} finished I/O operation')
threads = [threading.Thread(target=run_io_task, args=(f'Thread-{i}',)) for i in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
Кожен потік викликає time.sleep(2) інструкцію, яка звільняє GIL. Поки один потік чекає завершення sleep, інші потоки можуть виконуватися. У результаті всі потоки працюють ефективно, і програма виконується швидше (приблизно 2 секунди), ніж при послідовному виконанні (приблизно 8 секунд).
На глибокому рівні механізм роботи GIL реалізовано через примітиви синхронізації операційної системи. У CPython це зазвичай м’ютекси для блокування та семафори для сигналізації між потоками. У сучасних версіях Python (починаючи з Python 3.2) GIL реалізовано через м’ютекс і умови (pthreads condition variables) для підвищення продуктивності.
Умовні змінні у цьому процесі використовуються для того, щоб потік, який очікує, міг бути розблокований лише за сигналом. Таке розблокування запобігає активному циклу очікування (busy waiting), що могло б надмірно навантажувати процесор. Але перемикання супроводжуються додатковими витратами, оскільки такі дії включають системні виклики та доступ до структур ядра ОС.
Проблема полягає в тому, що через GIL далеко не всі завдання можуть бути вирішені у потоках. Навпаки, їх використання часто знижує продуктивність програми (у випадку CPU-bound завдань). Використання потоків вимагає контролю доступу до загальних ресурсів, таких як словники, файли, підключення до бази даних.
- GIL спрощує інтеграцію незахищених від багатопотоковості (non thread safe) бібліотек на C. Завдяки GIL у нас є так багато швидких модулів та зв'язків майже до всього.
- Бібліотекам на C доступний механізм керування GIL. Наприклад, NumPy звільняє його під час тривалих операцій.
На практиці, GIL у Python робить непотрібною ідею використання потоків для паралелізму в обчислювальних задачах. Вони будуть працювати послідовно навіть на багатопроцесорній системі. У випадку CPU-bound задач програма не прискориться, а тільки сповільниться, оскільки тепер потокам доведеться поділити процесорний час навпіл. При цьому операції I/O GIL не сповільнює, оскільки перед системним викликом потік звільняє GIL.
GIL - це деталь реалізації, а не частина специфікації мови. Він є не тільки в CPython, а й у PyPy; натомість Jython та IronPython GIL не мають (покладаються на потоки хостової платформи - JVM та .NET відповідно).
Способи обходу GIL
Кілька стандартних шляхів отримати справжню паралельність попри GIL у CPython:
- Багатопроцесність.
multiprocessing,concurrent.futures.ProcessPoolExecutor. Кожен процес має власний інтерпретатор і власний GIL, тому Python-код виконується паралельно на різних ядрах. Накладні витрати - порядку мегабайт пам'яті на процес і необхідність IPC замість прямих посилань на об'єкти. - GIL відпускається на IO-блокуючих syscall'ах (
recv,read,accept,time.sleep). Поки потік чекає у ядрі, інші потоки виконують байт-код. Тому потоки залишаються корисними для IO-bound задач: 100 потоків, що чекають на сокетах, не блокують одне одного. - C-розширення, що явно звільняють GIL. Бібліотеки на C/C++/Rust, які виконують довгі обчислення, можуть звільнити GIL через макрос
Py_BEGIN_ALLOW_THREADSна час чистого C-коду. NumPy, scipy, lxml, hashlib (для великих блоків), pandas (частково) роблять це. У результаті CPU-bound операція в NumPy через кілька потоків справді розпаралелюється. - Sub-interpreters (PEP 684, PEP 734). Кілька ізольованих інтерпретаторів у межах одного процесу, кожен з власним GIL. Доступне з Python 3.12 на рівні C API (PEP 684 - per-interpreter GIL); з Python 3.14 додано stdlib-модуль
concurrent.interpreters(PEP 734) з Python-API. Об'єкти не діляться між інтерпретаторами напряму; обмін - через канали / pickle. Накладні витрати менші заmultiprocessing(без окремих процесів), але вища складність моделі ізоляції. - No-GIL CPython (PEP 703). Експериментальна збірка CPython без GIL (
--disable-gil), доступна з 3.13 як опція. Дає справжню паралельність потоків без зміни моделі програмування, ціною накладних витрат на atomic-операції з лічильниками посилань і складнішої сумісності з C-розширеннями. Поки що "free-threaded" режим - офіційно підтримувана експериментальна функція, не дефолт; для продакшну орієнтуватися на ноти релізу конкретної версії.
Links
- GIL у Python. Ключ до стабільності чи ворог продуктивності - dou.ua
- GlobalInterpreterLock
- Как устроен GIL в Python
- Embracing the Global Interpreter Lock (GIL) - David Beazley
2. Наслідування⚑
🔥 94/100 · computer_science/oop.md
Summary
Наслідування - це один з ключових принципів ООП, який дозволяє створювати новий клас на основі вже існуючого класу (батьківського класу). Наслідування дозволяє використовувати властивості та методи батьківського класу в новому класі, що допомагає уникнути повторення коду та забезпечити використання вже існуючого функціоналу. Дочірній клас містить всі атрибути батьківського класу, при цьому деякі з них можуть бути перевизначені в дочірньому. Cтворення підкласу має супроводжуватися ідеєю спеціалізації - відношенням
is-a(є).
У Python, наслідування здійснюється за допомогою вказання батьківського класу у визначенні нового класу в дужках після імені класу:
class ParentClass:
def parent_method(self):
print("This is a method from the parent class")
class ChildClass(ParentClass):
def child_method(self):
print("This is a method from the child class")
У цьому прикладі ChildClass наслідує всі властивості та методи з ParentClass. Це означає, що ChildClass буде мати доступ до методу parent_method, а також може мати свої власні методи, наприклад, child_method.
При наслідуванні, клас, який успадковує властивості, називається підкласом або дочірнім класом, а клас, від якого успадковуються властивості, називається батьківським класом або суперкласом.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
dog = Dog("Buddy")
cat = Cat("Whiskers")
print(dog.name) # Output: Buddy
print(dog.speak()) # Output: Woof!
print(cat.name) # Output: Whiskers
print(cat.speak()) # Output: Meow!
Таким чином, наслідування дозволяє створювати ієрархії класів, де дочірні класи успадковують властивості та методи батьківських класів, а також можуть додавати свої власні методи та властивості.
Ми можемо отримати доступ до методів класу-предку або по прямому зверненню, або з допомогою функції super().
Хоча наслідування є потужною концепцією, воно має свої небезпеки. Основна з них полягає в тому, що кожного разу, коли розширювати базовий клас, створюється новий, який тісно пов’язаний з батьківським. Високе зв'язування (High Coupling) є однією з речей, яких потрібно уникати при розробці програмного забезпечення.
Хоча завжди повинні дбати про повторне використання коду, не варто змушувати дизайн використовувати успадкування для повторного використання коду лише тому, що отримуємо методи від батьківського класу безкоштовно. Правильний спосіб повторного використання коду полягає в тому, щоб мати дуже зчеплені (highly cohesive) об’єкти, які можна легко компонувати та працювати в багатьох контекстах.
3. Генератор vs Ітератор⚑
🔥 85/100 · python/iterator_and_generator.md
Різниця між генераторами та ітераторами в Python полягає в тому, що ітератори використовуються для перебору групи елементів (наприклад, у списку), тоді як генератори є способом реалізації ітераторів, і використовуються для генерації значень. Генератори використовують ключове слово yield для повернення значення з функції, але, за винятком цього, вони поводяться як звичайні функції.
Links
- Генераторы-итераторы Python
- Итерируемый объект, итератор и генератор
- https://pavel-karateev.gitbook.io/intermediate-python/struktury-dannykh/generators
4. Що таке декоратори. Навіщо вони потрібні⚑
🔥 76/100 · python/decorators.md
Summary
Декоратор - структурний шаблон проєктування, який дозволяє модифікувати функцію або клас без зміни їхнього вихідного коду. У Python декоратор - це функція, яка приймає іншу функцію або клас як аргумент і повертає модифіковану версію цієї функції або класу.
З допомогою декоратора ми можемо додати код до або після виконання функції. Синтаксис декораторів базується на використанні символу @, за яким слідує назва декоратора і перед функцією або класом, який потрібно декорувати.
Основна ідея декораторів полягає в тому, що вони додають додаткову функціональність до існуючої функції або класу, не змінюючи їхнього вихідного коду. Це зроблено шляхом обгортання (wrapping) оригінальної функції або класу в нову функцію або клас, яка виконує додаткову логіку.
Основне використання декораторів включає реалізацію логування, мемоізації, перехоплення виключень, аутентифікації та багато іншого. Завдяки декораторам можна використовувати ці функціональні можливості зокрема для багатьох функцій чи класів, спрощуючи процес розробки та підтримки програмного коду.
Декоратор використовує механізм замикання.
Приклад декоратора в Python, який друкує повідомлення перед виконанням функції.
def decorator(func):
def wrapper(*args, **kwargs):
print("Some logic before original func")
return func(*args, **kwargs)
return wrapper
@decorator
def my_function():
print("Original function")
print(my_function.__name__) # wrapper # NOTE: because we didn't use wraps
print(my_function.__closure__[0]) # <cell at 0x10...: function object at 0x108e25170>
print(my_function.__closure__[0].cell_contents.__name__) # my_function
Декоратор як патерн (GoF) проти декоратора Python
Суть патерну Декоратор: маючи об'єкт і потребу виконувати додаткову роботу, його не змінюють, а створюють обгортку, що (1) сумісна з вихідним об'єктом за інтерфейсом і (2) за потреби делегує йому роботу. Такий декоратор добре лягає на DI та фабрики: змінюють код створення об'єкта, а не код, що його використовує. Типовий приклад - кешування запитів до зовнішньої системи.
Декоратор Python через @ виконує схожу роль, але з відмінностями:
- застосовується лише до функції чи класу, а не до довільного об'єкта;
- застосовується в момент оголошення (при ініціалізації коду), а не в довільний момент;
- не вимагає сумісності інтерфейсів (хоча часто це хороша ідея).
Часто декоратори Python використовують інакше, ніж патерн: змінюють API об'єкта (@staticmethod, @property), виконують реєстрацію (@app.route("/")) або взагалі замінюють тіло об'єкта (@dataclass).
Типові помилки:
- Простий перенос виклику з тіла функції в декоратор - декоратору доводиться працювати з функціями різних сигнатур, а виклик логіки втрачає гнучкість вибору моменту.
- Недостатньо універсальні декоратори, що покладаються на конкретну сигнатуру чи очікують параметр із певним ім'ям.
- Глобальні реєстри об'єктів - можуть давати ефекти глобальних змінних (стан, імпорти заради побічного ефекту).
- Примусове застосування опціональної функціональності - наприклад, кеш, прибитий декоратором, заважає налаштувати TTL чи замінити реалізацію та ускладнює тестування. Тут краще DI плюс патерн Декоратор.
Links
- Понимаем декораторы в Python'e, шаг за шагом. Шаг 1
- Понимаем декораторы в Python'e, шаг за шагом. Шаг 2
5. Яка різниця між змінними і незмінними типами даних в Python?⚑
🔥 73/100 · python/data_types.md
В Python є два основні типи даних за їхньою здатністю до зміни: змінні (mutable) та незмінні (immutable).
Незмінні типи - це типи даних, значення яких не можна змінити після створення. Якщо змінювати значення змінної з таким типом, Python створить новий об'єкт у пам'яті, а не змінить існуючий. Зберігаються в пам'яті як окремі об'єкти. Якщо створюється нове значення, Python створює новий об'єкт, і змінна вказує на нього. Для малих та часто використовуваних об'єктів, таких як малі цілі числа чи короткі рядки, Python може використовувати "пул об'єктів", щоб зменшити кількість створюваних об'єктів.
Змінні типи можна змінювати без створення нового об'єкта. Це означає, що після зміни вмісту об'єкта його адреса в пам'яті не змінюється. Python зберігає посилання на об'єкт у пам'яті, і при зміні значення об'єкта його адреса в пам'яті залишається тією самою. Це означає, що якщо змінюється список або словник, зміни відбуваються на місці.
x = 10 # Immutable type
print(id(x)) # Shows memory address - 4392920520
x = 20 # Creates a new object
print(id(x)) # Address will change - 4392920840
y = [1, 2, 3] # Mutable type
print(id(y)) # Memory address - 4418755008
y.append(4) # Modifies the existing object
print(id(y)) # Address remains the same - 4418755008
У Python, всі об'єкти зберігаються в купі (heap). Це справедливо і для змінних, і для незмінних типів даних. Купа забезпечує динамічне виділення пам'яті, що дозволяє створювати нові об'єкти за потреби.
Namespace, або простір імен, створюється у стеку. Коли викликається функцію або імпортується модуль, стек використовується для зберігання контексту виконання, включаючи локальні змінні і простори імен. Об'єкти, на які посилається цей простір імен, залишаються у купі.
Тобто коли імпортується модуль, інтерпретатор Python зберігає змінні та об'єкти модуля в купі, а простір імен модуля використовується для доступу до цих змінних та об'єктів у програмі - купа зберігає дані, а стек - виклики функцій і контексти.
6. Масив⚑
🔥 68/100 · computer_science/data_structures.md
Summary
Масив - структура даних, що зберігає елементи в суміжній ділянці пам'яті й адресує їх за індексом через арифметику зсуву (
base + i * розмір_елемента), звідки доступ за індексом O(1), а вставка/видалення в середині O(n) через зсув решти.
Доступ за індексом сталого часу випливає саме зі суміжного розміщення: адресу елемента обчислюють як base + i * розмір_елемента, без обходу. Пошук довільного значення в невпорядкованому масиві - O(n) (лінійний перебір); якщо масив відсортований, застосовний бінарний пошук за O(log n) - але саме за передумови впорядкованості, не безумовно. Вставка й видалення в довільній позиції - O(n), бо решту елементів доводиться зсувати; у кінці (для динамічного масиву) - амортизовано O(1).
За розміром розрізняють статичні масиви (фіксований розмір, заданий при оголошенні) і динамічні (з резервом ємності: при вичерпанні вміст копіюється в більший буфер). Багатовимірні масиви адресують елемент кількома індексами (двовимірний - як сітка з індексами x та y).
Ефективність операцій (О велике)
- доступ за індексом - O(1)
- пошук (невпорядкований) - O(n)
- пошук (відсортований, бінарний) - O(log n)
- вставка/видалення в середині - O(n)
- вставка/видалення в кінці (динамічний масив) - амортизовано O(1)
7. Як працює хеш-таблиця⚑
🔥 66/100 · python/set_and_dict.md
Хеш-таблиця - це розріджений масив (масив, в якому є незаповнені позиції). У стандартних англомовних підручниках комірки хеш-таблиці називаються "bucket". Хеш-значення - це числове значення, яке обчислюється для кожного ключа з метою швидкого доступу до відповідного значення у словнику. У хеш-таблиці dict кожному елементу відповідає комірка, що містить два поля: посилання на ключ і посилання на значення елемента. Оскільки розмір усіх комірок однаковий, доступ до окремої комірки відбувається за зміщенням.
Python намагається залишити не менше третини комірок порожніми; якщо хеш-таблиця стає надто заповненою, вона копіюється в новий регіон пам'яті, де є місце для більшої кількості комірок.
Для розміщення елемента в хеш-таблицю потрібно спочатку обчислити хеш-значення ключа елемента. Це робить вбудована функція hash().
Для вибірки значення за допомогою виразу my_dict[search_key] Python звертається до функції hash(search_key), щоб отримати хеш-значення search_key, і використовує кілька молодших бітів отриманого числа як зміщення комірки відносно початку хеш-таблиці (скільки саме бітів залежить від поточного розміру таблиці). Якщо знайдена комірка порожня, виникає виключення KeyError. В іншому випадку в знайденій комірці є якийсь елемент - пара ключ:значення - і тоді Python перевіряє, чи справді search_key == found_key. Якщо так, елемент знайдено і повертається found_value. Якщо search_key і found_key не збігаються, відбувається колізія хешування. Для вирішення колізії алгоритм бере різні біти хеш-значення, виконує над ними певні дії і використовує результат як зміщення іншої комірки.
Links
8. Для яких завдань варто використовувати потоки, для яких - процеси, а для яких - асинхронність?⚑
🔥 65/100 · python/gil_threads_processes.md
Summary
- CPU-залежна задача => Multi Processing
- I/O-залежна, швидкий I/O => Multi Threading
- I/O-залежна, повільний I/O => Asyncio (робота з вебсокетами)
Наприклад треба написати HTTP або WebSocket сервер, який обробляє кожне з'єднання в окремому потоці.
Можна створити 100, а можливо, навіть 500 потоків, щоб обробити необхідну кількість одночасних з'єднань. Для коротких запитів це може працювати і дозволити витримувати навантаження 5000 RPS на найбільш доступному DO інстансі за п'ять доларів - це досить непогано. Якщо менше, то можливо вам не потрібні жодні AsyncIO/Tornado/Twisted.
Але що, якщо їх кількість наближається до нескінченності, наприклад, це великий чат з багатьма каналами, кількість одночасних учасників в якому необмежена. У такій ситуації краще не створювати стільки потоків, щоб вистачило кожному користувачеві.
Як уже зазначалося, поки GIL захоплений одним потоком, інші потоки не будуть працювати. Планувальник операційної системи про цей GIL нічого не знає і все одно буде віддавати процесор заблокованим потокам. Цей потік, звичайно, побачить, що GIL захоплений і одразу засне, але на перемикання контексту процесора буде витрачатися дорогоцінний час.
Перемикання контексту - взагалі витратна для процесора операція, яка потребує скидання регістрів, кешу та таблиці відображення сторінок пам'яті. Чим більше потоків запущено, тим більше процесор робить марні перемикання на потоки, що заблоковані GIL, перед тим, як дістатися до того самого, який утримує цей GIL. Не дуже ефективно.
Є також співпрограми - те, що пропонує AsyncIO і Tornado. Їх також називають корутинами або просто потоками на рівні користувача, які використовувалася ще в часи, коли були операційні системи без підтримки багатозадачності.
На відміну від потоків, співпрограми виконують лише корисну роботу, а перемикання між ними відбувається лише у той момент, коли співпрограма очікує завершення якоїсь зовнішньої операції.
Як і у випадку з потоками, асинхронні фреймворки використовуються для I/O-bound задач - таких як мережеві виклики, взаємодія з базами даних, робота з файлами. Для CPU-bound задач, які вимагають багато обчислень, такий підхід може бути неефективним, оскільки співпрограми теж працюють в одному потоці через GIL.
Ще одна найважливіша особливість корутину полягає в тому, що вони легкі. Вони "легші" за потоки. Тобто швидше запускаються і використовують менше пам'яті. По суті, корутини – це особливий різновид функцій, а от потоки представлені Python-об'єктами та пов'язані з потоками в операційній системі, з якими ці об'єкти мають взаємодіяти. Перемикання між потоками відбувається тоді, коли заманеться операційній системі. У випадку з asyncio - перемикання може відбутись на await, що дає більше контролю над програмою.
В Python-програмі можуть міститись тисячі потоків. А якщо говорити про корутини, то їх уже можуть бути десятки або сотні тисячі, і всі вони будуть розміщені в одному потоці.
Якщо задача потребує інтенсивних обчислень (CPU-bound), то потрібно використовувати процеси або оптимізації. Бібліотеки NumPy, Pandas вміють відпускати GIL. Також можна переписати критичні частини коду з допомогою Cyton, Numba, Clang. Оптимальна кількість процесів зазвичай дорівнює або трохи менша кількості ядер процесора. Рекомендується залишати хоча б одне ядро вільним, наприклад, запускати cpu_count() - 1 процесів, або експериментально визначати оптимальну кількість.
Links
- Асинхронний Python: види конкурентності - DevZone.ua
- Швидко та недорого покращуємо перфоманс невеликого Python-проєкту
- Розділяй і володарюй. Як працюють процеси в Python - dou.ua
9. Контекстний менеджер (Context manager)⚑
🔥 64/100 · python/namespace_and_context_manager.md
У Python є оператор with. Код всередині нього виконується з особливістю: перед та після нього гарантовано спрацьовують події входу до блоку with та виходу з нього (навіть у випадку коли з'являється виняток). Об'єкт, який визначає цю логіку, називається контекстним менеджером (context manager).
Події входу і виходу з блоку визначені методами __enter__ і __exit__. Перший спрацьовує в той момент, коли виконання програми переходить всередину with. Метод може повернути значення, яке буде доступне вкладеному коду всередині блоку with.
__exit__ спрацьовує при виході з блоку, включаючи випадок виникнення виключення. У цьому випадку в метод буде передано трійку значень (exc_class, exc_instance, traceback).
Найпоширеніший контекстний менеджер - це клас, створений функцією open. Він гарантує, що файл буде закрито навіть у випадку виникнення помилки всередині блоку.
Слід намагатися виходити з контекстного менеджера якомога швидше, щоб звільнити контекст та ресурси.
Приклад реалізації власного контекстного менеджера на основі класу
class MyOpen:
def __init__(self, filename):
self.filename = filename
def __enter__(self):
self.file = open(self.filename)
return self.file
def __exit__(self, exc_type, exception, traceback):
self.file.close()
>>> with open('test.txt', 'w') as file:
... file.write('Hello World!')
Приклад реалізації власного контекстного менеджера з використанням вбудованої бібліотеки contextlib:
from contextlib import contextmanager
@contextmanager
def printable():
print('enter')
try:
yield
finally:
print('exit')
Контекстні менеджери також можна використовувати для тимчасової заміни параметрів, змінних середовища, транзакцій БД.
Перевага with над try/finally. Поширена помилка - відкривати ресурс усередині try і звільняти його у finally: якщо саме захоплення ресурсу завершується винятком (файл не відкрився, блокування не взялося), finally спробує звільнити те, що не було захоплене. Тому ресурс захоплюють до try, а краще - делегують це менеджеру контексту: with викликає __exit__ лише після успішного __enter__, тож звільнення не доводиться дублювати в кожному finally.
Асинхронний контекстний менеджер. Для ресурсів, вхід/вихід яких є awaitable (мережеве з'єднання, пул БД, сесія aiohttp), використовують async with. Менеджер реалізує __aenter__ / __aexit__ (обидва - корутини) або функцію огортають у @asynccontextmanager з contextlib.
from contextlib import asynccontextmanager
class AsyncResource:
async def __aenter__(self):
await self.connect()
return self
async def __aexit__(self, exc_type, exc, tb):
await self.close()
@asynccontextmanager
async def resource():
conn = await connect()
try:
yield conn
finally:
await conn.close()
async with resource() as conn: # works only inside an async function
...
10. Що таке event loop (цикл подій) в asyncio і як він працює?⚑
🔥 60/100 · python/async.md
Цикл подій є ядром кожної асинхронної програми. Цикли подій запускають асинхронні завдання та зворотні виклики, виконують мережеві операції вводу-виводу та запускають підпроцеси.
Event Loop - це механізм, який дозволяє координувати виконання асинхронних операцій у Python. Він дозволяє програмі взаємодіяти з багатьма завданнями, такими як ввід-вивід, мережеві запити, без блокування основного потоку виконання.
Event Loop - це як певний безкінечний цикл, який дозволяє розпізнати, чи настала певна подія операційної системи (наприклад, запис даних до сокету).
Event Loop працює на принципі опитування: він постійно перевіряє список подій та завдань, які очікують виконання, і обробляє їх послідовно. Коли подія стає доступною для обробки (наприклад, завершення мережевого запиту), Event Loop викликає відповідну функцію-зворотний виклик, яку визначили, для обробки результату.
Під капотом, Event Loop в asyncio працює на подібних принципах, як в традиційних механізмах вводу-виводу як Select, Poll та Epoll. Він встановлює "наглядачі" на різні асинхронні операції та очікує їхньої готовності.
З допомогою Select формується список файлових дескрипторів, за якими планується спостерігати. У клієнтському коді доведеться перевіряти всі передані дескриптори на наявність подій (і їх кількість обмежена 1024), що робить його повільним та незручним.
У випадку Poll та Epoll, Event Loop використовує більш ефективні механізми опитування, дозволяючи ефективно взаємодіяти з більшим числом подій.
uvloop як альтернатива стандартному loop
uvloop - реалізація event loop'а на C поверх libuv (тієї ж бібліотеки, що лежить в основі Node.js). Drop-in заміна стандартного asyncio loop'а:
За бенчмарками авторів uvloop (вимірювання на HTTP- і echo-сервері), пропускна здатність зростає у кілька разів порівняно зі стандартним asyncio loop'ом за рахунок реалізації на C і оптимізованої роботи з epoll/kqueue. Фактичний виграш залежить від характеру навантаження - на CPU-bound коді або частих переходах user/kernel ефект менший. Семантика для прикладного коду не змінюється: ті ж async def, await, Task, gather.
Обмеження: не підтримує Windows нативно (libuv підтримує, але прив'язки uvloop - ні; для Windows у asyncio з 3.8 за замовчуванням використовується ProactorEventLoop поверх IOCP, і uvloop там не потрібен). Тому в більшості продакшн-сценаріїв uvloop ставлять на Linux-серверах, тоді як локальна розробка на macOS/Windows може використовувати стандартний loop.
11. Що таке магічні методи та для чого вони потрібні⚑
🔥 60/100 · python/class_and_object.md
Summary
Магічні методи (дандр методи, Magic methods, dundr methods) - це методи, імена яких починаються і закінчуються подвійним підкресленням. Магічні тому, що майже ніколи не викликаються явно. Їх викликають вбудовані функції або синтаксичні конструкції. Ці методи дозволяють налаштовувати поведінку об’єктів класу при взаємодії з іншими об’єктами або при виконанні певних операцій. Тобто вони дозволяють класам мати або перевизначити певну поведінку при виконанні різних операцій.
Наприклад, функція len() викликає метод __len__() переданого об'єкта. Метод __add__(self, other) автоматично викликається при використанні оператора + для додавання.
Найвживаніші магічні методи
__init__- конструктор класу. Приймає екземпляр класу.__new__- приймає не instance, а об'єкт класу. Ініт вже приймає інстанс. Створює даний об'єкт, після цього на нього може відбуватись ініціалізація__class__- визначає клас або тип, екземпляром якого являється об'єкт. Клас і тип - різні назви одного і того ж -type(x) == x.__class____name__- імя класу__str__, __repr__- повертають рядкове представлення обєкту. repr - для девелопера, str - для прінта__getattr__- використовується для обробки доступу до атрибутів, що не існують в об'єкті при доступі за допомогою точкової нотаціїobj.<attribute>__setattr__, __delattr__- викликаються при будь-якому присвоєнні (obj.attr = ...) чи видаленні (del obj.attr) атрибута, незалежно від того, чи він уже існує (на відміну від__getattr__, який спрацьовує лише для відсутнього атрибута)__getattribute__- викликається при спробі отримати значення атрибуту. Якщо цей метод перевизначений, стандартний механізм пошуку значень атрибутів не буде задіяний.__dict__- сховище атрибутів, визначених користувачем. Пошук в ньому проводиться під час виконання і при пошуку враховується__dict__класу обєкту і базових класів.__bases__- список прямих батьків__iter__- повертає ітератор__eq__- перевірка на рівність з іншим об'єктом__add__- додавання до іншого об'єкта__call__- дозволяє екземпляру класу поводитися як функція. Це означає, що об’єкт класу можна викликати як функцію з допомогою дужок__getitem__- метод, який викликається, коли викликається при доступі за допомогою квадратних дужокmyobject[key], передаючи ключ або індекс (значення в квадратних дужках) як параметр. По послідовностях (списки, кортежі, рядки) можна ітеруватись, оскільки вони, реалізовуюсть метод__getitem__і__len____len__- повертає довжину послідовності__enter__,__exit__- потрібні для реалізації менеджера контексту
Найкращий спосіб правильно реалізувати ці методи (і перевірити набір методів, які потрібно реалізувати разом) — імплементувати базовий абстрактний клас, визначений в модулі collections.abc (https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes). Ці інтерфейси надають методи, які потрібно реалізувати, а також подбають про правильне створення типу (коли функція isinstance() викликається для об’єкта).
12. Які популярні типи індексів існують?⚑
🔥 59/100 · infrastructure/database.md
- B-дерево (B-Tree) індекс: Це один з найпоширеніших типів індексів. B-дерева використовуються для оптимізації пошуку даних у впорядкованих полях, таких як числа та рядки. Вони дозволяють виконувати операції порівняння (менше, більше, дорівнює) дуже швидко.
- Хеш-індекс: Хеш-індекси використовують хеш-функції для швидкого пошуку даних. Вони підходять для запитів з точним збігом значень. Однак, вони не так ефективні для діапазонних запитів або запитів, які використовують оператори порівняння.
- GIN (Generalized Inverted Index) індекс: Цей тип індексу використовується для повнотекстового пошуку та інших варіантів пошуку, які вимагають обробки великої кількості слів або термінів.
- GiST (Generalized Search Tree) індекс: GiST індекси призначені для роботи зі складними даними, такими як географічні дані, діапазони дат та інші. Це збалансоване дерево пошуку, точно так само, як і розглянуте раніше B-дерево. Але B-дерево застосовне лише до тих типів даних, для яких має сенс операція порівняння та є можливість упорядкування. Проте PostgreSQL дозволяє зберігати і такі дані, для яких операція упорядкування не має сенсу, наприклад, геодані та геометричні об'єкти.
- SP-GiST (Space-Partitioned Generalized Search Tree) індекс: Цей тип індексу використовується для роздільних даних, які можна розбити на області. Наприклад, для географічних даних це можуть бути координати на карті. SP-GiST схожий із GiST, але він дозволяє створювати незбалансовані дерева. Такі дерева можуть бути корисними при розбитті множини на об'єкти які не перетинаються. Літери SP означають space partitioning.
Підрядковий і повнотекстовий пошук. B-дерево обслуговує точні та діапазонні порівняння, але не пошук по підрядку (LIKE '%abc%') - воно впорядковане за повним значенням. Для ILIKE/підрядка ставлять GIN з розширенням pg_trgm (триграмним). Повнотекстовий пошук (to_tsvector) через GIN вимагає коректної мовної конфігурації: дефолтний english не зведе словоформи кирилиці, тож для україно-/російськомовного тексту задають відповідну конфігурацію.
Links
13. Що може бути ключем словника? Що не може?⚑
🔥 59/100 · python/set_and_dict.md
Ключем словника може бути будь-який хешований незмінний об'єкт: число, рядок, datetime, функція або навіть модуль. Такі об'єкти мають метод __hash__(), який однозначно відповідає об'єкту деяким числом. За цим числом словник шукає значення для ключа. А також метод __eq__() - для порівняння ключів на рівність, а не за адресами пам'яті. Це означає, що, навіть якщо два об'єкти мають різні адреси пам'яті, вони вважатимуться рівними, якщо __eq__() повертає True.
Списки, словники, множини є змінними і не мають методу хешування. При спробі вставити їх у словник виникне помилка.
Об'єкти, які є екземплярами класів, визначених користувачем, є хешованими за замовчуванням і їхнє хеш-значення базується на їхньому id(). Тобто усі будуть не рівними. Навіть якщо перевизначити метод __hash__, наступний словник поверне 2 ключі (потрібно перевизначити метод __eq__, щоб він повертав True)
class C:
def __hash__(self):
return 42
c, d = C(), C()
x= {c: 'c', d: 'd'}
print(list(x.keys())) # [<__main__.C object at 0x101656300>, <__main__.C object at 0x101655d30>]
Хеш кортежу обчислюється рекурсивно по всім його елементам. Таким чином, кортеж (1, (True, (42, ('hello', )))) складається лише з незмінних елементів, тому може бути ключем. Однак кортеж (1, (True, (42, ({'hello': 'world'},)))) не може бути ключем, оскільки в ньому є змінний словник.
14. Композиція проти Наслідування (Composition vs Inheritance)⚑
🔥 57/100 · computer_science/oop.md
Summary
- Агрегація та композиція - це відношення
has-a(має) між об'єктами, де один клас містить посилання на інший клас.- Наслідування - це відношення
is-a(є), коли один клас є різновидом іншого. Наслідуваний клас отримує всі методи та властивості батьківського класу.- В загальному випадку, слід віддавати перевагу композиції перед наслідуванням, бо вона дає більше гнучкості.
Композиція - це підхід до створення програмного коду, при якому об'єкти одного класу включаються в інший клас як його атрибути, а не через наслідування. Це означає, що один клас містить посилання на об'єкти інших класів, і він може використовувати їх функціонал для досягнення певних завдань. Композиція дозволяє створювати складніші структури даних і об'єкти, комбінуючи простіші компоненти. Хороший код повторно використовується, маючи невеликі цілісні абстракції, а не створюючи ієрархії.
Відмінність від агрегації полягає в тому, що агрегація відображає "відношення частини до цілого", де один клас "складається" з інших класів, але ці частини можуть існувати окремо від цілого. Наприклад, автомобіль має двигун, але двигун може існувати сам по собі. Композиція також представляє "відношення частини до цілого", але тут частини не можуть існувати окремо від цілого. Якщо ціле знищується, то і частини припиняють своє існування. Наприклад, будинок має кімнати, і ці кімнати залежать від будинку.
Переваги
- Допомагає уникнути class explosion problem - успадкування може привести до величезної ієрархічної структури класів, яку важко зрозуміти та підтримувати. А також може збільшити ризик конфліктів імен методів та змінних.
- Гнучкість та мінімізація залежностей. При композиції відношення між двома класами вважаються слабко зв'язаними. Це означає, що зміни в класі компонента рідко впливають на клас композиту, і зміни в класі композиту ніколи не впливають на клас компонента. Це забезпечує кращу адаптивність до змін і дозволяє додаткам вводити нові вимоги, не впливаючи на існуючий код.
- Полегшує тестування - тестувати окремі компоненти легше, оскільки вони є менш залежними одне від одного.
Наслідування (is-a) - Dog — це Animal, тобто собака є твариною.
class Animal:
def make_sound(self):
print("Some generic sound")
class Dog(Animal):
def make_sound(self):
print("Woof!")
dog = Dog()
dog.make_sound() # Woof!
Агрегація (has-a - слабкий зв'язок) - Company має Employees, але якщо компанія закриється, співробітники можуть працювати в іншому місці.
class Employee:
def __init__(self, name):
self.name = name
def work(self):
print(f"{self.name} is working.")
class Company:
def __init__(self, name, employees):
self.name = name
self.employees = employees
def show_employees(self):
for emp in self.employees:
print(f"Employee: {emp.name}")
emp1 = Employee("Alice")
emp2 = Employee("Bob")
company = Company("TechCorp", [emp1, emp2])
company.show_employees()
del company
print(emp1.name) # Alice still exists
Композиція (has-a - сильний зв'язок) - House має Rooms, але кімнати не можуть існувати без будинку.
class Room:
def __init__(self, name):
self.name = name
class House:
def __init__(self):
self.rooms = [Room("Kitchen"), Room("Bedroom")] # Rooms are created in the house
def show_rooms(self):
for room in self.rooms:
print(f"Room: {room.name}")
house = House()
house.show_rooms()
del house # We delete the house - the rooms also disappear
print(house.rooms) # Error because house no longer exists
15. Хеш-таблиця⚑
🔥 53/100 · computer_science/data_structures.md
Summary
Хеш-таблиця (Hash Table) — це структура даних, яка використовується для зберігання пар "ключ-значення" і забезпечує ефективний доступ до даних. Основною особливістю хеш-таблиці є використання хеш-функції для перетворення ключа в індекс масиву, де зберігається відповідне значення. Це дозволяє здійснювати пошук, вставку і видалення елементів з високою швидкістю.
Хеш-таблиці забезпечують складність вставки, видалення та доступ до елементів з постійним часом (O(1)).
Хеш-функція перетворює ключ у індекс масиву, за яким зберігається відповідне значення. При цьому можуть виникати колізії. Вони виникають, коли хеш-функція дає однаковий індекс для різних ключів. Існують різні методи вирішення колізій, такі як ланцюгове з'єднання (chaining) і відкрита адресація (open addressing).
Переваги хеш-таблиць
- Швидкість: Операції вставки, пошуку і видалення зазвичай виконуються за O(1) час (в середньому випадку).
- Гнучкість: Можуть зберігати будь-які типи даних як ключі та значення.
Ефективність (О велике)
- Індексування- O(1)
- Пошук - O(1)
- Вставка - O(1)
class HashTable:
def __init__(self, size):
self.size = size
self.table = [[] for _ in range(size)]
def hash_function(self, key):
return hash(key) % self.size # size of hash table
def insert(self, key, value):
index = self.hash_function(key)
for pair in self.table[index]:
if pair[0] == key:
pair[1] = value
return
self.table[index].append([key, value])
def search(self, key):
index = self.hash_function(key)
for pair in self.table[index]:
if pair[0] == key:
return pair[1]
return None
def delete(self, key):
index = self.hash_function(key)
for i, pair in enumerate(self.table[index]):
if pair[0] == key:
del self.table[index][i]
return
hash_table = HashTable(10)
hash_table.insert("apple", 1)
hash_table.insert("banana", 2)
print(hash_table.search("apple")) # Output: 1
hash_table.delete("apple")
print(hash_table.search("apple")) # Output: None
16. Що таке транзакція? Що таке ACID?⚑
🔥 53/100 · infrastructure/database.md
Транзакція - це послідовність дій, які виконуються в базі даних або іншій системі з метою забезпечення консистентності даних та виконання операцій атомарно (тобто вони повинні бути виконані цілісно або не виконуватися взагалі - все або нічого). Іншими словами, транзакція - це група дій, які мають бути виконані як єдине ціле. Транзакція є робочою одиницею роботи з базою даних (БД).
Транзакція - це внесення змін у БД. Наприклад, якщо ми створюємо, змінюємо або видаляємо запис, то ми виконуємо транзакцію. Дуже важливо контролювати транзакції для гарантування цілісності. Крім того, транзакції дозволяють відновлювати базу даних до попереднього стану в разі виникнення помилки або аварії.
Тобто транзакція — це набір SQL-команд, які об'єднані в один фізичний і логічний блок і вважаються, згідно з ACID, найменшим неподільним об'єктом.
Основні концепції (властивості) транзакції описуються абревіатурою ACID
- Atomicity - Атомарність
- Consistency - Узгодженість
- Isolation - Ізоляція
- Durability - Довговічність
Атомарність (Atomicity)
Атомарність гарантує, що транзакція або виконується повністю або зовсім не виконується. Якщо одна з операцій у послідовності не буде виконана, то вся транзакція буде відмінена. Тут вводиться поняття "відкату" (rollback). Тобто всередині послідовності відбуватимуться певні зміни, але в результаті всі вони будуть скасовані ("відкочені"), і в результаті користувач не побачить жодних змін, тобто зміни повертаються до стану, який був на початку запуску транзакції. У випадку вкладених транзакцій, rollback відбувається до найближчого SAVEPOINT.
Наприклад, якщо гроші списалися з одного рахунку, але не надійшли на інший через помилку, вся операція скасовується і гроші залишаються на першому рахунку.
Узгодженість (Consistency)
Узгодженість означає, що будь-яка завершена транзакція (транзакція, яка досягла завершення транзакції - end of transaction) фіксує лише допустимі результати. Система має перебувати в узгодженому, несуперечливому стані до початку дії транзакції і по її завершенню - не має бути порушення FK, PK, Unique key та інших обмежень даних. При цьому вона може перебувати в неузгодженому стані протягом виконання транзакції, проте ця неузгодженість не буде видимою за межами транзакції завдяки іншим властивостям - атомарності та ізольованості.
Наприклад, при переведенні коштів з рахунку на рахунок, кошти можна спочатку зняти з першого рахунку, після чого нараховувати на другий. Відповідно, після зняття коштів, але до їх нарахування система перебуває в неузгодженому стані: коштів немає на жодному з рахунків. Але після завершення транзакції повна сума перебуватиме на другому (або першому у випадку скасування транзакції) рахунку.
Ізоляція (Isolation)
Кожна транзакція повинна бути ізольована від інших, тобто її результат не повинен залежати від виконання інших паралельних транзакцій та не повинна впливати одна на одну. На практиці, ізоляції важко досягнути, тому тут вводиться поняття "рівні ізоляції" (транзакція ізолюється не повністю).
Наприклад дві людини одночасно намагаються змінити баланс на одному рахунку. Ізольованість гарантує, що одна транзакція не вплине на іншу.
Довговічність (Durability)
Довговічність означає, що після успішного завершення транзакції її результати зберігаються у базі даних, навіть якщо відбувається збій системи. Це означає, що дані не втрачаються і залишаються доступними після будь-яких помилок, таких як збій живлення.
Наприклад після того, як ви завершили переказ грошей, нова інформація про баланси зберігається і залишається доступною, навіть якщо сервер бази даних раптово вимкнеться. Коли сервер перезапуститься, дані будуть відновлені, оскільки транзакцію було завершено.
Links
17. Що таке MRO⚑
🔥 50/100 · python/class_and_object.md
Summary
MRO (Method Resolution Order) - порядок вирішення методу. Це алгоритм пошуку методу в разі, якщо у класу є два або більше батьків.
При успадкуванні класів нового стилю застосовується правило MRO (порядок вирішення методів), тобто лінійний обхід дерева класів, при цьому вкладений елемент успадкування стає доступним у атрибуті __mro__ даного класу. Такий алгоритм називається C3-лініаризація. Наслідування за правилом MRO здійснюється приблизно за наступним порядком.
- Перерахування всіх класів, успадкованих екземпляром, за правилом пошуку DFLR для класичних класів, причому клас включається в результат пошуку стільки разів, скільки він зустрічається при обході.
- Перегляд у отриманому списку дублікатів класів, з яких видаляються всі, крім останнього (останнього справа) дубліката в списку.
Обхід у глибину та зліва направо - DFLR
- Спочатку екземпляр
- Потім його клас
- Далі всі суперкласи його класу з обходом спочатку у глибину, а потім зліва направо
- Використовується перше знайдене входження.
Упорядкування за правилом MRO застосовується при успадкуванні та виклику вбудованої функції super(), яка завжди викликає наступний клас за правилом MRO (відносно точки виклику).
Приклад успадкування в не-ромбовидних ієрархічних деревах
class D: attr = 3 # D:3 E:2
class B(D): pass # | |
class E: attr = 2 # B C:1
class C(E): attr = 1 # \ /
class A(B, C): pass # A
X = A() # |
print(X.attr) # X
>>> DFLR = [X, A, B, D, C, E]
>>> MRO = [X, A, B, D, C, E, object]
>>> Outputs string "3"
Приклад успадкування в ромбовидних ієрархічних деревах
class D: attr = 3 # D:3
class B(D): pass # / \
class C(D): attr = 1 # B C:1
class A(B, C): pass # \ /
X = A() # A
print(X.attr) # |
... # X
>>> DFLR = [X, A, B, D, C, D]
>>> MRO = [X, A, B, C, D, object] (keeps only the last duplicate D)
>>> Outputs string "1"
18. Поліморфізм⚑
🔥 48/100 · computer_science/oop.md
Summary
Поліморфізм - це один з ключових принципів ООП, який дозволяє об'єктам з різних класів використовувати один і той же інтерфейс, але мати різну реалізацію цього інтерфейсу. Іншими словами, це різна поведінка одного і того ж методу в різних класах. Тобто - один інтерфейс, але різна поведінка і різна реалізація.
Наприклад ми можемо додати два числа, і можемо додати два рядки. При цьому отримаємо різний результат - так як числа і рядки являються різними класами. Це реалізується з допомогою __add__ - "магічний методів" в Python, який використовується для перевантаження оператора додавання +. Він дозволяє задати спеціальну реалізацію операції додавання для об'єктів певного класу.
__add__ для int
class MyInt:
def __init__(self, value):
self.value = value
def __add__(self, other):
return self.value + other
num1 = MyInt(5)
num2 = 10
result = num1 + num2
print(result) # Output: 15
__add__ для str
class MyString:
def __init__(self, value):
self.value = value
def __add__(self, other):
return f"{self.value}{other}"
str1 = MyString("Hello, ")
str2 = "world!"
result = str1 + str2
print(result) # Output: Hello, world!
Види поліморфізму
- Підтиповий (subtype). Поліморфна поведінка через успадкування: об'єкт дочірнього класу можна підставити там, де очікується батьківський, поки дочірній не ламає його інтерфейс. Прямо пов'язаний з принципом підстановки Лісков (LSP).
- Параметричний (parametric). Тип не важливий - важливо, щоб об'єкт реалізовував потрібну поведінку. У Python це качина типізація (а також узагальнення через
TypeVar/ generics): функція приймає будь-що, у чого є очікуваний метод. - Ad-hoc (перевантаження). Одна операція з різними реалізаціями для різних типів. Перевантаження операторів через dunder-методи (
__add__вище) - саме цей вид. Окремих функцій, перевантажених за сигнатурою (як у C++), Python не має; найближче в рантаймі -functools.singledispatch, що обирає реалізацію за типом першого аргументу:
from functools import singledispatch
@singledispatch
def to_log(value) -> str:
return f"raw: {value}"
@to_log.register
def _(value: int) -> str:
return f"int: {value}"
@to_log.register
def _(value: list) -> str:
return f"list of {len(value)}"
to_log(42) # "int: 42"
to_log([1, 2]) # "list of 2"
typing.overload, на відміну від singledispatch, впливає лише на перевірку типів і не дає диспетчеризації під час виконання.
19. Що таке рівні ізоляції транзакцій. Які вони бувають⚑
🔥 47/100 · infrastructure/database.md
Ізоляція у сенсі ACID означає, що конкурентно виконувані транзакції ізольовані одна від одної - вони не можуть заважати одна одній. Класичні підручники з баз даних розуміють під ізоляцією серіалізованість (serializability). Тобто кожна транзакція виконується так, наче вона єдина в усій базі. БД гарантує, що результат фіксації транзакцій такий самий, як якщо б вони виконувались послідовно (по одній за раз), хоча насправді вони можуть виконуватись конкурентно.
При паралельному виконанні транзакцій можливі такі проблеми
- втрачене оновлення (lost update) - при одночасній зміні одного блоку даних різними транзакціями, одна із змін втрачається. Наприклад, T1 i T2 працюють над записом з полем foo = 10, T1 додає до поля 5 а T2 додає 15. У результаті ми очікуємо, що буде 30, але транзакція T1 не знає нічого про T2 і навпаки. Вони працюють із полем foo, яке знаходиться в їхній пам'яті, а не з реальним значенням, що міститься в базі, тому зміни T1 або T2 будуть втрачені.
- "брудне" читання (dirty read) - читання даних, які додані чи змінені транзакцією, яка потім не підтвердиться (відкотиться). Наприклад, T1 i T2 працюють над записом із полем foo=10, T1 додає 5, а T2 додає 15. У цьому випадку, якщо одна транзакція змінила поле, інша побачить це та буде працювати з новим полем. Але якщо одна з транзакцій не виконається після того, як інша завершить своє виконання, дані не будуть повернуті в попередній стан. Тобто якщо T1 завершить свою роботу раніше Т2, в полі foo буде 30, а не 25.
- неповторюване читання (non-repeatable read) - транзакція читає один і той самий запис в рамках однієї транзакції, але отримує різні значення. Наприклад, T1 читає запис, через деякий час T2 закінчує оновлення полів цього запису і при повторному читанні T1 отримує інший результат значень полів.
- фантомне читання (phantom reads) - схожа до non-repeatable read, але одна транзакція в ході свого виконання декілька разів вибирає множину рядків за одними і тими ж критеріями. Інша транзакція в інтервалах між цими вибірками додає чи видаляє рядки чи змінює стовпці деяких рядків, що використовується в критеріях вибірки першої транзакції, і успішно закінчується. В результаті отримаємо, що одні і ті ж вибірки в першій транзакції дають різні множини рядків, містять більше/менше записів відносно попередньої операції.
Рівень ізольованості транзакцій — значення, що задає рівень, при якому в транзакції дозволяються неузгоджені дані, тобто ступінь ізольованості однієї транзакції від іншої. Більш високий рівень ізольованості підвищує точність даних, але при цьому може знижуватись кількість транзакцій, що виконуються паралельно. З іншого боку, більш низький рівень ізольованості дозволяє виконувати більше паралельних транзакцій, але знижує точність даних. Тобто при найменшому рівні ізоляції ми отримуємо найбільший перформанс і навпаки.
У стандарті SQL описується чотири рівні ізоляції транзакцій
- Read uncommited (Читання незафіксованих даних)
- Read committed (Читання зафіксованих даних)
- Repeatable read (Повторюване читання)
- Serializable (Серіалізований)
Читання незафіксованих даних (Read Uncommitted)
Найнижчий рівень ізоляції. Транзакція може читати дані, які ще не були зафіксовані іншою транзакцією. Це може призводити до "брудного читання" (dirty reads), коли транзакція працює з даними, які можуть бути відкочені.
Читання зафіксованих даних (Read Committed)
Транзакція бачить лише ті зміни, які вже зафіксовані іншими транзакціями. Це запобігає "брудному читанню", але не вирішує проблему "неповторюваного читання" (non-repeatable reads), коли один і той самий запит повертає різні результати в межах однієї транзакції.
У загальному використанні без FOR UPDATE/SHARE операції працюють зі snapshot-даними, ніякі блокування полів для читання (shared lock) не застосовуються.
Реалізація завершеного читання може ґрунтуватися на одному з двох підходів: блокуванні або версійності.
- Блокування даних, що читаються та змінюються.
- Полягає в тому, що записуюча транзакція блокує змінні дані для читаючих транзакцій, що працюють на рівні read committed або вищому, до свого завершення, перешкоджаючи таким чином "брудному" читанню, а дані, що зчитуються, які блокуються транзакцією, звільняються одразу після завершення операції
SELECT(отже ситуація "неповторного читання" може виникати на даному рівні ізоляції).
- Полягає в тому, що записуюча транзакція блокує змінні дані для читаючих транзакцій, що працюють на рівні read committed або вищому, до свого завершення, перешкоджаючи таким чином "брудному" читанню, а дані, що зчитуються, які блокуються транзакцією, звільняються одразу після завершення операції
- Збереження декількох версій рядків, що змінюються паралельно.
- При кожній зміні рядка СУБД створює нову версію цього рядка, з якої продовжує працювати транзакція, що змінила дані, в той час як будь-якій іншій "читає" транзакції повертається остання зафіксована версія. Перевага такого підходу в тому, що він забезпечує більшу швидкість, оскільки запобігає блокуванню. Однак він вимагає, порівняно з першим, значно більшої витрати оперативної пам'яті, яка витрачається на зберігання версій рядків.
Повторюване читання (Repeatable Read)
Забезпечує, що дані, прочитані транзакцією, залишаються незмінними до її завершення. Вирішує проблему "неповторюваного читання", але не запобігає "фантомному читанню" (phantom reads), коли нові рядки додаються іншими транзакціями і впливають на результати.
Тобто транзакція весь час буде працювати тільки з однією копією snapshot-а, але власні зміни будуть видимі для всіх операцій всередині транзакції.
Транзакція на цьому рівні ізоляції не може використовувати exclusive lock для полів, що були модифіковані іншою транзакцією, або змінювати модифіковані поля. Якщо ж транзакція спробує це зробити, вона отримає помилку серіалізації. Усі зміни повинні відкотитись, і потрібно спробувати провести транзакцію знову.
Серіалізований (Serializable)
Найвищий рівень ізоляції. Гарантує, що транзакції виконуються так, ніби вони були послідовними, тобто одна за одною. Усуває всі згадані проблеми, включаючи "фантомне читання", але може значно знизити продуктивність через блокування ресурсів.
Однакова назва ≠ однакова поведінка
Стандарт SQL описує чотири рівні, але кожна СУБД реалізує їх по-своєму. Часта пастка - припускати, що REPEATABLE READ у PostgreSQL і MySQL InnoDB означають одне і те ж.
PostgreSQL (docs: Transaction Isolation):
- Default -
Read Committed. Read Uncommittedприймається синтаксично, але поводиться якRead Committed: "PostgreSQL's Read Uncommitted mode behaves like Read Committed. This is because it is the only sensible way to map the standard isolation levels to PostgreSQL's multiversion concurrency control architecture". Фактично реалізовано три рівні з чотирьох.Repeatable Readреалізовано як Snapshot Isolation: "implemented using a technique known in academic database literature and in some other database products as Snapshot Isolation".Serializableз версії 9.1 - Serializable Snapshot Isolation (SSI): "implemented using a technique known in academic database literature as Serializable Snapshot Isolation, which builds on Snapshot Isolation by adding checks for serialization anomalies". До 9.1Serializableповодився якRepeatable Read.
MySQL InnoDB (docs: InnoDB Transaction Isolation Levels):
- Default -
Repeatable Read(неRead Committed, як у Postgres). - Plain (non-locking)
SELECTвикористовує consistent snapshot, не бере блокувань. - Для locking reads (
SELECT ... FOR UPDATE,FOR SHARE),UPDATE,DELETEз range-умовою InnoDB бере gap locks і next-key locks: "InnoDB locks the index range scanned, using gap locks or next-key locks to block insertions by other sessions into the gaps covered by the range". Це може заблокувати insert у "сусідній" діапазон. - Виняток: для unique index з unique search condition блокується лише знайдений запис, без gap.
SQL Server (docs: SET TRANSACTION ISOLATION LEVEL):
- Default -
Read Committed(у on-prem; Azure SQL Database маєREAD_COMMITTED_SNAPSHOT ONза замовчуванням). - Поведінка
Read Committedзалежить відREAD_COMMITTED_SNAPSHOT: зOFF- locks-based, зON- row versioning, читання не блокують записи. - Окремий рівень
SNAPSHOT(потребуєALLOW_SNAPSHOT_ISOLATION ON) - як PostgresRepeatable Read. Це п'ятий рівень, окремий відRepeatable Read. Serializable- класична range-lock-based реалізація, не SSI.
Oracle (docs: Data Concurrency and Consistency):
- Default -
Read Committed. Реалізовано через undo segments і snapshot read consistency. - Oracle підтримує лише два рівні з чотирьох стандартних:
Read CommittedіSerializable(плюсRead-Onlyяк власне розширення).Read UncommittedіRepeatable Readяк окремі рівні відсутні. - Dirty reads неможливі ні на якому рівні: "Oracle Database never permits a dirty read".
Serializableдає transaction-scope snapshot - те, що в інших БД називали б Repeatable Read; справжня serial-order serializability за чистим визначенням Berenson 1995 - частковий випадок.
Базове правило: перед написанням коду, який покладається на конкретний рівень ізоляції, дивитися документацію конкретної версії конкретної бази, не тільки на назву.
Links
- Реалізація рівнів ізоляцій у базах даних - dou.ua
- PostgreSQL: Transaction Isolation
- MySQL InnoDB: Transaction Isolation Levels
- SQL Server: SET TRANSACTION ISOLATION LEVEL
- Oracle: Data Concurrency and Consistency
20. Що таке MQ⚑
🔥 47/100 · infrastructure/mq.md
Черги повідомлень, по суті, є сполучною ланкою між різними процесами у додатках та надають надійний та масштабований інтерфейс взаємодії з іншими підключеними системами та пристроями. Черга — структура даних з принципом доступу до елементів "перший прийшов — перший вийшов". Додавання елемента можливе лише в кінець черги, вибірка — лише з початку черги, при цьому вибраний елемент з черги видаляється.
Десять причин, чому черги повідомлень є життєво важливим компонентом для будь-якої архітектури чи додатка
- Слабке зв'язування — черги повідомлень створюють неявні інтерфейси обміну даними, які дозволяють процесам бути незалежними одне від одного, тобто ви просто визначаєте формат повідомлень, відправлених від одного процесу іншому.
- Надмірність — черги дозволяють уникнути випадків неефективного використання ресурсів процесу (наприклад, пам'яті) внаслідок зберігання невикористаної (надмірної) інформації.
- Масштабованість — черги повідомлень дозволяють розподілити процеси обробки інформації. Таким чином, вони дозволяють легко збільшувати швидкість, з якою повідомлення додаються в чергу та обробляються.
- Еластичність та можливість витримувати пікові навантаження — черги повідомлень можуть виконувати роль свого роду буфера для накопичення даних в разі пікового навантаження, згладжуючи таким чином навантаження на систему обробки інформації та запобігаючи її відмові.
- Стійкість до відмов — черги повідомлень дозволяють розділити процеси один від одного, так що якщо процес, який обробляє повідомлення з черги, падає, повідомлення можуть бути додані в чергу для обробки пізніше, коли система відновиться.
- Гарантована доставка — використання черг повідомлень гарантує, що повідомлення буде доставлено та оброблено в будь-якому випадку (поки є принаймні один обробник).
- Гарантований порядок доставки — більшість систем черг повідомлень здатні забезпечити гарантії того, що дані будуть оброблятися у певному порядку (найчастіше у порядку їх надходження).
- Буферизація — черги повідомлень дозволяють відправляти та отримувати повідомлення, при цьому працюючи з максимальною ефективністю, надаючи буферний шар — процес запису в чергу може відбуватися настільки швидко, наскільки це може робити черга повідомлень, а не обробник повідомлення.
- Розуміння потоків даних — черги повідомлень дозволяють виявляти вузькі місця в потоках даних додатка, легко можна визначити, яка з черг заповнюється, яка не використовується, і визначити, що необхідно робити — додавати нових обробників повідомлень або оптимізувати поточну архітектуру.
- Асинхронний зв'язок — черги повідомлень надають можливість асинхронної обробки даних, що дозволяє помістити повідомлення в чергу без обробки, дозволяючи системі обробити повідомлення пізніше, коли з'явиться можливість.
- Винесення довгих задач із циклу запит-відповідь — частина роботи є одночасно тривалою і CPU-bound (наприклад, обробка великого файлу, що триває години). Її не можна виконати в межах HTTP-запиту (клієнт/проксі відваляться по таймауту), не рятує й
async(event loop блокується на CPU-роботі), ані просте додавання інстансів застосунку за балансувальником. Рішення — покласти задачу в чергу і виконати її фоновим воркером, лишивши в основному застосунку лише швидкі операції.
21. Asyncio⚑
🔥 47/100 · python/async.md
Asyncio – модуль асинхронного програмування, який був представлений в Python 3.4. Він призначений для використання співпрограм і future для спрощення написання асинхронного коду і робить його майже таким самим читаним, як синхронний код, через відсутності callback-ів.
Asyncio надає цикл подій та ще деякі інші функції. Цикл подій реагує на різні I/O-події та перемикається на завдання, що можуть виконуватися і призупиняє ті, що чекають на I/O. Тобто ми не витрачаємо час на завдання, що ще не готові виконуватися.
Asyncio використовує різні конструкції: event loop, співпрограми та future.
- event loop управляє і контролює виконання різних завдань. Він реєструє їх і обробляє розподіл потоку управління між ними.
- Співпрограми – це спеціальні функції, робота яких схожа з роботою генераторів в Python, за допомогою
awaitвони повертають потік управління назад вevent loop. Запуск співпрограми повинен бути запланований вevent loop. Заплановані співпрограми будуть обгорнуті в Завдання, що є типомFuture. - Future - це об'єкти, які представляють результат, що буде доступний у майбутньому. Вони як порожнє місце, яке ще не має значення, але обіцяє його надати. Найкраще порівняння — футура = обіцянка. Коли результат стає доступним (наприклад, прийшла відповідь з мережі), футура позначається як завершена і викликає колбек — саме той, який був зареєстрований, щоб продовжити виконання корутини. Тобто
futureпоказує результат задачі, яка може або не може бути виконана. Результатом може бути exception. Коли огортаємо співпрограму вFuture- отримуємо об’єктTask. - Завдання (
asyncio.Tasks) - це обгортки над корутинами, які запускаються у фоновому режимі й одразу передаються під контроль циклу подій. На відміну від звичайної корутини, яка просто "готова" до виконання, задача може почати працювати відразу після створення. Вона стежить за перебігом виконання, зберігає результат або помилку, і дозволяє іншим частинам коду дізнатись, чим усе завершилось. Тобто вона огортає корутини, аби їх виконання могло незалежно плануватись циклом подій, коли йому передається управління (зазвичай за допомогоюawait). Створити завдання можна за допомогоюasyncio.create_task(). У PythonTask— це підкласFuture, тобто кожна таска по суті є футурою, але з додатковою логікою виконання корутин.
Спрощено схема роботи виглядає наступним чином: У нас є цикл подій (event loop) та асинхронні функції, I/O-операції. Ми передаємо свої функції до циклу подій, щоб він запустив їх. Цикл подій повертає нам об'єкт Future. Можна сказати, що це обіцянка, що ми отримаємо якісь дані в майбутньому. Ми зберігаємо його і час від часу перевіряємо чи не має наш Future результату виконання. І якщо так, то використовуємо ці дані для подальшої обробки.
Щоб зупиняти та відновлювати завдання asyncio використовує генератори та співпрограми (generators and coroutines). У разі, якщо в черзі очікування є завдання, то контекст буде перемикнуто, в іншому випадку – ні.
Визначення корутини починається з async, а її виклик - з await. asyncio.run(coroutine) є основною точкою входу для асинхронних програм.
Функції wait(), gather() і as_completed() запускають кілька корутин одночасно. Модуль asyncio також надає власні класи Queue, Event, Lock і Semaphore.
Asyncio на прикладі
import asyncio
import datetime
import random
async def my_sleep_func():
await asyncio.sleep(random.randint(0, 5))
async def display_date(num, loop):
end_time = loop.time() + 50.0
while True:
print("Loop: {} Time: {}".format(num, datetime.datetime.now()))
if (loop.time() + 1.0) >= end_time:
break
await my_sleep_func()
loop = asyncio.get_event_loop()
asyncio.ensure_future(display_date(1, loop))
asyncio.ensure_future(display_date(2, loop))
loop.run_forever()
- Асинхронна функція
display_dateприймає число-індентифікатор та цикл подій. - Функція має безкінечний цикл, що переривається через 50 секунд. Але поки 50 секунд не минуло, вона друкує час і засинає на випадкову кількість секунд. Ключове слово
awaitвказує, що під час виконання функції, що стоїть після нього, можна перемкнутися на іншу асинхронну функцію (співпрограму). - Функції додаються до циклу подій за допомогою функції
ensure_future. - Запускається цикл подій.
Links
22. Дерево⚑
🔥 46/100 · computer_science/data_structures.md
Особлива різновидність графа, в якому немає ребер, які вказують у зворотньому напрямку. Іноді дерева називають ациклічними графами.
23. Що таке SOLID⚑
🔥 45/100 · computer_science/solid.md
Summary
П'ять принципів ООП-дизайну (SRP, OCP, LSP, ISP, DIP) для систем, що довго розширюються й підтримуються; інструмент проти зайвої складності, а не самоціль.
SOLID - акронім із перших літер п'яти принципів ООП-дизайну. Самі принципи сформулював Роберт Мартін (статті в C++ Report у 1990-х, есе "Design Principles and Design Patterns" 2000 р.), а сам акронім запропонував Майкл Фезерс близько 2004 р.
Принципи SOLID використовуються для дизайну і розробки таких програмних систем, які зможуть тривалий час розширятись, розвиватись і підтримуватись.
S: Single Responsibility Principle (Принцип єдиної відповідальності). Кожен клас повинен мати лише одну відповідальність, вирішувати тільки одне завдання. Або ж кожен клас повинен мати лише одну причину для змін.
O: Open-Closed Principle (Принцип відкритості-закритості). Код має бути відкритим до розширення (тобто до додавання нового функціоналу) та закритим до змін (все, що вже написано, не повинно змінюватися).
L: Liskov Substitution Principle (Принцип підстановки Барбари Лісков). Об'єкти в програмі можуть бути заміненими їх нащадками без зміни коду програми. Клас-нащадок повинен доповнювати, а не змінювати базовий.
I: Interface Segregation Principle (Принцип розділення інтерфейсу). Багато спеціалізованих інтерфейсів краще за один універсальний. Клієнти не повинні залежати від інтерфейсів, які вони не використовують.
D: Dependency Inversion Principle (Принцип інверсії залежностей). Об'єктом залежності повинна бути абстракція, а не щось конкретне.
- Модулі верхніх рівнів не повинні залежати від модулів нижніх рівнів. Обидва типи модулів повинні залежати від абстракцій.
- Абстракції не повинні залежати від деталей. Деталі повинні залежати від абстракцій.
SOLID - компас, а не самоціль. Класичні приклади прийшли зі строго типізованих мов (Java, C#), де частину рішень диктував компілятор. Python дає й легші засоби для тих самих цілей - функції першого класу, dataclass-и, typing.Protocol, - тож нижче для кожного принципу, крім класичного формулювання, показано і ці варіанти (єдиного "правильного" стилю немає). Сліпе ж слідування SOLID шкодить:
- Over-engineering простих скриптів - крон-парсер на 200 рядків не потребує абстрактних фабрик і протоколів.
- Ігнорування вбудованих структур - клас
UserDataCollectionтам, де вистачаєdictчиlist. - Порушення YAGNI - абстракція "на випадок, якщо колись перейдемо з Postgres на Mongo", яку найімовірніше доведеться роками підтримувати мертвою, бо перехід так і не настане.
Links
24. Що таке збирач сміття (garbage collector)?⚑
🔥 45/100 · python/interpreter.md
Збирач сміття (generational garbage collector або GC) - це механізм, створений для виявлення та видалення циклічних посилань. gc є вбудованим модулем в Python і може бути вимкнений або запущений вручну (або не запущений) за потреби.
На відміну від інших популярних мов програмування, Python не повністю звільняє всю пам'ять назад до операційної системи, як тільки він видаляє якийсь об'єкт. Замість цього, він використовує додатковий менеджер пам'яті для малих об'єктів (розміром менше 512 байтів). Для роботи з такими об'єктами він виділяє великі блоки пам'яті, в яких надалі будуть зберігатися багато малих об'єктів.
Коли один з малих об'єктів видаляється, пам'ять під ним не повертається операційній системі, Python залишає її для нових об'єктів з таким самим розміром. Якщо в одному з виділених блоків пам'яті не залишилося об'єктів, Python може звільнити його операційній системі. Звільнення блоків зазвичай відбувається, коли скрипт створює багато тимчасових об'єктів.
Таким чином, якщо довгоживучий процес Python з часом починає споживати більше пам'яті, це зовсім не означає, що в коді є проблема з витоком пам'яті.
Стандартний інтерпретатор Python (CPython) використовує два алгоритми збирання сміття: підрахунок посилань (reference counting) та збирач сміття (generational garbage collector або GC), більш відомий як стандартний модуль gc в Python.
Алгоритм підрахунку посилань дуже простий і ефективний, але він має один великий недолік. Він не може виявити циклічні посилання. Саме тому в Python існує додатковий збирач, який називається GC і відстежує об'єкти з потенційними циклічними посиланнями. Коли об’єкти посилаються один на одного, утворюючи цикли (A посилається на В, В посилається на С, С посилається на А). У таких випадках, навіть якщо всі зовнішні посилання на ці об’єкти видалені, їхні лічильники посилань не стають нульовими, і пам’ять не звільняється.
На відміну від алгоритму підрахунку посилань, збирач сміття не працює в режимі реального часу і запускається періодично. Кожен запуск збирача створює мікропаузи в роботі коду.
Збирач сміття розділяє всі об'єкти на 3 покоління. Нові об'єкти потрапляють до першого покоління. Якщо новий об'єкт виживає після процесу збирання сміття, він переміщується до наступного покоління. Чим вище покоління, тим рідше воно сканується на сміття. Оскільки нові об'єкти часто мають дуже короткий термін життя (є тимчасовими), то їх варто опитувати частіше, ніж ті, які пройшли кілька етапів збирання сміття.
У кожному поколінні є спеціальний лічильник і поріг спрацювання, які визначають початок процесу збирання сміття. Кожен лічильник зберігає кількість алокацій мінус кількість deallocations (звільнень) у даному поколінні. Як тільки в Python створюється який-небудь контейнерний об'єкт, він перевіряє ці лічильники. Якщо умови спрацьовують, починається процес збирання сміття.
Якщо одразу кілька або більше поколінь перевищили поріг, вибирається найстарше покоління. Це зроблено через те, що старі покоління також сканують всі попередні. Щоб зменшити кількість пауз збирання сміття для довгоживучих об'єктів, найстарша генерація має додатковий набір умов.
Стандартні пороги спрацювання для поколінь встановлені на значення 700, 10 і 10 відповідно, але завжди можна змінити їх за допомогою функцій gc.get_threshold та gc.set_threshold.
import gc
print(gc.get_threshold()) # (700, 10, 10)
gc.set_threshold(500, 10, 10)
print(gc.get_threshold()) # (500, 10, 10)
GC-паузи і латентність. Запуск збирача - синхронна пауза в тому ж потоці, тож на гарячому шляху він дає сплески latency (p99 помітно вищий за p50). Інструменти тюнінгу для сервісів із жорстким таймаутом: підняти пороги (gc.set_threshold), щоб GC спрацьовував рідше; gc.freeze() - позначити вже наявні довгоживучі об'єкти (завантажені на старті конфіги, кеші) як "заморожені", щоб GC більше їх не сканував - так скорочують роботу кожного проходу; у крайньому разі gc.disable() для критичної ділянки з ручним gc.collect() у спокійний момент. Засіб діагностики такого спайку - трасування (OpenTelemetry/Jaeger), де GC-пауза видно як "дірку" між спанами.
Слабкі посилання (weakref)
Звичайне посилання інкрементує лічильник refcount і не дає об'єкту звільнитися. Модуль weakref створює посилання, яке не збільшує лічильник: щойно зникають усі звичайні посилання, об'єкт знищується, а слабке посилання починає повертати None. Це дозволяє розривати цикли, не покладаючись на періодичний GC - наприклад, дочірній об'єкт тримає слабке посилання на батька, тому взаємний цикл не утворюється взагалі. Типове застосування - кеші (WeakValueDictionary), спостерігачі та зворотні посилання, які не повинні утримувати об'єкт у пам'яті.
Links
- Всё, что нужно знать о сборщике мусора в Python
- CPython Garbage Collection: The Internal Mechanics and Algorithms
25. Інкапсуляція⚑
🔥 44/100 · computer_science/oop.md
Summary
Інкапсуляція - це один з основних принципів об'єктно-орієнтованого програмування (ООП), що дозволяє об'єднати дані та функції в один об'єкт і приховати їх внутрішню реалізацію від зовнішнього світу. Інкапсуляція - приховання реалізації, працюємо з API.
Тобто, інкапсуляція - це обмеження доступу до компонентів об'єкту (методів і змінних). Інкапсуляція робить деякі з компонентів доступними тільки з середини класу.
Інкапсуляція в Python працює тільки на рівні домовленості між програмістами про те, які атрибути являються загальнодоступними, а які - внутрішніми. У Python, інкапсуляцію зазвичай забезпечують за допомогою модифікаторів доступу, таких як public, private та protected.
public- це найбільш видимий рівень доступу, коли змінні і методи можуть бути доступними ззовні класу і за його межами без обмежень.protected- змінні та методи, які починаються з одного символу підкреслення (наприклад,_protected_var), вважаються захищеними: за домовленістю їх не використовують ззовні класу, хоча технічно доступ можливий. Зазвичай застосовують для внутрішньої реалізації та в успадкуванні.private- змінні та методи, які починаються з подвійного символу підкреслення (наприклад,__private_var), вважаються приватними: інтерпретатор застосовує name mangling (__x→_ClassName__x), тож ззовні класу за початковим іменем вони недоступні.
Інкапсуляція дозволяє захистити внутрішню реалізацію класу і приховати змінні та методи, які не повинні бути доступними для зовнішнього використання. Це сприяє створенню більш чистого, безпечного та легкозмінного коду, оскільки змінні можуть змінюватися тільки зсередини класу, а зовнішні користувачі використовують лише визначені публічні методи.
class BankAccount:
def __init__(self, balance):
self.__balance = balance # private (name mangling)
def deposit(self, amount):
self.__balance += amount
def withdraw(self, amount):
if amount <= self.__balance:
self.__balance -= amount
else:
print("Insufficient funds!")
def get_balance(self):
return self.__balance
account = BankAccount(1000)
account.deposit(500) # use public methods to interact with account
account.withdraw(200)
print(account.get_balance()) # 1300
print(account.__balance) # AttributeError: 'BankAccount' object has no attribute '__balance'
26. Оператор та види JOIN⚑
🔥 44/100 · infrastructure/sql.md
Оператор JOIN в SQL використовується для об'єднання даних з двох або більше таблиць на основі визначених умов. JOIN дозволяє комбінувати дані з різних таблиць в один результат.
Умова з'єднання вказується в реченні ON. Ця умова визначає, які рядки двох вихідних таблиць вважаються "відповідними" один одному.
Слова INNER та OUTER не є обов'язковими у всіх формах. За замовчуванням передбачається INNER(внутрішнє з'єднання), а при вказанні LEFT, RIGHT і FULL - зовнішнє з'єднання.
Види джойнів
JOIN(INNER JOIN)LEFT JOIN(LEFT OUTER JOIN)RIGHT JOIN(RIGHT OUTER JOIN)FULL JOIN(FULL OUTER JOIN)-
CROSS JOIN -
INNER JOIN- Повертає ті рядки, які мають відповідні значення в обох таблицях.
SELECT Orders.order_id, Customers.customer_name
FROM Orders
JOIN Customers ON Orders.customer_id = Customers.customer_id;
LEFT OUTER JOIN- Повертає всі рядки з лівої (першої) таблиці та відповідні значення з правої (другої) таблиці. Якщо відповідних значень немає, повертаєтьсяNULL.
SELECT Customers.customer_name, Orders.order_id
FROM Customers
LEFT JOIN Orders ON Customers.customer_id = Orders.customer_id;
RIGHT OUTER JOIN- Повертає всі рядки з правої (другої) таблиці та відповідні значення з лівої (першої) таблиці. Якщо відповідних значень немає, повертаєтьсяNULL. Це з'єднання є протилежним до лівого (LEFT JOIN).
SELECT Customers.customer_name, Orders.order_id
FROM Customers
RIGHT JOIN Orders ON Customers.customer_id = Orders.customer_id;
FULL OUTER JOIN- Повертає всі рядки з обох таблиць, і якщо відповідних значень немає, повертаєтьсяNULL. Фактично це одночаснийLEFTтаRIGHT JOIN.
SELECT Customers.customer_name, Orders.order_id
FROM Customers
FULL JOIN Orders ON Customers.customer_id = Orders.customer_id;
CROSS JOIN- Повертає декартовий добуток (комбінацію) всіх рядків із першої таблиці та всіх рядків із другої таблиці.
Скільки рядків поверне JOIN. Поширене питання на співбесіді: для таблиць розмірів X і Y оцінити межі кількості рядків результату.
INNER JOIN: від0(жодного збігу за умовоюON) доX·Y(усі значення ключових стовпців однакові з обох боків - тоді з'єднання вироджується в декартів добуток).LEFT JOIN: відX(рядки лівої таблиці зберігаються завжди; за відсутності збігу праві стовпці =NULL) доX·Y(той самий вироджений випадок з повним перетином).CROSS JOIN: завжди рівноX·Y- декартів добуток без умови.
Латеральний JOIN, (LATERAL JOIN) дозволяє використовувати результати попереднього запиту як джерело даних для подальших операцій JOIN, що дозволяє створювати більш складні і гнучкі запити.
Основна властивість латерального JOIN полягає в тому, що він дозволяє включати підзапити, які "бачать" дані з рядків, отриманих від зовнішніх таблиць. Це дозволяє використовувати значення з поточного рядка як параметр для підзапиту, що в свою чергу дозволяє гнучко маніпулювати даними.
SELECT e.employee_name, t.task_name
FROM employees e
LEFT JOIN LATERAL (
SELECT task_name
FROM tasks
WHERE tasks.employee_id = e.employee_id
LIMIT 5 -- Limit the number of tasks per employee, for example
) t ON true;
27. Які механізми використовує Python для очищення пам'яті?⚑
🔥 44/100 · python/interpreter.md
Python має 2 основні механізми для очищення пам'яті
- Підрахунок посилань (Reference Counting). Python відстежує кількість активних посилань на об'єкт. Коли лічильник посилань об'єкта зменшується до нуля (тобто на об'єкт більше ніхто не посилається), пам'ять, зайнята цим об'єктом, автоматично звільняється.
- Збирання сміття (Garbage Collection). Для об'єктів, що можуть утворювати циклічні посилання, Python використовує garbage collector, який автоматично видаляє такі об'єкти. Це важливо для структур даних, які можуть мати взаємні посилання, що не дозволяють зменшити лічильник посилань до нуля.
У Python алгоритм підрахунку посилань є фундаментальним і не може бути вимкнений, тоді як GC є необов'язковим і може бути вимкнений.
-
Модуль
gcнадає програмістам інструменти для управління процесом збирання сміття, включаючи можливість примусового запускуgc.collect()для звільнення пам'яті або зміни налаштувань частоти запуску garbage collection. Модуль також дозволяє тимчасово вимкнути автоматичне збирання сміття, якщо є необхідність у мануальному контролі. -
Оптимізація пам'яті. Python має внутрішні механізми кешування, зокрема для малих об'єктів (як, наприклад, малі цілі числа чи короткі рядки), які повторно використовують пам'ять, запобігаючи зайвим витратам.
28. Що таке бази даних і для чого вони потрібні?⚑
🔥 41/100 · infrastructure/database.md
База даних - це організована структура для зберігання та управління інформацією. Вона дозволяє зберігати, організовувати, шукати та забезпечувати доступ до даних з різних джерел та для різних цілей.
Під базою даних зазвичай розуміють систему керування базами даних. Але варто розрізняти базу даних — сховище даних, та СКБД — засоби для роботи з базою даних.
Бази даних служать для
- Збереження різноманітних даних, які можуть бути структурованими.
- Організації даних у логічних структурах, таких як таблиці, колекції ключ-значення.
- Забезпечення швидкого доступу до даних через індекси та оптимізовані запити.
- Забезпечення безпеки даних шляхом управління правами доступу.
- Можливості спільної роботи над даними багатьох користувачів.
- Виконання аналітичних завдань та генерація звітів.
29. Що таке серіалізація⚑
🔥 41/100 · python/files_and_io.md
Серіалізація - це процес збереження об'єктів у бінарному або рядковому вигляді для зберігання, передачі та відновлення. Зворотний процес називається десеріалізацією.
30. Наведіть приклади РБД та NoSQL БД⚑
🔥 40/100 · infrastructure/database.md
Приклади реляційних баз даних (РБД)
- MySQL
- PostgreSQL
- Microsoft SQL Server
- Oracle Database
- SQLite
Приклади баз даних типу NoSQL
- MongoDB
- Орієнтована на документи NoSQL база даних, яка зберігає дані у вигляді документів (наприклад, JSON) з гнучкою схемою.
- MongoDB легко масштабується горизонтально та підтримує індексування даних, що забезпечує швидкий доступ до них.
- Для зберігання інформації використовується тип даних BSON (Binary JSON). BSON – це бінарний формат для представлення JSON-подібних документів. BSON забезпечує більш ефективне зберігання даних, ніж JSON.
- Cassandra
- Ключ-значення база даних, яка підходить для великих обсягів даних та горизонтального масштабування.
- Redis (REmote DIctionary Server)
- Швидка база даних типу ключ-значення, яка використовується для кешування та зберігання швидкозмінних даних.
- Redis швидко обробляє дані завдяки тому, що всі дані зберігаються у пам'яті.
31. Що таке async/await, навіщо вони потрібні і як їх використовувати⚑
🔥 39/100 · python/async.md
Ключове слово async використовується перед def, щоб показати, що функція є асинхронною (корутиною). Тобто, якщо визначити функцію async def f(): ... та викликатиме її як f() — повернеться корутина. Прийшла на зміну декоратору @asyncio.coroutine в Python 3.5+.
Ключове слово await вказує, що очікується завершення співпрограми. await може бути використане лише в співпрограмі. awaitable — все, що підтримує await, тобто корутини, asyncio.Futures, asyncio.Tasks, об'єкти з методом __await__. await прийшов на зміну yield from в Python 3.5+.
import asyncio
import aiohttp
urls = ['https://www.google.com', 'https://www.python.org']
async def call_url(session, url):
print(f'Run {url}')
async with session.get(url) as response:
data = await response.text()
print(f'{url}: {len(data)} bytes')
return data
async def main(urls):
async with aiohttp.ClientSession() as session:
tasks = [call_url(session, url) for url in urls]
results = await asyncio.gather(*tasks)
return results
asyncio.run(main(urls)) # Run the event loop
Програма містить асинхронний метод. Під час виконання він повертає співпрограму, яка потім перебуває у стані очікування.
async/await необхідні для того, щоб не блокувати виконання потоку під час очікування асинхронної події. Конструкція async/await фактично перетворює функцію на корутину (співпрограму): вона призупиняє своє виконання під час await, очікує асинхронної події та продовжує роботу.
Історична еволюція корутин у Python
Перехід від генератор-базованих корутин до нативного синтаксису відбувся не за один крок:
- Python 3.4 - модуль
asyncioдодано до стандартної бібліотеки. Корутини визначалися як генератори, обгорнуті декоратором@asyncio.coroutine; передача керування - черезyield from. - Python 3.5 (PEP 492) - додано нативний синтаксис
async def/await, окремий від генераторів.await- заміна дляyield from, але семантично прив'язана до корутин, а не до загального протоколу ітерації. - Python 3.8 - декоратор
@asyncio.coroutineоголошено застарілим (deprecated) з нотаткою "will be removed in version 3.10". - Python 3.11 -
@asyncio.coroutineостаточно видалено (whatsnew 3.11: "Removed the@asyncio.coroutinedecorator enabling legacy generator-based coroutines to be compatible with async/await code").
Нативні корутини (async def) існують із Python 3.5 і весь цей час були окремою сутністю від генераторів на рівні API. У 3.11 видалили саме legacy-варіант із декоратором @asyncio.coroutine, а не нативну реалізацію. Внутрішні CPython-механізми (паузи, збереження стану кадру) історично спільні з генераторами, проте з точки зору API async def ніколи не була generator-based.
32. Партиціювання та шардування⚑
🔥 38/100 · infrastructure/database.md
Партиціювання та шардування - це техніки, які дозволяють розбити велику базу даних на менші частини, щоб забезпечити більшу масштабованість та продуктивність.
Партиціювання (Partitioning) - це процес розділення таблиці так, що її дані розділяються на менші, більш керовані та ефективно оброблювані частини, які називаються "партиціями". Кожна партиція може бути розміщена на окремому диску або файловій системі, що дозволяє базі даних обробляти запити швидше, оскільки кожна партиція може бути оброблена незалежно від інших, отже обробляється менше даних. Також, можна створювати індекси на окремих партиціях, що полегшує оптимізацію запитів.
Партиціювання включає в себе наступні кроки
- Визначення критеріїв партиціювання: це може бути дата, час, ідентифікатор або будь-який інший атрибут, що дозволяє логічно розділити дані.
- Створення партицій: кожна партиція створюється відповідно до визначених критеріїв.
- Розподіл даних: дані переносяться в відповідні партиції.
Шардування (Sharding) - це процес розділення таблиці на менші фрагменти - шарди, які можуть бути збережені на різних серверах. Однак, на відміну від партиціювання, шардування зазвичай використовується для розділення даних на різних серверах у різних локаціях. Це дозволяє збільшити доступність та надійність бази даних, оскільки в разі відмови одного сервера, інші сервери можуть продовжувати працювати.
Шардування буває вертикальне і горизонтальне
- Горизонтальне шардування відбувається, коли рядки даних розподіляються між різними базами даних (шардами). Кожен шард містить унікальний набір рядків. Наприклад, один шард може містити дані про користувачів з іменами, що починаються на літери A-M, а інший шард - про користувачів з іменами на N-Z.
- Вертикальне шардування відбувається, коли стовпці даних розподіляються між різними базами даних. Кожен шард містить унікальний набір стовпців. Наприклад, один шард може містити контактну інформацію користувача, а інший - інформацію про замовлення цього користувача.
Проблема гарячого ключа (hot key / celebrity problem). Якщо ключ шардування дає нерівномірний розподіл навантаження, один шард перевантажується, тоді як інші простоюють. Класичний приклад - "знаменитості" в соцмережі: шардування за user_id рівномірно розкидає самих користувачів, але запити на читання стрічки кількох акаунтів-мільйонників б'ють в один шард. Ключ обирають за патерном доступу, а не лише за рівномірністю самих рядків; для гарячих сутностей застосовують окреме кешування, реплікацію читань або додаткове розбиття гарячого ключа.
33. В чому відмінність потоків від процесів⚑
🔥 38/100 · python/gil_threads_processes.md
Потоки історично з'явились пізніше процесів, і в Linux вони реалізовані однією і тією ж структурою даних, відомою як "таск" (Task). Для ядра Лінуксу немає різниці між потоком і процесом - потоки та процеси розглядаються як завдання (tasks). Тобто для створення того та іншого використовується єдина структура даних, де відмінність тільки в тому, що у треда буде встановлений TID, та декілька інших полей, які залежать від аргументів переданих до fork/clone викликів. CFS (Completely Fair Scheduler) та новий EEVDF (Earliest Eligible Virtual Deadline First) — планувальники, які використовуються для керування розподілом процесорного часу між завданнями в операційній системі - сприймають процеси та треди просто як "таски", тобто як абстракції завдань, тобто одиниці роботи, що потребують часу процесора. Вони не розрізняють тип цих завдань, а лише розподіляють час між ними залежно від заданих правил і алгоритмів.
Основна відмінність полягає у розділенні пам'яті. Процеси є незалежними один від одного, мають окремі адресні простори, ідентифікатори та ресурси. Потоки виконуються у спільному адресному просторі, мають спільний доступ до пам'яті, змінних, завантажених модулів. Можна визначити змінну, і всі потоки її можуть бачити. А процеси такої можливості позбавлені та нам потрібно процеси якось зв'язувати.
Створення та знищення процесів може бути важким процесом, оскільки вони вимагають виділення окремих ресурсів. IPC (Inter-Process Communication) — це механізм, який дозволяє процесам взаємодіяти між собою, обмінюватися даними та координувати виконання.
Таким чином потоки є легшими, між ними швидше перемикається контекст.
Links
34. Що таке ООП? Яка відмінність від процедурного?⚑
🔥 37/100 · computer_science/oop.md
Об'єктно-орієнтоване програмування (ООП) - це парадигма програмування, яка базується на концепції "об'єктів". Основними принципами ООП є ідея поділу програми на об'єкти, які взаємодіють один з одним, та спрощення розробки та обслуговування програм завдяки модульності і розподіленій відповідальності. ООП корисний тоді, коли у об'єктів є стан.
Кожен об'єкт групує заданий набір інформації (властивостей) та дій (методів), які може виконувати цей об'єкт.
Основні поняття ООП включають
- Класи і об'єкти (Classes and Objects): Клас - це шаблон або опис об'єкта, який визначає його властивості та методи. Об'єкт - це конкретний екземпляр класу, який може мати свої унікальні значення властивостей.
- Інкапсуляція (Encapsulation): Інкапсуляція визначає, що властивості та методи об'єкта повинні бути збережені разом та захищені від зовнішнього доступу. Це допомагає управляти доступом до даних і методів.
- Наслідування (Inheritance): Наслідування дозволяє створювати новий клас на основі існуючого класу, наслідуючи його властивості та методи. Це спрощує створення нових класів та перевикористання коду.
- Поліморфізм (Polymorphism): Поліморфізм дозволяє об'єктам різних класів використовувати однаковий інтерфейс для виконання різних операцій. Це спрощує роботу з різнорідними об'єктами.
На відміну від ООП, в процедурному програмуванні код організований навколо послідовного виконання функцій. Функції отримують дані, обробляють їх та повертають результат.
35. Master-Slave⚑
🔥 37/100 · infrastructure/database.md
Архітектура Master-Slave (тепер часто називається Primary-Replica, щоб уникнути застарілих термінів) використовується для горизонтального масштабування баз даних і забезпечення високої доступності. У цій моделі одна база даних виступає як Master, а інші як Slave. Використовується для підвищення продуктивності, коли операції читання значно переважають операції запису, або для забезпечення резервування даних. Ця архітектура є популярною для додатків із високими вимогами до масштабованості з великою кількістю запитів на читання.
Master (Primary) - основна база даних, яка приймає всі операції запису. Зміни, внесені до Master, реплікуються (передаються) до всіх Slave.
Slave (Replica) - це одна або більше копій Master, які синхронізуються через реплікацію. Slave обслуговують лише операції читання, що дозволяє розподілити навантаження на систему.
Реплікація може бути синхронною (запис на Master завершується тільки після оновлення Slave) або асинхронною (Master завершує операцію запису, не чекаючи синхронізації з Slave).
Переваги
- Розвантаження Master - операції читання перенаправляються на Slave, що зменшує навантаження на Master.
- Масштабованість - легко додати нові Slave для обробки більшого обсягу запитів на читання.
- Резервування даних - у разі відмови Master, один із Slave може бути підвищений до Master.
Недоліки
- Затримка реплікації - у разі асинхронної реплікації можуть виникати затримки, що призводять до читання застарілих даних зі Slave.
- Складність управління - підтримка синхронізації між Master і Slave потребує додаткових ресурсів і інструментів.
- Відсутність автоматичного переходу на новий Master - у разі відмови Master зазвичай потрібне ручне втручання для призначення нового Master.
36. EXPLAIN та EXPLAIN ANALYZE⚑
🔥 37/100 · infrastructure/sql.md
EXPLAIN - це команда, яка надає інформацію про план виконання запиту. Вона показує, як база даних планує виконати запит, включаючи індекси, порядок виконання операцій та очікувані витрати, що допомагає в оптимізації запитів. Сам запит у разі не виконується.
EXPLAIN ANALYZE виконує пояснюваний вираз, навіть якщо це insert, update або delete.
Приклад використання команди EXPLAIN
EXPLAIN SELECT * FROM employees WHERE age > 30;
Seq Scan on employees (cost=0.00..12.50 rows=3 width=100)
Filter: (age > 30)
База даних буде виконувати послідовне сканування (Seq Scan) таблиці employees і застосовувати фільтр age > 30. Поле cost показує передбачувані витрати виконання операції термінах часу і ресурсів.
EXPLAIN допомагає виявити потенційні проблеми у запитах, такі як відсутність індексного сканування, що може вказувати на необхідність додавання індексів або переписування запитів для покращення продуктивності.
Використання EXPLAIN особливо корисне при роботі зі складними запитами або великими обсягами даних, оскільки дозволяє заздалегідь зрозуміти, як виконуватиметься запит, і які оптимізації можуть знадобитися.
37. Чи можливе множинне наслідування⚑
🔥 37/100 · python/class_and_object.md
Так, в класах в Python можна вказати більше одного батька для похідного класу. На відміну від, наприклад, Java.
38. Чому використання змінних об'єктів як параметрів за замовчуванням погана практика⚑
🔥 37/100 · python/functions.md
Функція створюється один раз під час завантаження модуля. Іменовані параметри і їх значення за замовчуванням також створюються один раз і зберігаються в одному з полів об'єкта-функції (__defaults__). Це стосується списків, множин і словників.
У даному прикладі bar має значення порожнього списку. Список - це змінний об'єкт, тому значення bar може змінюватися від виклику до виклику.
def foo(bar=[]):
bar.append(1)
return bar
foo()
>>> [1]
foo()
[1, 1]
foo()
>>> [1, 1, 1]
foo.__defaults__
>>> ([1, 1, 1],)
Хорошим тоном вважається вказувати для параметра пусте незмінне значення, наприклад 0, None, '', False. В тілі функції перевіряти на False/None і створювати нову колекцію:
def foo(bar=None):
if bar is None:
bar = []
bar.append(1)
return bar
foo()
>>> [1]
foo()
>>> [1]
foo()
>>> [1]
foo.__defaults__
>>> (None,)
39. S - Single Responsibility Principle⚑
🔥 36/100 · computer_science/solid.md
Summary
У модуля - одна причина для зміни: він відповідає перед одним актором (групою стейкхолдерів), а те, що змінюється з різних причин, розносять окремо.
S - Single Responsibility Principle - SRP - Принцип єдиної відповідальность Кожен клас повинен мати лише одну відповідальність, вирішувати тільки одне завдання. Це означає, що клас має бути створений для виконання лише однієї задачі, яку він повинен повністю інкапсулювати. Отже, всі сервіси цього класу мають бути підпорядковані її виконанню. Результатом слідування цій концепції є наявність лише однієї причини для зміни класу.
Уточнення Мартіна. Формулювання "одна причина для зміни" Мартін згодом визнав таким, що часто розуміють хибно - нібито "клас має робити одну дію". Точніше: модуль має бути відповідальним перед одним актором (групою/роллю стейкхолдерів, яка ініціює зміни). Тобто збирають разом те, що змінюється з одних причин, і розділяють те, що змінюється з різних. SRP - не про "один метод", а про один напрям змін.
Переваги та недоліки
- Протидіє дублюванню коду, адже якщо функціональність розташована в неправильному місці, то доведеться копіювати її в потрібне
- Зменшує потребу зміни вже випробуваного коду
- Забезпечує відповідність назв класів та їх функціональності, що полегшує життя тим, хто обслуговуватиме код в майбутньому
- Мінус - зростання кількості класів, що призводить до зростання складності системи.
Реалізація - розділити більші класи на менші. Наприклад - є клас, який створює звіт і друкує звіт. Такий клас може змінитись через 2 причини - може змінитись сам звіт або формат друку. Тому цей клас треба розділити на 2 нових.
Антипатерном до цього принципу є Божественний клас (God object) - коли один виконує дуже багато всього. І тоді з'являється ефект сніжного кому - зміна в одному місці викликає зміну в багатьох. Це також порушує GRASP-принципи High Cohesion і Low Coupling: незв'язані обов'язки зібрані в одному класі, від якого до того ж залежить багато коду.
SRP та ідея повторного використання коду тісно пов’язані з ідеєю зчеплення (cohesion) в розробці програмного забезпечення. Потрібно прагнути досягти того, щоб класи були розроблені таким чином, щоб більшість їхніх властивостей і атрибутів використовувалися їхніми методами більшу частину часу. Та повинні мати мінімальний рівень відповідальності: виконувати одну задачу, лише одну, і виконувати її добре. Коли це трапляється, ми знаємо, що це пов'язані концепції, і тому має сенс об'єднати їх під одну абстракцію. Чим менші за розміром компоненти, тим вони більш універсальні, і тим легше їх застосувати в іншому контексті без перенесення зайвої поведінки, що спричиняє зв'язування (coupling) та залежності, які роблять програмне забезпечення негнучким.
Функціями чи класами. SRP не диктує форму: розділити відповідальності можна і на функції + dataclass-и + модулі, і на класи - єдиного стандарту немає. Якщо дія не має стану, функція уникає одно-метод-класу; якщо є стан, конфігурація чи поліморфізм - доречніший клас. Приклад розділення функціями:
from dataclasses import dataclass
@dataclass
class ReportData:
user_id: int
total_sales: float
def fetch_user_sales(db, user_id: int) -> list[dict]: # db.py - data layer
return db.execute(...)
def build_report(user_id: int, sales: list[dict]) -> ReportData: # analytics.py - logic
return ReportData(user_id, sum(s["price"] for s in sales))
def save_report_csv(report: ReportData, path: str) -> None: # export.py - I/O
...
Щоб додати експорт у JSON, пишуть нову функцію save_report_json, не чіпаючи розрахунки чи запити до БД. Тестувати теж легше - у функцію розрахунку передають мок-список, не піднімаючи тестову базу.
Links
- https://www.pythontutorial.net/python-oop/python-single-responsibility-principle/
- Все, що ви хотіли знати про принципи SOLID. Частина перша: SRP - dou.ua
40. Які готові реалізації MQ ви знаєте⚑
🔥 36/100 · infrastructure/mq.md
- RabbitMQ: Це відкрите програмне забезпечення, яке реалізує стандартизований протокол AMQP (Advanced Message Queuing Protocol). Він дозволяє надійно та масштабовано обмінюватися повідомленнями між різними системами та компонентами додатків.
- Apache Kafka: Це розподілена платформа для потокової обробки та зберігання даних. Він спроектований для обробки великої кількості подій (повідомлень) та забезпечує гарантовану доставку та зберігання цих подій.
- Redis: Redis може бути використаний як MQ за допомогою свого механізму публікації-підписки (pub-sub) та черги повідомлень (list). Він швидкий та легкий у використанні.
- Apache ActiveMQ: Це ще одна реалізація протоколу AMQP. Він надає різноманітні можливості обробки повідомлень, включаючи публікацію-підписку, черги повідомлень та багато іншого.
- ZeroMQ (0MQ): Це бібліотека для передачі повідомлень між процесами через різні протоколи. Вона дозволяє вам побудувати власні механізми обміну повідомленнями, а також підходить для складних топологій зв'язку.
- Apache RocketMQ: Це система повідомлень, яка забезпечує надійну та масштабовану передачу повідомлень між різними компонентами додатків.
Links
- Message middleware deployment and comparison: rabbitMQ, activeMQ, zeroMQ, rocketMQ, Kafka, redis
- RabbitMQ против Kafka: два разных подхода к обмену сообщениями
- Kafka VS RabbitMQ
- Выбор MQ для высоконагруженного проекта
41. Метакласи⚑
🔥 36/100 · python/metaclass.md
Summary
Метаклас - клас, який створює класи і визначає поведінку класів. Вони дозволяють контролювати процес створення класу, модифікувати його атрибути, методи та поведінку.
Метакласи працюють шляхом перехоплення створення класу на момент його визначення. При визначенні класу інтерпретатор Python шукає метаклас у його базових класах і, якщо знаходить, використовує його для створення нового класу.
Один з основних способів використання метакласів полягає в реалізації паттерну проектування "Одинак" (Singleton), де метаклас може гарантувати, що від класу може бути створений лише один екземпляр. Метакласи також використовуються в фреймворках, таких як Django і SQLAlchemy, для створення моделей бази даних.
У Python 3 метаклас задається ключовим аргументом metaclass= у заголовку класу (class MyClass(metaclass=Registry): ...). Атрибут __metaclass__ - застарілий механізм Python 2; у Python 3 він ігнорується.
Створення класу з автоматичною реєстрацією у системі
class Registry(type):
def __init__(cls, name, bases, attrs):
super().__init__(name, bases, attrs)
register(cls)
class MyClass(metaclass=Registry):
pass
Links
42. Django ORM⚑
🔥 36/100 · python_framework/django.md
Django ORM (Object-Relational Mapping) — це компонент Django, який дозволяє взаємодіяти з базою даних, використовуючи об'єктно-орієнтовані моделі, замість написання сирих SQL-запитів. Він автоматично перетворює Python-код у SQL-запити, роблячи роботу з базами даних зручнішою і більш безпечною.
Основні переваги Django ORM
- Автоматизація створення таблиць і запитів.
- Захист від SQL-ін'єкцій.
- Зручний синтаксис для роботи з базою даних.
- Можливість гнучкої побудови складних запитів.
- Підтримка різних типів баз даних: PostgreSQL, MySQL, SQLite, Oracle тощо.
Основні компоненти Django ORM
- Моделі (Models)
- Моделі є основою ORM і визначають структуру таблиць у базі даних.
- Кожна модель в Django — це Python-клас, який успадковує
models.Model. - Поля класу відповідають стовпцям таблиці бази даних.
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
-
Поля (Fields)
- Кожне поле моделі (
CharField,IntegerField,DateTimeField, тощо) визначає тип даних у відповідному стовпці бази даних. - Можна додавати валідатори, налаштовувати значення за замовчуванням, встановлювати унікальність та інші параметри.
- Кожне поле моделі (
-
Менеджер об’єктів (QuerySet API)
objects— це менеджер, за допомогою якого виконується доступ до записів у таблиці.- За допомогою менеджера можна створювати, читати, оновлювати та видаляти дані.
Product.objects.create(name="Laptop", price=1200.00) # Create object
products = Product.objects.all() # Getting all records
laptops = Product.objects.filter(name__icontains="laptop") # Filtering
-
Фільтри (Filters)
- Django ORM надає різноманітні методи для створення запитів:
filter(),exclude(),get(). - Використовуються спеціальні ключі (наприклад,
__icontains,__gte), які визначають умови.
- Django ORM надає різноманітні методи для створення запитів:
-
Відношення (Relationships)
- Django підтримує зв'язки між моделями:
ForeignKey,OneToOneField,ManyToManyField. - Це дозволяє створювати зв’язки між таблицями в базі даних.
- Django підтримує зв'язки між моделями:
class Category(models.Model):
name = models.CharField(max_length=100)
class Product(models.Model):
name = models.CharField(max_length=100)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
- Міграції (Migrations)
- Міграції — це механізм, що дозволяє перетворювати зміни у моделях на SQL-команди для синхронізації структури бази даних.
- Генерація та застосування міграцій
- Сигнали (Signals)
- Django дозволяє виконувати дії перед або після певних подій у моделях (наприклад, збереження, видалення записів).
- Наприклад,
pre_saveабоpost_delete.
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=Product)
def notify_new_product(sender, instance, created, **kwargs):
if created:
print(f"New product added: {instance.name}")
- Транзакції (Transactions)
- Django ORM підтримує атомарні операції через
transaction.atomic(), що дозволяє виконувати кілька операцій як одну.
- Django ORM підтримує атомарні операції через
from django.db import transaction
with transaction.atomic():
category = Category.objects.create(name="Electronics")
Product.objects.create(name="Smartphone", category=category)
- Raw SQL
- Якщо потрібно виконати нестандартний SQL-запит, ORM дозволяє це зробити через метод
raw(), черезRawSQL-вираз для вбудування шматка у звичайний ORM-запит, або черезconnection.cursor()для повністю довільного SQL.
- Якщо потрібно виконати нестандартний SQL-запит, ORM дозволяє це зробити через метод
Manager.raw(sql, params) повертає RawQuerySet - lazy, як звичайний QuerySet. Параметри обов'язково передавати через placeholder (%s для більшості бекендів, ? для SQLite) - ніколи не інтерполювати через f-string чи %-формат, інакше відкривається SQL injection.
# Safe: parameterized
Product.objects.raw("SELECT * FROM app_product WHERE name = %s", [user_input])
# UNSAFE: never do this
Product.objects.raw(f"SELECT * FROM app_product WHERE name = '{user_input}'")
raw() мапить колонки на атрибути моделі за іменами; колонки без відповідного поля можна додати як RawQuerySet[i].extra_attr. Якщо моделі не існує - тоді connection.cursor() з cursor.execute(sql, params) повертає dict/tuple-рядки:
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("SELECT id, name FROM app_product WHERE price > %s", [1000])
rows = cursor.fetchall() # List of tuples
Для вбудування невеликого SQL-фрагмента у звичайний ORM-запит - RawSQL-вираз:
from django.db.models.expressions import RawSQL
products = Product.objects.annotate(
discount=RawSQL("price * %s", [0.1]), # Inline SQL fragment in annotate
)
43. Що таке функція у Python⚑
🔥 35/100 · python/functions.md
Summary
Функція - це блок коду, який виконується лише тоді, коли його викликають. Функції використовуються для організації коду та повторного використовуваним. Функцію оголошують за допомогою ключового слова
def, за яким слідує ім'я функції та параметри у круглих дужках.
Тіло функції - це блок коду, який виконується при виклику функції. Він може містити різні операції та вказівки. Функція може повертати значення за допомогою ключового слова return. Це значення може бути використане у коді, який викликав функцію.
Для виклику функції використовується її ім'я та передаються аргументи у круглих дужках. Параметри - це значення, які функція отримує при виклику. Аргументи - це конкретні значення, передані функції при її виклику.
Параметри завжди передаються однаково - присвоєнням імені тому самому об'єкту (call by object reference); копій при виклику не створюється. Різниця лише в наслідках:
- незмінний об'єкт (int, str, tuple) неможливо змінити на місці, тож зміни всередині функції його не зачіпають - виглядає як "по значенню"
- змінний об'єкт (list, dict) можна змінити на місці, і ці зміни видно в коді, що викликав функцію - виглядає як "по посиланню"
В Python функція - це об'єкт. Все що є за межами функцій і класів - глобальні змінні.
44. Методи HTTP⚑
🔥 35/100 · web_development/http.md
Методи HTTP поділяються на кілька основних груп, включаючи ідемпотентні та неідемпотентні методи.
Ідемпотентність — це властивість операцій, яка означає, що багаторазове виконання однієї і тієї ж операції призведе до того самого результату, що і перше її виконання. Незалежно від того, скільки разів операція буде викликана, результат залишатиметься незмінним.
У контексті HTTP ідемпотентність важлива для забезпечення надійності запитів. Наприклад, якщо під час виконання операції мережа зазнала тимчасової проблеми, клієнт може безпечно повторити запит, не ризикуючи створити додаткові або небажані зміни на сервері.
Ідемпотентні методи HTTP
- GET - використовується для отримання інформації з сервера та не має побічних ефектів на дані на сервері.
- HEAD - подібний до GET, але повертає тільки заголовки без тіла відповіді.
- OPTIONS - використовується для запиту інформації про можливості сервера або ресурсу. Він дозволяє клієнту дізнатися, які HTTP-методи, заголовки та інші можливості підтримуються сервером для конкретного ресурсу без виконання реального запиту, що зменшує навантаження на сервер.
- PUT- використовується для зміни або оновлення існуючих ресурсів на сервері.
- DELETE - використовується для видалення ресурсів на сервері. Повторні виклики DELETE для того ж ресурсу не мають ефекту.
Неідемпотентні методи HTTP
- POST - використовується для відправки даних на сервер для створення нового ресурсу або виконання операцій, які можуть призвести до зміни стану сервера кожного разу, коли він викликається. POST не є ідемпотентним, оскільки два однакових POST-запити можуть мати різні наслідки.
- PATCH - використовується для зміни або оновлення частини ресурсу на сервері, але не завжди є ідемпотентним. Результат може залежати від попереднього стану ресурсу.
45. Що таке REST?⚑
🔥 35/100 · web_development/rest_soap.md
Summary
REST (Representational State Transfer, "передача стану представлення") - це архітектурний стиль для розподілених систем. Іншими словами - це набір правил того, як програмісту організувати написання коду серверної програми, щоб усі системи легко обмінювалися даними і програму можна було масштабувати. Під REST часто розуміють так звані HTTP REST API. Зазвичай це веб-додаток з набором URL-адрес - кінцевих точок. URL-адреси приймають та повертають дані у форматі JSON. REST є альтернативою RPC.
REST не накладає правил на реалізацію на низькому рівні, а лише надає високорівневі рекомендації та залишає вам волю для власної реалізації.
Основні поняття REST
- Ресурс - будь-який об’єкт або дані, з якими взаємодіє клієнт через сервер. Ресурс може бути будь-чим: користувач, файл, замовлення, продукт тощо. У REST API ресурси ідентифікуються через унікальні URL-адреси (Uniform Resource Locator), тобто посилання.
- Посилання - URL (уніфікований локатор ресурсу) є унікальним ідентифікатором ресурсу в REST API. Він слугує як шлях до конкретного ресурсу або його колекції. URL має бути інтуїтивно зрозумілим і відображати структуру та ієрархію ресурсів.
/users— колекція всіх користувачів/users/123— конкретний користувач із ідентифікатором 123
- Версіонування - можливість підтримки кількох версій сервісу для різних клієнтів або застосунків. Можливі варіанти версіонування
- версія у URL -
GET /v1/users,GET /v2/users - версія у заголовках -
Accept: application/vnd.myapi.v1+json - версія у параметрах запиту -
GET /users?version=1
- версія у URL -
- HATEOAS (Hypermedia as the Engine of Application State) — використання гіпермедіа для управління станом застосунку. Гіпермедіа тут означає використання гіперпосилань у відповідях API для надання клієнту можливості переходити між різними станами або ресурсами системи.
REST API будується на кількох ключових принципах. Якщо ці принципи дотримані, інтерфейс сервісу можу називатися RESTful.
Основні принципи REST
- Stateless -Відсутність стану - кожен HTTP-запит відбувається в повній ізоляції
- Client-Server - Клієнт-сервер - автономність клієнта і сервера та їх взаємодія
- Uniform Interface - Однорідний інтерфейс - підтримка однорідного інтерфейсу
- Cacheable - Кешування - підтримка кешування
- Layered System - Шари абстракції - поділ на шари
- Code on Demand - Код на запит - необов’язкове обмеження, що дозволяє завантажувати код клієнта
Операції визначаються HTTP-методами, тому не варто використовувати дієслова в URL. Метод запиту HTTP визначає тип операції
GET- отримати об'єкт або список об'єктівPOST- створити об'єктPUT- оновити існуючий об'єктPATCH- частково оновити існуючий об'єктDELETE- видалити об'єктHEAD- отримати метадані об'єкта
REST-архітектура активно використовує можливості протоколу HTTP, щоб уникнути власних рішень. Наприклад, параметри кешування передаються стандартними заголовками Cache, If-Modified-Since, ETag. Аутентифікація здійснюється за допомогою заголовка Authentication.
Links
46. Абстракція⚑
🔥 34/100 · computer_science/oop.md
Summary
Абстракція - відокремлення деталей з метою отримання можливості зосередитись на найважливіших особливостях об'єкту. Або це використання тільки тих характеристик об'єкту, які з достатньою точністю представляють його в системі.
У ООП, абстракція зазвичай досягається за допомогою створення абстрактних класів або інтерфейсів, які описують специфікацію поведінки об'єктів, але не надають конкретної реалізації. Абстрактні класи містять абстрактні методи, які мають бути перевизначені в дочірніх класах. Інтерфейси, у свою чергу, описують набір методів, які мають бути реалізовані в класах, які їх імплементують.
Простим прикладом абстракції може бути клас Shape, який представляє геометричну фігуру. Абстрактний клас містить абстрактний метод area, який об'явлений, але не має реалізації:
Дочірні класи, наприклад, Circle та Rectangle, успадковують Shape і повинні перевизначити метод area згідно своїх потреб:
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
За допомогою абстракції, ми можемо робити узагальнені операції над об'єктами типу Shape, не знаючи конкретних деталей реалізації. Наприклад:
def print_area(shape):
print(f"The area of the shape is: {shape.area()}")
circle = Circle(5)
rectangle = Rectangle(4, 6)
print_area(circle) # Output: The area of the shape is: 78.5
print_area(rectangle) # Output: The area of the shape is: 24
47. Методи. @staticmethod, @classmethod⚑
🔥 34/100 · python/class_and_object.md
Метод - це функція, яка викликається від інстансу класу. Метод зберігаються в класі, а не інстансі. Використовує параметр self, який представляє екземпляр класу.
class MyClass:
def instance_method(self):
return f"Instance method called. Attribute: {self.attribute}"
obj = MyClass()
obj.attribute = 5
print(obj.instance_method()) # Output: Instance method called. Attribute: 5
Оскільки функції реалізують протокол дескриптора (реалізують метод __get__), перед викликом методу спочатку викликається метод __get__(). Це перетворює функцію на метод, що означає прив'язку викликаного до екземпляра об'єкту, з яким він збирається працювати.
Це можна імітувати за допомогою MethodType із модуля types. Перший параметр цього класу має бути callable, а другий — це об'єкт, до якого прив'язується ця функція. Щось подібне до цього використовують об'єкти функцій у Python, щоб вони могли працювати як методи, коли їх визначають всередині класу. У цьому прикладі абстракція MyClass намагається імітувати об'єкт функції, оскільки в реальному інтерпретаторі це реалізовано на C.
from types import MethodType
class Method:
def __init__(self, name):
self.name = name
def __call__(self, instance, arg1, arg2):
print(f"{self.name}: {instance} called with {arg1} and {arg2}")
def __get__(self, instance, owner):
if instance is None:
return self
return MethodType(self, instance)
3 типи методів
- метод інстансу - тільки від інстансу. Якщо від класу - помилка. Перший параметр - інстанс
self. Цей метод має доступ до атрибутів і методів екземпляра. - метод класу
@classmethod- першим параметром приймає класcls. Може викликатись як від інстансу, так і від класу. - статичний метод
@staticmethod
І @staticmethod, і @classmethod реалізовані за допомогою дескриптора.
@staticmethod Статичний метод не використовує self або cls. Він не має доступу до атрибутів екземпляра або класу. Це по суті функція всередині класу.
Функція __get__ забезпечує, що жодні параметри не прив'язуються, окрім тих, які визначені самою функцією. Іншими словами, вона скасовує прив'язку, здійснену методом __get__() для функцій, які роблять self першим параметром цієї функції.
Коли використовувати:
- Коли метод не потребує доступу до екземпляра або класу.
- Коли метод логічно зв'язаний з класом, але не використовує його атрибути або методи.
class MyClass:
@staticmethod
def static_method():
return "Static method called"
print(MyClass.static_method()) # Output: Static method called
@classmethod Метод класу приймає параметр cls, який представляє сам клас. Це дозволяє методам класу доступ до атрибутів і методів класу.
Функція __get__ у дескрипторі забезпечує, що клас передається як перший параметр до функції, яка декорується, незалежно від того, викликається вона безпосередньо з класу чи через екземпляр.
Коли використовувати:
- Коли метод повинен працювати з класом як з об'єктом.
- Коли потрібно створити метод, що працює з загальними для всіх екземплярів даними.
class MyClass:
class_attribute = "Class attribute"
@classmethod
def class_method(cls):
return f"Class method called. Attribute: {cls.class_attribute}"
print(MyClass.class_method()) # Output: Class method called. Attribute: Class attribute
48. Які типи даних є в Python⚑
🔥 34/100 · python/data_types.md
Примітивні типи даних:
- Цілі числа (int)
- Python не має обмеження по значенню int
- Числа від -5 до 256 кешуються при старті інтерпретатора CPython (станом на 3.14; у 3.15 діапазон розширили до -5..1024): якщо створити ціле число в цьому діапазоні, воно посилатиметься на той самий об'єкт у пам'яті
- Дійсні числа (float) - числа з плаваючою комою
- Комплексні числа (complex)
- Рядки (str)
- Булеві значення (bool) - True, False
- None - тип, який вказує на відсутність значення.
Кешування малих цілих (int) у CPython:
a = 1
b = 1
a is b # True - values in range -5..256 are cached (small-int cache)
a = 300
b = 300
a is b # False - outside the cache range
a == b # True
Колекції (містять в собі інші об'єкти):
- Списки (list)
- Кортежі (tuple)
- Словники (dict)
- Множини (set)
- Фрозенсет (frozenset) - аналог множини, але не змінюється після створення
Також до примітивних типів ще відносять:
- Байти (bytes) - незмінний (immutable) тип даних, який використовується для представлення послідовності байтів -
b'hello' - Байтові масиви (bytearray) - подібно до байтів, але можуть бути змінюваними -
bytearray(b'hello')
Також типи даних діляться на mutable and immutable:
- Mutable - list, dict, set
- Immutable - int, string, tuple, frozenset, boolean, NoneType
49. Що таке лямбди. Які їх особливості⚑
🔥 34/100 · python/functions.md
Лямбда (lambda) - це анонімна функція, яка не резервує ім'я в просторі імен.
Лямбда може приймати необмежену кількість аргументів та мати будь-яку кількість параметрів. Однак лямбда-функція може мати лише один вираз або інструкцію, і вона завжди повертає результат цього виразу.
Лямбди використовується в ситуаціях, коли потрібна анонімна функція на короткий період часу. Наприклад передати в map, reduce, filter, або для сортування.
У Python лямбди можуть складатися лише з одного виразу. Використовуючи синтаксис дужок, можна оформити тіло лямбди у декілька рядків.
Не можна використовувати крапку з комою для розділення операторів.
a = lambda x, y: x + y
print(a(5, 6)) # 11
students = [{"name": "Alice", "age": 25}, {"name": "Bob", "age": 20}]
sorted(students, key=lambda x: x["age"]) # [{"name": "Bob", "age": 20}, {"name": "Alice", "age": 25}]
Наступні вирази при завантаженні модуля викличуть SyntaxError. Тіло лямбди може містити лише вирази. pass і raise є операторами.
Пастка пізнього зв'язування (late binding) у циклі
Класична особливість лямбд у циклі: лямбда - це замикання, а замикання захоплює саму змінну (за іменем), а не її значення на момент створення. Тому всі лямбди, створені в циклі, бачать останнє значення змінної циклу, а не те, що було на відповідній ітерації:
Усі три лямбди посилаються на ту саму змінну i, яка після циклу дорівнює 2. Щоб зафіксувати значення кожної ітерації, його прив'язують через аргумент за замовчуванням - він обчислюється в момент визначення функції:
Це той самий механізм захоплення за посиланням, що й у секції про замикання (Closure) нижче, - тут він лише найчастіше проявляється з лямбдами.
50. Види дескрипторів⚑
🔥 33/100 · python/class_and_object.md
Є два види дескрипторів
- дескриптор даних
- дескриптор не даних
Якщо об’єкт визначає __set__() або __delete__(), він вважається дескриптором даних. Дескриптори, які визначають лише __get__(), називаються дескрипторами не даних (вони часто використовуються для методів, але можливі й інші способи використання).
Функції в Python являються дескрипторами (дескрипторами не даних) - так як реалізовують __get__()
Дескриптори даних і не даних відрізняються тим, як обчислюються перевизначення щодо записів у словнику екземпляра. Якщо в словнику екземпляра є запис із таким самим іменем, як і дескриптор даних, дескриптор даних має пріоритет. Якщо в словнику екземпляра є запис із таким же ім’ям, що й дескриптор не даних, пріоритет має словниковий запис.
Щоб створити дескриптор даних лише для читання, визначте __get__() і __set__() з __set__(), що викликає AttributeError під час виклику. Щоб зробити його дескриптором даних, достатньо визначити метод __set__(), що викликає винятки.
51. Які магічні методи відповідають за перетворення типів?⚑
🔥 33/100 · python/class_and_object.md
Summary
Кожна вбудована функція-конвертер шукає у класі відповідний дандр-метод:
int()→__int__,float()→__float__,str()→__str__,bool()→__bool__,bytes()→__bytes__,complex()→__complex__,repr()→__repr__.
Коли викликаємо int(obj), Python шукає у класі obj метод __int__ і повертає його результат. Аналогічно для решти. Це дозволяє кастомним класам поводитися як числа, рядки чи булеві значення в контекстах, де очікується відповідний тип.
| Виклик | Магічний метод | Призначення |
|---|---|---|
int(obj) | __int__(self) | перетворення на ціле число |
float(obj) | __float__(self) | перетворення на число з рухомою крапкою |
complex(obj) | __complex__(self) | перетворення на комплексне число |
bool(obj) | __bool__(self) | перетворення на булеве значення |
str(obj) | __str__(self) | "людське" рядкове представлення |
repr(obj) | __repr__(self) | "девелоперське" рядкове представлення |
bytes(obj) | __bytes__(self) | перетворення на байтову послідовність |
class Money:
def __init__(self, value):
self.value = value
def __int__(self):
return int(self.value)
def __float__(self):
return float(self.value)
def __str__(self):
return f"${self.value:.2f}"
def __bool__(self):
return self.value != 0
m = Money(42.7)
int(m) # 42
float(m) # 42.7
str(m) # '$42.70'
bool(m) # True
Якщо __bool__ не визначений - Python пробує __len__ (порожній об'єкт → False). Якщо немає ні того, ні іншого - об'єкт завжди істинний.
52. Що таке потік (Thread)? Що таке процес (Process)?⚑
🔥 33/100 · python/gil_threads_processes.md
Потік - це легкий підпроцес, що існує в межах основного процесу і може виконувати паралельні задачі. Кожен потік має свій стек викликів, але поділяє адресний простір, купу та регістри процесора з іншими потоками в рамках процесу. Потоки дозволяють програмі виконувати багатозадачні операції, розділюючи I/O завдання між різними потоками для збільшення продуктивності. У Python, для роботи з потоками можна використовувати модуль threading.
Процес - це незалежний виконуваний екземпляр програми, який має свою власну пам'ять та ресурси (файлові дескриптори, незалежні стеки викликів, купи та регістри процесора) і виконує код незалежно від інших процесів. Кожен процес має власний ідентифікатор процесу (PID) та виконується в окремому розділі пам'яті. У Python, для роботи з процесами можна використовувати модуль multiprocessing. Процеси є корисними для використання багатоядерних систем, оскільки кожен процес може працювати на власному ядрі процесора, що підвищує продуктивність програм. Особливо у випадку Python, де через GIL багатозадачність не дає справжнього паралелізму.
Links
- Python без блокувань. Як працюють потоки - dou.ua
- Розділяй і володарюй. Як працюють процеси в Python - dou.ua
53. Що таке локальні та глобальні змінні в Python? global, nonlocal⚑
🔥 33/100 · python/namespace_and_context_manager.md
Глобальні змінні - змінні, оголошені поза функцією або в глобальному просторі, називаються глобальними змінними. Ці змінні можуть бути доступні будь-якою функцією в програмі.
Локальні змінні - будь-яка змінна, оголошена всередині функції, відома як локальна змінна. Ця змінна присутня в локальному просторі, а не в глобальному.
if, for, while не мають власного скоупу (області видимості) - змінні будуть доступні після виконання
global: Ключове словоglobalдозволяє звертатися до глобальних змінних всередині функцій та змінювати їх значення. Глобальні змінні оголошуються на рівні програми та є видимими в усій програмі.nonlocal: Ключове словоnonlocalдозволяє звертатися до локальних змінних в зовнішніх функціях (батьківських функціях) з внутрішніх функцій (вкладених функцій). Це дозволяє змінювати значення змінних у батьківській функції з внутрішньої функції.
global використовується для роботи з глобальними змінними, а nonlocal - для роботи з локальними змінними у вкладених функціях.
>>> def function():
... global x
... x = 'global assigned inside function'
... print (x)
...
>>> function()
global assigned inside function
>>> x
'global assigned inside function'
>>>
>>> def outer():
... x = "x assigned inside outer"
... def inner():
... nonlocal x
... x = "x assigned inside inner"
... print("inner():", x)
... inner()
... print("outer():", x)
...
>>> outer()
inner(): x assigned inside inner
outer(): x assigned inside inner
>>>
Links
54. Нормалізація баз даних⚑
🔥 32/100 · infrastructure/database.md
Нормалізація баз даних - це процес поділу таблиці на дрібніші, пов'язані таблиці для уникнення надмірності даних та забезпечення узгодженості та цілісності бази даних.
Нормалізація дозволяє розділити таблиці таким чином, що кожна таблиця містить лише ті атрибути, які взаємодіють з однією конкретною сутністю. Це виконується за допомогою розподілу таблиць та визначення зв'язків між ними.
Денормалізація баз даних - це процес об'єднання даних із кількох таблиць в одну або кілька денормалізованих таблиць для оптимізації продуктивності та спрощення запитів.
Денормалізації включає
- Зменшення числа з'єднань таблиць, щоб покращити продуктивність під час виконання запитів.
- Дублювання даних для спрощення та прискорення вилучення даних.
- Використання агрегованих стовпців для попереднього обчислення агрегатних функцій.
Нормальні форми в базах даних визначають, наскільки добре структуровані таблиці бази даних щодо уникнення дублювання даних та забезпечення консистентності. Існують кілька рівнів нормальних форм, кожен з яких вказує на певний ступінь оптимізації та структурованості бази даних.
Існують такі рівні нормалізації: перша нормальна форма (1НФ), 2НФ, 3НФ, нормальна форма Бойса-Кодда (БКНФ), 4НФ, 5НФ. Але дотепер жодна з реляційних СКБД не надає належної підтримки усім п'яти нормальним формам. Це відбувається через жорсткі вимоги до продуктивності. Суть справи полягає в тому, що в повністю нормалізованій БД для виконання запиту треба з'єднати настільки багато таблиць, що продуктивність такої системи не зможе задовольнити користувачів. Тому на практиці використовують лише перші три рівня нормалізації – 1НФ, 2НФ, ЗНФ.
Види нормальних форм
- Перша нормальна форма (1NF) утворює ґрунт для структурованої схеми бази даних
- Кожна таблиця повинна мати первинний ключ: мінімальний набір колонок, які ідентифікують запис.
- Відсутні групи атрибутів з однаковими за змістом значеннями, які повторюються у межах одного кортежу.
- Атомарність - кожен атрибут повинен мати лише одне значення, а не множину значень. Наприклад, якщо у атрибуті "Прізвище, ім'я, по батькові" міститься прізвище, ім'я та по батькові читача, вимога неподільності не виконується
- Друга нормальна форма (2NF)
- Таблиця повинна бути у 1NF
- Всі неключові стовпці повинні повністю залежати від первинного ключа.
- Третя нормальна форма (3NF)
- Таблиця повинна бути у 2NF
- Усуває транзитивні залежності, тобто кожен неключовий атрибут повинен прямо залежати від первинного ключа, а не від інших неключових атрибутів. Це ще більше зменшує надмірність і підвищує цілісність.
Переваги нормалізації
- Зменшення надмірності даних та забезпечення цілісності бази даних.
- Поліпшення продуктивності під час оновлення даних.
- Спрощення структури бази даних, що робить її легшою для розуміння та підтримки.
Недоліки нормалізації
- Збільшення складності при вилученні даних із кількох пов'язаних таблиць.
- Збільшення числа з'єднань таблиць при виконанні складних запитів, що може негативно позначитися на продуктивності.
Links
55. Ітератор⚑
🔥 32/100 · python/iterator_and_generator.md
Summary
Ітератор - об'єкт, який знає, як повертати свої елементи по одному за раз, підтримує ітерацію по послідовності. З точки зору Python він повинен мати метод
__iter__(), який повертає сам об'єкт ітератора, і метод__next__(), який повертає наступний елемент послідовності або викидає винятокStopIteration, якщо більше немає елементів.
Ітератори використовуються в циклі for для ітерації по колекції. Кожен об'єкт, який підтримує ітератор, є ітерабельним, але не кожен ітерабельний об'єкт є ітератором. Наприклад рядки та словники - оскільки вони не мають методу __next__, натомість їхня ітерабельність забезпечується методом __iter__, який повертає окремий об'єкт-ітератор (для словника - ітератор по ключах).
Ітерабельність - це властивість об'єкта підтримувати ітерацію. Всі послідовності в Python є ітерабельними, оскільки вони підтримують ітерацію через свої елементи.
Ітератори представлені абстрактним класом collections.abc.Iterator:
class Iterator(Iterable):
__slots__ = ()
@abstractmethod
def __next__(self):
"""Return the next item from the iterator. When exhausted, raise StopIteration"""
raise StopIteration
def __iter__(self):
return self
@classmethod
def __subclasshook__(cls, C):
if cls is Iterator:
return _check_methods(C, '__iter__', '__next__')
return NotImplemented
__next__повертає наступний доступний елемент і викликає винятокStopIteration, коли елементів не залишилося.__iter__повертаєself. Це дозволяє використовувати ітератор там, де очікується ітерабельний об'єкт, наприклад, в цикліfor.__subclasshook__перевіряє наявність у класу методів__iter__і__next__
По ітератору можна пройтись тільки один раз.
Вимога __iter__ для протоколу for
for не викликає __next__ напряму - він спершу запитує ітератор через iter(obj). Сам iter(), за документацією, "raises TypeError if it does not support either of those protocols" (__iter__ або застарілий __getitem__ з цілочисельним індексом):
class OnlyNext:
def __next__(self):
return 1
obj = OnlyNext()
next(obj) # 1 - direct call works
for x in obj: ... # TypeError: 'OnlyNext' object is not iterable
Реалізація лише __next__ без __iter__ технічно можлива, але такий об'єкт не сумісний із циклом for, розпакуванням, list() тощо - усі вони покладаються на iter(). Канонічна форма ітератора реалізує обидва методи, де __iter__ повертає self.
my_list = [1, 2, 3, 4, 5]
my_iterator = iter(my_list)
print(next(my_iterator)) # 1
print(next(my_iterator)) # 2
print(next(my_iterator)) # 3
for item in my_iterator:
print(item) # 4, 5
try:
print(next(my_iterator)) # raises StopIteration
except StopIteration:
print("Iteration is done")
56. Microservices - Мікросервіси⚑
🔥 31/100 · architecture/microservices.md
Мікросервіси — це архітектурний стиль, який передбачає розбиття програми на набір незалежних компонентів (сервісів), кожен з яких відповідає за окрему бізнес-функцію і може розгортатися та масштабуватися окремо. Цей підхід спрямований на підвищення гнучкості, простоти підтримки та масштабування великих систем.
Кожен сервіс є незалежним, має свою кодову базу, базу даних (або доступ до окремих таблиць) і виконує одне конкретне завдання. Взаємодія між сервісами зазвичай відбувається через легкі протоколи, такі як HTTP (REST, GraphQL) або брокери повідомлень (RabbitMQ, Kafka).
Підходить для великих і динамічних проєктів, де важлива можливість масштабувати окремі частини системи або швидко впроваджувати зміни. Мікросервіси дозволяють створювати гнучкі та масштабовані системи, але потребують ретельного планування, щоб уникнути зростання складності в управлінні.
Переваги мікросервісів
- Зручність масштабування: кожен сервіс можна масштабувати незалежно від інших.
- Можливість використовувати різні технології для різних сервісів залежно від їхніх потреб.
- Полегшення підтримки: зміни в одному сервісі не впливають на інші (за умови чітких контрактів).
- Зменшення ризику: збій в одному сервісі не зупиняє роботу всієї системи.
Недоліки мікросервісів
- Ускладнена інфраструктура: потрібен надійний механізм для оркестрації сервісів, моніторингу та логування.
- Накладні витрати на комунікацію: виклики між сервісами додають латентність і вимагають захисту від помилок.
- Складнощі в забезпеченні узгодженості даних між сервісами.
57. В чому різниця між списком і кортежем⚑
🔥 31/100 · python/sequences.md
Кортеж (tuple), тюпл - це незмінна послідовності. Представлені класом tuple.
Список (list), ліст - це змінна послідовність, яка зазвичай використовується для зберігання однотипних даних (хоча Python не забороняє зберігати в них дані різних типів). Представлені класом list.
На рівні мови вони відрізняються тим, що до кортежу не можна додати або видалити елемент. Внутрішньо list і tuple реалізований як масив вказівників на об'єкти в пам'яті.
Tuple
a = (), a = (25,), a = tuple()- Оскільки розмір кортежа фіксований, його можна зберігати більш компактно, ніж списки, які потребують додаткового виділення пам'яті для ефективної операції
append(). - Кортежі зберігаються в одному блоку пам'яті. Кортежі є незмінними, тому не потребують додаткового місця для зберігання нових об'єктів. Це причина, чому створення кортежу є швидшим за створення списку. Це також пояснює невелику різницю в швидкості індексування, яка є швидшою ніж в списках, оскільки в кортежах для індексування слідкує за меншою кількістю вказівників (оскільки структура фіксована).
typedef struct {
PyObject_VAR_HEAD
/* ob_item contains space for 'ob_size' elements.
Items must normally not be NULL, except during construction when
the tuple is not yet visible outside the function that builds it. */
PyObject *ob_item[1];
} PyTupleObject;
- Макрос
PyObject_HEADдодає лічильник посилань та вказівник на батьківський тип до об'єкта. - Макрос
PyObject_VAR_HEADвключає в себеPyObject_HEADі додає полеPy_ssize_t ob_size, в якому зберігається кількість елементів. PyObject *ob_item[1]- це масив вказівників на об'єкти, які зберігаються в кортежі і є статичним масивом.- Порожній кортеж у CPython - кешований singleton: усі
()посилаються на один об'єкт, тоді як кожен новий порожній список створюється заново (бо список змінний, спільний об'єкт неможливий). Це деталь реалізації CPython (не гарантія мови), яка економить пам'ять:
List
a = []- швидша ініціалізація ніжlist()- Списки виділяються в двох блоках: фіксованому з усією інформацією об'єкта Python і блоку змінного розміру для даних.
typedef struct {
PyObject_VAR_HEAD
/* Vector of pointers to list elements. list[0] is ob_item[0], etc. */
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
PyObject_VAR_HEAD- це макрос, який використовується для реалізації базового об'єкта Python. Він містить розмір та посилання на тип об'єкта.PyObject **ob_item- це масив вказівників на об'єкти, які зберігаються в списку.Py_ssize_t allocated- це кількість виділеної пам'яті для об'єктів списку. Поле 'allocated' дорівнює розміру буфера, до якого додавання нових елементів відбувається швидко. Якщо кількість елементів 'ob_size' стає рівною 'allocated', то створюється новий масив більшої місткості і копіюються в нього елементи старого масиву. Встановлюється новий 'allocated', який є більшим відносно старого. 'allocated' завжди кратний 4, і модель його зростання наступна: 0, 4, 8, 16, 24, 32, 40, 52, 64, 76 і так далі.
Незмінність кортежа поверхнева
Кортеж фіксує лише власні вказівники - якщо елемент сам є змінним об'єктом (наприклад, списком), його можна змінити на місці; сам кортеж при цьому "не змінюється", бо вказівник лишається тим самим. Звідси неочевидна поведінка доповненого присвоєння +=:
t = ([1], 2)
t[0] += [3] # TypeError: 'tuple' object does not support item assignment
print(t) # ([1, 3], 2) - the list was extended anyway!
+= для списку - це __iadd__ (зміна на місці), яка відпрацьовує до спроби переприсвоїти t[0]. Тому операція і кидає виняток, і встигає змінити список - вона не атомарна.
Множення списку копіює посилання, а не об'єкти. Оператор * для списку не клонує вкладені об'єкти - він заповнює новий список тими самими посиланнями. Тому [[]] * 3 створює три посилання на той самий список, і зміна одного видно в усіх:
a = [[]] * 3
a[0].append(1)
print(a) # [[1], [1], [1]] - not three independent lists
matrix = [[0] * 3] * 3 # the same trap for a "matrix"
matrix[0][0] = 1
print(matrix) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]
Щоб отримати незалежні вкладені списки, використовують генератор - [[] for _ in range(3)] (чи [[0] * 3 for _ in range(3)] для матриці). Множення безпечне лише для незмінних елементів: [0] * 3 коректно дає [0, 0, 0].
58. Словник (dict)⚑
🔥 31/100 · python/set_and_dict.md
Summary
Словник (dict) - це контейнерний об'єкт (хеш-таблиця), який використовується для зберігання пар ключ-значення. У словнику кожен ключ є унікальним ідентифікатором, який використовується для доступу до відповідного значення.
Ініціалізація - a = {}, a = dict()
Ключі можуть бути майже будь-якого типу даних (цілі числа, рядки, кортежі тощо), але вони повинні бути незмінними (immutable). А якщо точніше - то hashable. Наприклад, рядок або ціле число може бути ключем, але список не може. Об'єкт може бути ключем, оскільки він hashable.
Починаючи з версії 3.7, усі словники зберігають порядок вставки ключів - вони будуть прочитані в тому порядку, в якому були записані. Це гарантія, яку дає сам dict, а не окремий клас OrderedDict.
В Python практично всі обєкти - dict.
Доступ до ключа - O(1)
59. Що таке колізія⚑
🔥 31/100 · python/set_and_dict.md
Колізія - ситуація, коли хеш-функція повертає однакове значення (або однаковий індекс після взяття по модулю розміру таблиці) для двох різних ключів. CPython вирішує колізії виключно через open addressing (відкрите адресування); chaining (ланцюжки) - це альтернативна стратегія, яка використовується в інших реалізаціях (наприклад, у Java HashMap), але не у CPython.
Open addressing (стратегія CPython)
При колізії алгоритм не створює окрему структуру для конфліктуючих ключів - він шукає іншу вільну комірку у тому ж самому масиві. Пошук наступної комірки називається пробінгом (probing). Найпростіші його варіанти - лінійний (i+1, i+2, …) і квадратичний (i+1, i+4, i+9, …) - використовуються в навчальних реалізаціях, але страждають від кластеризації колізій.
CPython використовує власну схему пробінгу з перетурбацією (perturbation), яка залучає всі біти хешу, а не лише ті, що потрапили в початковий індекс. Це знижує кластеризацію для типових структур ключів. Формула наступного індексу взята з коментаря "Major subtleties ahead" у Objects/dictobject.c (CPython main, актуальна на момент написання):
#define PERTURB_SHIFT 5
perturb >>= PERTURB_SHIFT;
i = (i*5 + perturb + 1) & mask; /* mask = size - 1, size is a power of 2 */
perturb ініціалізується значенням хешу і на кожній ітерації зсувається на PERTURB_SHIFT біт вправо, поступово підмішуючи у пробінг старші біти хешу. Вибір значення 5 для PERTURB_SHIFT - результат експериментів Tim Peters (з того ж коментаря у джерелі): значення оптимізує сумарну кількість колізій, але 4 чи 6 дають порівнянний результат.
Алгоритм пошуку ключа:
- Обчислити
hash(key), взятиindex = hash & (size - 1)(size- степінь двійки). - Якщо комірка порожня - ключа немає.
- Якщо у комірці є ключ і його
__eq__дорівнює шуканому - повернути значення. - Інакше це колізія - обчислити наступний індекс за схемою пробінгу з перетурбацією і повторити.
При видаленні елемента на його місце ставиться спеціальний маркер dummy (tombstone) замість повного очищення комірки - інакше пошук міг би зупинитися на порожньому слоті, не дійшовши до ключа, який раніше при колізії перейшов далі.
Переваги open addressing у CPython над chaining
- Cache locality. Усі дані лежать в одному суцільному масиві, що добре дружить з кешем процесора. Chaining натомість змушує переходити по вказівниках між елементами ланцюжка, які лежать у довільних місцях heap'у.
- Менше алокацій. Не потрібно виділяти окремі вузли списку при кожній колізії.
- Передбачуваний layout. Розмір таблиці завжди степінь двійки; пробінг не вимагає додаткових структур.
Chaining (для порівняння)
У реалізаціях з chaining (Java HashMap, C++ std::unordered_map) кожна комірка таблиці зберігає вказівник на ланцюжок (зв'язаний список або, у нових версіях Java, збалансоване дерево) елементів з однаковим індексом. Пошук - спочатку по індексу, потім по ланцюжку через __eq__. Простіша модель, але гірша cache locality і додаткові алокації на колізію.
Links
60. Що таке індекси? Навіщо вони потрібні? Як вони працюють?⚑
🔥 30/100 · infrastructure/database.md
Summary
Індекси - це структури даних у базах даних, які допомагають прискорити пошук і фільтрацію даних. Вони створюються для певних полів таблиць і дозволяють базі даних здійснювати швидший доступ до конкретних рядків.
Індекси потрібні для покращення продуктивності запитів, особливо коли потрібен пошук або фільтрація даних за певними полями. Вони допомагають зменшити кількість рядків, які база даних повинна перевірити, щоб знайти відповідні результати.
Індекси працюють шляхом створення додаткової структури даних, яка має відображення на значення поля та посилання на відповідний рядок у таблиці. При пошуку даних за індексованим полем база даних спочатку переходить до відповідного запису в індексі, а потім знаходить відповідний рядок у таблиці.
Індекси не є безкоштовними. Вони займають додатковий обсяг місця на диску і впливають на швидкість вставки, оновлення і видалення даних. Тому їх слід створювати обдумано, зважаючи на типи запитів, які будуть використовуватися в проекті.
Індекси підвищують продуктивність запитів на вибірку даних, але одночасно можуть збільшити час виконання операцій вставки, оновлення та видалення, адже база даних також повинна оновити індекс.
Тому індекси додають свідомо, а не "про всяк випадок". Дві типові помилки в обидва боки: колонки, за якими йде JOIN, **недо**індексовані - PostgreSQL автоматично індексує лише первинний ключ, а колонки зовнішнього ключа ні, тож без ручного індексу кожен JOIN дає seq scan приєднуваної таблиці; натомість давно створені **не**використовувані індекси лише з'їдають місце і сповільнюють запис - кандидатів на видалення видно у pg_stat_user_indexes (idx_scan = 0 - індексом жодного разу не скористались).
У PostgreSQL індекси реалізовані різними способами. Наприклад, B-дерева є одним з найпоширеніших типів індексів. Вони дозволяють швидкий пошук значень і забезпечують ефективну підтримку різних операцій порівняння, таких як менше, більше, дорівнює тощо.
Принцип роботи B-дерева. Індекс - це збалансоване дерево зі сторінок. Пошук починається з кореневої сторінки, що містить ключі-межі та посилання на дочірні сторінки, кожна з яких відповідає за неперервний діапазон ключів. Спускаючись від кореня через проміжні сторінки, пошук доходить до сторінки-листа із самими значеннями (або посиланнями на них). Кількість посилань на одній сторінці - коефіцієнт розгалуження (branching factor) - зазвичай кількасот, тому навіть мільярди ключів вкладаються в дерево глибиною 3-4 рівні. При вставці, якщо на потрібній сторінці бракує місця, її розбивають на дві напівпорожні, а батьківську оновлюють. Сторінки-листи часто зв'язані вказівниками вліво та вправо, що дає впорядкований обхід діапазону без повернення до батьківських сторінок. Для відмовостійкості зміни спершу пишуть у журнал упереджувального запису (WAL, redo log) і лише потім застосовують до сторінок дерева - після збою дерево відновлюють із WAL.
Links
61. Як Kafka працює під капотом?⚑
🔥 30/100 · infrastructure/mq.md
Kafka - розподілена лог-орієнтована система повідомлень, спроектована для високої продуктивності, стійкості й обробки великих потоків даних у реальному часі.
1. Основні сутності
- Producer - відправляє повідомлення в Kafka.
- Consumer - читає повідомлення з Kafka.
- Broker - сервер Kafka, що зберігає й передає дані.
- Topic - логічна категорія/канал повідомлень.
- Partition - фізичний сегмент усередині топіка (забезпечує масштабування).
- Consumer Group - група консьюмерів, що спільно читають партиції.
2. Зберігання повідомлень (лог-файли)
Kafka зберігає повідомлення у файлах на диску як стійкий впорядкований лог:
- Кожна партиція - це append-only лог-файл.
- Нові повідомлення дописуються в кінець, старі не змінюються.
- Дані передаються з диску з використанням zero-copy.
3. Чому Kafka швидка
- Append-only запис - найшвидша операція на диску.
- Zero-copy I/O - дані передаються з page cache у сокет, обходячи user-space.
- Партиції дозволяють розпаралелювати обробку.
4. Підписка і зсуви (offsets)
- У кожного консьюмера є
offset- позиція в партиції. - Консьюмер сам вирішує, де починати читати (можна "перемотати").
- Kafka не видаляє повідомлення одразу після прочитання (на відміну від RabbitMQ) - вони зберігаються за часом (
retention.ms) або розміром (retention.bytes).
Consumer lag - ключова метрика стану. Lag - це різниця між останнім записаним у партицію offset (log end) і зафіксованим offset споживача, тобто скільки повідомлень ще не оброблено. Це головний сигнал здоров'я конвеєра: стабільний lag (хай і ненульовий) означає, що споживачі встигають; зростаючий lag - що не встигають, і потрібно масштабувати споживачів (до кількості партицій - більше консьюмерів за партиції не прискорить) або шукати повільну обробку. На відміну від CPU чи пам'яті, lag видно лише зсередини брокера, тож на нього обов'язково вішають окреме сповіщення - інакше відставання накопичується непомітно, поки споживачі формально "живі". У RabbitMQ аналог - довжина черги (queue depth): росте так само, коли продюсери випереджають консьюмерів цієї черги.
5. Масштабування і відмовостійкість
- Кластер складається з кількох брокерів.
- Кожна партиція може мати репліки на інших брокерах.
- Один брокер - leader партиції, решта - followers.
- Kafka використовує Zookeeper (або KRaft у нових версіях) для виборів лідерів, метаданих і кворуму.
62. Анотації типів (Type Hints). PEP 484⚑
🔥 30/100 · python/data_types.md
Підказки типів (Type Hints), Аннотації типів (Type Annotations) — це спосіб вказати очікувані типи аргументів функцій, значень змінних і результатів, що повертаються. Це допомагає зробити код більш зрозумілим, полегшує підтримку і знижує ризик помилок.
PEP 484 (Python Enhancement Proposal 484) - вводить підтримку підказок типів у Python. Він стандартизує синтаксис та семантику аннотацій типів і надає рекомендації щодо їх використання в Python-проектах.
- Анотації додаються до сигнатури функцій або змінних, але вони не впливають на виконання коду. Python ігнорує їх під час виконання, залишаючи це на розсуд розробника чи сторонніх інструментів, таких як статичні аналізатори (наприклад,
mypy). mypy- інструмент для перевірки типів (Type Checking).mypyаналізує код і перевіряє, чи відповідають вказані типи фактичним типам даних, що передаються та повертаються функціями.- Основне призначення анотацій типів — документування і поліпшення якості коду. Вони дозволяють чітко вказати, з якими типами даних працює функція, що значно спрощує розуміння логіки програми.
- Анотації виконуються під час компіляції (у момент інтерпретації файлу Python), але лише у вигляді метаданих. Їх можна отримати через атрибут
__annotations__. - Хоча анотації не виконуються під час виконання коду, їх можна використовувати в спеціалізованих сценаріях, наприклад, для генерації API або перевірки типів у реальному часі.
def add_numbers(x: int, y: int) -> int:
return x + y # Function takes two integers and returns an integer
Анотації також можна використовувати зі змінними
Для складних структур, таких як списки чи словники, в старіших версіях Python використовують модуль typing
from typing import List, Dict
numbers: List[int] = [1, 2, 3] # List of integers
user_data: Dict[str, int] = {"age": 30, "score": 100} # Dictionary with string keys and integer values
63. Опишіть функції map, filter (вбудовані) та functools.reduce⚑
🔥 30/100 · python/functional_programming.md
Функція map застосовує функцію до кожного елемента послідовності.
Функція filter залишає тільки ті елементи послідовності, для яких задана функція є істинною.
Для Python більш канонічним є використання спискових виразів замість map-filter.
Функція reduce приймає функцію з двома аргументами, послідовність і опціональне початкове значення і обчислює скорочення (складання) послідовності як результат послідовного застосування даної функції до поточного значення (так званого аккумулятора) і наступного елемента послідовності.
64. Як Python працює з пам'яттю?⚑
🔥 30/100 · python/interpreter.md
В Python диспетчер пам'яті відповідає за розподіл і звільнення пам'яті. Він виділяє пам'ять у вигляді простору в купі, в якому зберігаються всі об'єкти Python та структури даних. Пам’ять у Python можна розділити на 4 категорії, кожна з яких відповідає за зберігання різних типів даних і структур. Цей простір недоступний для програміста безпосередньо, проте базовий API дозволяє розробнику отримати доступ до деяких інструментів для написання коду.
- Об'єктно-специфічна пам’ять — це частина пам’яті, яка виділяється для об’єктів, що використовують спеціалізовані механізми управління пам’яттю, відмінні від стандартних. Це дозволяє оптимізувати зберігання та доступ до даних для певних типів об’єктів.
- Необ'єктна пам’ять ядра Python — це частина пам’яті, яка виділяється для зберігання даних та структур, що не є об’єктами Python. Вона використовується самим інтерпретатором для забезпечення своєї роботи та управління виконанням програм. Вона містить дані, необхідні для роботи самого інтерпретатора Python, такі як стеки викликів, таблиці символів, інформація про потоки виконання, конфігураційні параметри, налаштування середовища виконання.
- Пам'ять об’єктів — це частина пам’яті, яка виділяється для зберігання самих об’єктів Python. Кожен об’єкт у Python (числа, рядки, кастомні класи тощо) займає певний обсяг пам’яті, який необхідний для зберігання його даних та метаданих. Ця частина пам’яті містить у собі реальні дані, які зберігає об’єкт (наприклад, значення числа, текст рядка тощо).
- Внутрішні буфери — це області пам’яті, які виділяються інтерпретатором Python для тимчасового зберігання даних під час виконання різних операцій. Вони потрібні для оптимізації роботи та забезпечення ефективності різних процесів.
- Буфери вводу/виводу зберігають дані, що читаються з файлів або мережі, а також дані, що готуються для запису. Це дозволяє оптимізувати операції вводу/виводу, зменшуючи кількість системних викликів.
- Буфери інтерпретації коду зберігають проміжні результати під час компіляції та виконання байт-коду Python.
- Кеші слугують для зберігання даних, що часто використовуються, або результатів обчислень, щоб уникнути повторних витратних операцій.
Коли програма запитує пам’ять, CPython-інтерпретатор використовує malloc метод для запиту цієї пам’яті в операційної системи, і розмір приватної купи збільшується. Щоб уникнути виклику malloc і free для кожного створення і видалення невеликого об’єкта, CPython визначає кілька алокаторів і деалокаторів для різних цілей. Алокатор пам’яті — це "менеджер", що відповідає за виділення і звільнення пам’яті для програм, які її потребують.
Коли об’єкт потребує пам’яті і для нього визначені специфічні алокатори, виділення пам’яті відбувається саме через ці спеціальні алокатори. Якщо для об’єкта немає визначених алокаторів і потрібен обсяг пам’яті понад 512 байтів, менеджер пам’яті Python безпосередньо звертається до алокатора необробленої пам’яті. Якщо ж обсяг запитуваної пам’яті менший за 512 байтів, то для її виділення використовуються стандартні алокатори, оптимізовані для роботи з дрібними об’єктами.
Стандартний алокатор складається з трьох компонентів: арени, пули та блоки.
- Арена — це велика область пам’яті (1 МБ на 64-бітних системах; на 32-бітних — 256 кБ), яка виділяється операційною системою. Python керує кількома аренами, кожна з яких може містити кілька пулів.
- Пул — це фрагмент пам’яті фіксованого розміру (зазвичай 4 кБ), що виділяється з арени. Кожен пул спеціалізується на виділенні пам’яті під об’єкти певного розміру. Наприклад, один пул може використовуватися для об’єктів розміром 16 байтів, інший — для об’єктів розміром 32 байти тощо. Це дозволяє ефективно використовувати пам’ять і зменшити фрагментацію.
- Блок — це найменша одиниця пам’яті, яка виділяється для об’єктів. Кожен пул ділиться на блоки однакового розміру. Коли створюється об’єкт, пам’ять під нього виділяється у вигляді одного або кількох блоків з відповідного пулу.
Коли потрібно виділити пам’ять під новий об’єкт, Python шукає потрібну арену. Якщо не знаходить, запитує нову в операційної системи. Для того, щоб додати новий об’єкт, обирається найбільш заповнена арена для компактнішого розміщення об’єктів у пам’яті. Далі вибирається необхідний пул (відповідно до розміру об’єкта) і виділяється потрібна кількість блоків. Якщо в пулі немає вільних блоків, виділяється новий пул із наявної арени або створюється нова арена. Така структура дозволяє ефективно управляти пам’яттю, зменшуючи фрагментацію та прискорюючи виділення пам’яті. Це особливо корисно для малих об’єктів, які часто створюються і знищуються в Python.
Стандартний інтерпретатор Python (CPython) використовує два алгоритми збирання сміття: підрахунок посилань (reference counting) та збирач сміття (generational garbage collector або GC).
Також важливо відзначити, що в Python є різні види об'єктів, такі як незмінні (immutable) і змінні (mutable), і вони мають різні правила використання пам'яті та керування нею.
Links
- Мистецтво управління пам’яттю в Python: розуміння, використання та оптимізація
- Как работает память в Python
- Память и Python
65. Основні принципи DDD⚑
💡 29/100 · architecture/ddd.md
Domain-Driven Design (DDD) — це підхід до проєктування складних програмних систем, спрямований на чітке вираження бізнес-логіки та її ізоляцію від технічних деталей. Головна ідея DDD полягає в тому, щоб модель програми максимально відповідала реальним бізнес-процесам і була зрозумілою як для розробників, так і для експертів предметної області.
DDD складається з двох основних рівнів:
Стратегічний рівень
- Bounded Contexts — визначає межі системи та контексти, в яких певні терміни мають специфічне значення
- Ubiquitous Language — формує єдину мову для ефективної взаємодії з бізнес-експертами та між командами
- Context Mapping — керує відносинами між різними контекстами та їх інтеграцією
Тактичний рівень - описує конкретні шаблони реалізації всередині bounded context
- Entities — сутності з унікальною ідентичністю
- Aggregates — кластери пов'язаних об'єктів
- Value Objects — об'єкти без ідентичності
- Domain Services — сервіси для бізнес-логіки, що не належить конкретній сутності
Уся бізнес-логіка зосереджена в доменному шарі, а саме в сутностях, агрегатах та Value Objects. Тут визначаються:
- Бізнес-інваріанти (правила, що завжди мають виконуватися)
- Допустимі переходи станів
- Валідні операції
Зміна стану та перевірка інваріантів відбуваються виключно через методи сутності, які явно виражають бізнес-зміст: complete(), cancel(), change_owner() тощо.
Доменні моделі не залежать від інфраструктури (ані від баз даних, ані від транспорту, ані від зовнішніх DTO), і це дозволяє повторно використовувати бізнес-логіку незалежно від того, які технології використовуються в інфраструктурному шарі. Репозиторії, адаптери, транспорт, логування, моніторинг та інша інфраструктура виносяться за межі домену й використовуються тільки в application-шарі.
Сервіси-оркестратори - це тонкий шар, який координує дії між доменними об'єктами та інфраструктурою. Він сам не містить бізнес-логіки, але керує викликами доменних методів, збором даних і збереженням результатів. Він також єдиний шар, який може спілкуватися з зовнішніми системами й взаємодіяти з репозиторіями, надсилати події, логувати тощо.
Переваги використання DDD:
- Чітке вираження бізнес-логіки — код відображає реальні бізнес-процеси
- Покращена комунікація — спільна мова з бізнес-експертами
- Модульність — чіткі межі між компонентами
- Тестованість — легко тестувати бізнес-логіку ізольовано
- Еволюційність — система легко адаптується до змін у бізнесі
- Повторне використання — доменна логіка не залежить від технологій
66. Що таке mocking⚑
💡 29/100 · computer_science/testing.md
Mock (англ. "підробка") - це імітація або підробка. Принцип його роботи простий: якщо потрібно протестувати функцію, то все, що не стосується самої функції (наприклад, читання з диска або мережі), можна замінити макетами-пустушками. При цьому тестовані функції не потрібно адаптувати для тестів: Mock підміняє об'єкти в інших модулях, навіть якщо код не передає їх як параметри.
Стандартна бібліотека надає об'єкти Mock і MagicMock у модулі unittest.mock. Перший є тестовим двійником, який можна налаштувати на повернення будь-якого значення та відстежуватиме виклики, зроблені до нього. Другий робить те саме, але також підтримує магічні методи. Це означає, що якщо потрібно протестувати код, який використовує магічні методи (і частини коду, які тестуються, будуть від них залежати), доведеться використовувати екземпляр MagicMock замість простого Mock.
Links
67. Чим Реляційні БД відрізняються від NoSQL БД?⚑
💡 29/100 · infrastructure/database.md
Реляційні бази даних (РБД) та нереляційні бази даних (NoSQL) відрізняються за своєю структурою, способом зберігання та обробки даних.
- Схема даних
- РБД: Використовують жорстку, попередньо визначену схему даних з таблицями, колонками і типами даних, з визначеними відношеннями між ними.
-
NoSQL БД: Забезпечують гнучку схему даних, що дозволяє зберігати дані різного типу без жорсткої фіксованої структури.
-
Масштабованість
- РБД: Масштабування може бути складнішим, особливо при горизонтальному масштабуванні.
-
NoSQL БД: Зазвичай пропонують легше горизонтальне масштабування завдяки спеціалізованій архітектурі.
-
Запити
- РБД: Використовують мову запитів SQL для взаємодії з даними.
-
NoSQL БД: Використовують різні мови запитів або інтерфейси для операцій з даними.
-
Транзакції та консистентність
- РБД: Забезпечують гарантії ACID для збереження цілісності даних.
- NoSQL БД: Можуть пропонувати менш жорсткі гарантії цілісності, забезпечуючи вищу швидкість та масштабованість.
68. __slots__⚑
💡 29/100 · python/class_and_object.md
Класи зберігають поля та їх значення у прихованому словнику __dict__. Оскільки словник є змінною структурою, можна додавати та видаляти поля класу в будь-який момент. Параметр __slots__ у класі жорстко фіксує набір полів класу. Слоти використовуються, коли клас може мати дуже багато полів, наприклад, в деяких ORM або коли критична продуктивність, оскільки доступ до слоту відбувається швидше, ніж пошук у словнику, або коли в процесі виконання програми створюється мільйони екземплярів класу, застосування __slots__ дозволить економити пам'ять.
Слоти активно використовуються в бібліотеці requests.
Мінуси: не можна присвоїти класу поле, якого немає в слотах. Не працюють методи __getattr__ і __setattr__.
Рішення: включити в __slots__ елемент __dict__.
class Person:
__slots__ = ('name', 'age')
def __init__(self, name, age):
self.name = name
self.age = age
person = Person("John", 30)
try:
person.address = "New York" # This will raise an AttributeError
except AttributeError as e:
print("Error:", e)
person.name = "Alice" # Setting values for attributes defined in __slots__
person.age = 25
Під капотом це реалізується за допомогою дескрипторів. Оскільки атрибуту __dict__ для зберігання значень змінних екземпляра немає, Python замість цього створює дескриптор для кожного слоту та зберігає значення там. Це має побічний ефект: ми не можемо змішувати атрибути класу з атрибутами екземпляра (наприклад, використання атрибута класу як значення за замовчуванням для атрибута екземпляра неможливо, тому що значення будуть перезаписані).
69. Що таке властивість (@property)?⚑
💡 29/100 · python/class_and_object.md
Summary
Property (властивість) - це спеціальний метод класу, який дозволяє контролювати доступ до атрибутів об'єкта, коли вони читаються, записуються або видаляються. Property використовується для забезпечення контрольованого доступу до даних об'єкта. Реалізується з допомогою декоратора
@property.
Property дозволяє викликати метод як атрибут і всередині заховати складну логіку (точкова нотація). Або коли змінюється логіка всередині, щоб не міняти API.
Метод property() приймає на вхід методи get, set и delete, і повертає об'єкти класу property.
class Circle:
def __init__(self, radius):
self._radius = radius # Attribute with an underscore is a protected attribute
@property # Getter to retrieve the radius value
def radius(self):
return self._radius
@radius.setter # Setter to set the radius value
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@radius.deleter
def radius(self):
del self._radius
@property # Getter to compute the area of the circle
def area(self):
return 3.14159 * self._radius * self._radius
circle = Circle(5)
print(circle.radius) # Output: 5
circle.radius = 7
print(circle.radius) # Output: 7
print(circle.area) # Output: 153.93845
Декоратори @property і @radius.setter створюють property для атрибута radius. Property radius дозволяє отримувати і змінювати значення радіусу об'єкта circle.
Приблизна реалізація property() за допомогою протоколу дескрипторів
class Property:
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError
self.fdel(obj)
70. Що таке замикання (Closure)⚑
💡 29/100 · python/functions.md
Summary
Замикання (closure) – це вкладена функція, яка дозволяє отримати доступ до змінних зовнішньої функції навіть після завершення виконання зовнішньої функції.
Замикання — це конструкція, яка дозволяє функції "запам'ятовувати" значення змінних з її зовнішнього оточення, навіть після того, як ці змінні вийшли з області видимості. Це досягається завдяки вкладеним функціям, які можуть мати доступ до змінних з оточуючої функції.
Синтаксично це виглядає як функція, що повністю знаходиться у тілі іншої функції. При цьому вкладена внутрішня функція містить посилання на локальні змінні зовнішньої функції. Коли зовнішня функція завершує виконання, контекст її виконання не зникає повністю. Замість цього, він зберігається у вигляді замикання.
def counter():
count = 0
def increment():
nonlocal count
count += 1
print(f"Current count: {count}")
return increment
counter1 = counter()
counter1() # "Current count: 1"
counter1() # "Current count: 2"
print(counter1.__closure__) # return tuple with cell objects
Під капотом використовується атрибут функції, який називається __closure__, який повертає кортеж об'єктів cell, що зберігають значення з області видимості функції, в якій була створена вкладена функція. Кожен елемент кортежу являє собою значення однієї із змінних, що використовуються в замиканні.
Сценарії застосування
- Реалізація декораторів. Декоратор - це майже завжди замикання: внутрішня функція
wrapperпосилається на функцію-аргументfuncзовнішньої функції-декоратора. - Фабрики функцій. Створення функцій із зафіксованими параметрами:
def power_of(exponent):
def raise_to(base):
return base ** exponent
return raise_to
square = power_of(2)
cube = power_of(3)
square(5) # 25
cube(5) # 125
- Інкапсуляція стану без створення класу - "приватні" змінні живуть у замиканні, доступні лише через інтерфейс внутрішньої функції (як у прикладі з
counter). - Функціональне програмування - передача поведінки з зафіксованим контекстом замість об'єктів зі станом.
Пастка з присвоюванням
Замикання захоплює змінну зовнішнього scope, лише якщо вона не присвоюється у внутрішній функції. Присвоювання робить ім'я локальним і ламає захоплення:
def make_counter():
count = 0
def inc():
count += 1 # SyntaxError-ish at runtime: UnboundLocalError
return count
return inc
Тут Python бачить count = ... всередині inc і вважає count локальною змінною з самого початку функції; count += 1 намагається зчитати неіснуючу локальну. Щоб змінювати захоплену змінну, потрібен nonlocal:
Для зміни глобальної змінної - аналогічно, але global замість nonlocal. Якщо замикання лише читає змінну (не присвоює), ніяких ключових слів не потрібно.
71. Singleton (Одинак)⚑
💡 28/100 · computer_science/design_patterns.md
Summary
Породжувальний патерн: гарантує, що клас має лише один екземпляр, і надає глобальну точку доступу до нього. У Python природна заміна - модуль (він кешується в
sys.modules).
Принцип роботи
Клас контролює власне інстанціювання, повертаючи той самий об'єкт при кожному виклику. Типові реалізації - перевизначення __new__, метаклас або декоратор; найпростіша й найідіоматичніша - модуль-одинак.
class Settings:
_instance = None
def __new__(cls, *a, **kw):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
Варіанти реалізації (метаклас, декоратор, модуль) і їхні компроміси - python/general.md. Singleton часто критикують як прихований глобальний стан, що ускладнює тестування.
"Singleton" у Python - перевантажений термін. Слово вживають у кількох різних значеннях: (1) кортеж довжини 1 (термінологія документації); (2) модуль - імпорт кешує єдиний об'єкт у sys.modules; (3) flyweight - спільні незмінні значення, яких більше одного на клас: True/ False (bool має рівно два екземпляри), малі цілі -5..256, порожні str/tuple; їх кешують заради економії, але це не Singleton, бо клас має й інші екземпляри; (4) well-known об'єкт - єдиний екземпляр свого класу з глобальним іменем: None (єдиний екземпляр NoneType; у Python 3 навіть type(None)() повертає той самий None), Ellipsis - це найближче до Singleton; (5) класичний GoF-Singleton - звичайний клас, штучно обмежений одним екземпляром через __new__ чи метаклас. Тому None коректно вважати singleton-подібним well-known об'єктом, а True/False - це флайвейти, не Singleton'и.
У Python-екосистемі. Імпортований модуль (кешується у sys.modules) і logging.getLogger(name) (один Logger на ім'я) - готові одинаки, які мова забезпечує без додаткового коду.
import logging, sys
a = logging.getLogger("app")
b = logging.getLogger("app")
print(a is b) # True - logging keeps one Logger per name
print("logging" in sys.modules) # True - the imported module is itself a singleton
Links
- refactoring.guru: Singleton
- Wikipedia: Singleton pattern
- SourceMaking: Singleton
- Amir Lavasani: Singleton (Medium)
- python-patterns.guide: The Singleton Pattern
72. Як обробити виняток?⚑
💡 28/100 · python/exceptions.md
Для обробки винятків у Python використовується конструкція try...except...finally.
try:
result = 10 / 0 # Code that may raise an exception
except ZeroDivisionError:
print("Error: Division by zero!") # Handling a specific type of exception
except Exception as e:
print("An error occurred:", e) # Handling other types of exceptions
else:
print("Division was successful!") # Executed if no exception occurred
finally:
print("Finishing the processing") # Executed regardless of whether an exception occurred
У цьому прикладі конструкція try...except дозволяє спробувати виконати код, в якому може виникнути виняток. Якщо виняток виникає, програма переходить до відповідного блоку except, який обробляє виняток певного типу. Якщо потрібно обробити винятки, які знаходяться в ієрархії успадкування, по-різному, спочатку слід вказувати обробники менш загальних винятків, а потім - більш загальних. У разі, якщо тип винятку не відповідає жодному блоку except, виняток може бути оброблений в блоку except Exception.
Блок else виконується, якщо виняток не виник, тобто код у try виконався успішно.
Блок finally виконується завжди, незалежно від того, чи виникла помилка чи ні. Він часто використовується для звільнення ресурсів або виконання завершальних операцій. Блок finally буде виконаний навіть якщо в блоці try є оператор return. Це одна з особливостей конструкції try...finally в Python. Навіть якщо в блоці try виконується return, блок finally все одно буде виконаний перед тим, як функція поверне значення. Це зручно використовувати для завершення сесії бази даних або закриття файлу.
73. Витісняюча vs кооперативна багатозадачність⚑
💡 28/100 · python/gil_threads_processes.md
Витісняюча (preemptive): планувальник сам вирішує, коли перервати задачу - після кванта часу (quantum / time-slice) або коли задача йде в очікування I/O. Так працюють усі сучасні ОС (Linux, Windows NT+, macOS) і потоки Python.
Кооперативна (cooperative): задача утримує CPU, доки сама явно не віддасть управління.
Ключове: ця дихотомія діє на двох рівнях. На рівні ОС - завжди витісняюча. Усередині одного процесу asyncio та грінлети реалізують кооперативну модель: await (чи gevent-switch) - єдина точка передачі управління. Звідси правило: один блокуючий виклик без await зупиняє весь event loop, бо ніхто його не витіснить.
74. Генератор⚑
💡 28/100 · python/iterator_and_generator.md
Залежно від контексту, може означати або функцію-генератор, або ітератор генератора (зазвичай останнє). Методи __iter__ і __next__ для генераторів створюються автоматично.
Генератор - це лінивий ітератор. Генератор не зберігає в пам'яті всі елементи, а лише внутрішній стан для обчислення наступного елемента. На кожному кроці можна обчислити лише наступний елемент, але не попередній. Пройти генератор в циклі можна лише один раз.
Тобто ідея генератора полягає в тому, щоб створити об'єкт, який є ітерованим і під час ітерації створює елементи, які він містить, по одному за раз. Основна мета генераторів — заощадити пам'ять: замість того, щоб зберігати великий список елементів у пам'яті одночасно, ми маємо об'єкт, який знає, як створити кожен окремий елемент, коли це необхідно. Ця функція дає змогу виконувати "ліниві обчислення" важких об'єктів у пам'яті. Це дозволяє працювати з нескінченними послідовностями, оскільки "лінивий" характер генераторів відкриває таку можливість.
Переваги використання генераторів
- Економія пам'яті - генератори працюють ліниво, генеруючи значення на вимогу. Можуть використовуватися для завантаження даних за потреби, що корисно при роботі з великими файлами або мережевими ресурсами, що дозволяє економити пам'ять
- Ефективна обробка даних - генератори полегшують обробку великих наборів даних, тому що вони можуть обробляти дані поелементно, що може бути більш ефективним, ніж завантаження та обробка всіх даних одразу.
З точки зору реалізації, генератор в Python - це мовна конструкція, яку можна реалізувати двома способами: як функцію з ключовим словом yield або як генераторний вираз. При виклику функції або обчисленні виразу отримуємо об'єкт-генератор типу types.GeneratorType. Класичний приклад - генератор, який породжує послідовність чисел Фібоначчі, яка, будучи нескінченною, не могла б поміститися в будь-яку колекцію. Іноді термін використовується для самої генераторної функції, а не тільки для об'єкта, який повертається.
Оскільки в об'єкті-генераторі визначені методи __next__ і __iter__, тобто реалізований протокол ітератора, то в Python будь-який генератор є ітератором.
Коли виконання функції-генератора завершується (за допомогою ключового слова return або досягненням кінця функції), виникає виняток StopIteration.
75. Різниця між select_related та prefetch_related⚑
💡 28/100 · python_framework/django.md
У Django, select_related та prefetch_related — це два методи, які використовуються для оптимізації запитів до бази даних з метою зменшення кількості виконаних запитів і підвищення ефективності доступу до пов'язаних об'єктів. Проте їхня робота відрізняється.
select_related - повертає QuerySet, який автоматично включає у вибірку дані зв'язаних об'єктів при виконанні запиту. Працює з ForeignKey та OneToOneField. Створює запит, який об'єднує зв'язані таблиці і включає додаткові поля в SELECT
prefetch_related - повертає QuerySet, який отримує за один прохід зв'язані об'єкти для кожного з вказаних параметрів пошуку. Вибирає дані для кожного зв'язку окремо, і виконує об'єднання на рівні Python.
select_related- Тип зв'язку: Використовується для оптимізації доступу до пов'язаних об'єктів через зв'язки ForeignKey або OneToOneField.
- Механізм: Виконує SQL-запит за допомогою операції JOIN, об'єднуючи пов'язані таблиці та отримуючи всі дані в одному запиті.
prefetch_related- Тип зв'язку: Використовується для оптимізації доступу до пов'язаних об'єктів через зв'язки ManyToManyField або ForeignKey з багатьма елементами.
- Механізм: Виконує окремі SQL-запити для головної та пов'язаної таблиць, а потім зберігає їх у пам'яті і здійснює об'єднання на стороні Python.
books = Book.objects.prefetch_related('authors').all()
for book in books:
for author in book.authors.all():
print(author.name)
Об'єкт Prefetch і фільтрація в циклі. Фільтрація вже завантаженої колекції в циклі скасовує prefetch - на кожен рядок іде новий запит (повертається N+1):
stores = Store.objects.prefetch_related('books') # 2 queries
for store in stores:
expensive = store.books.filter(price__gte=250) # +1 query per store -> N+1
.filter(...) будує новий QuerySet, а prefetch кешує лише незмінений store.books.all(). Щоб відфільтрувати на рівні БД і зберегти prefetch - передати фільтрований QuerySet в об'єкт Prefetch:
from django.db.models import Prefetch
stores = Store.objects.prefetch_related(
Prefetch('books', queryset=Book.objects.filter(price__gte=250)),
) # back to 2 queries
for store in stores:
expensive = store.books.all() # uses the prefetched cache
to_attr='expensive_books' дозволяє покласти результат в окремий атрибут, не затираючи повний store.books.
76. D - Dependency Inversion Principle⚑
💡 27/100 · computer_science/solid.md
Summary
Бізнес-логіка залежить від абстракції, а не від конкретної інфраструктури (БД, HTTP); конкретну реалізацію підставляють ззовні - обидва рівні залежать від абстракції.
D - Dependency Inversion Principle - DIP - Принцип інверсії залежностей Модулі вищих рівнів не мають залежати від модулів нижчих рівнів. Обидва типи модулів повинні залежати від абстракцій. Це досягається через використання інтерфейсів та ін'єкції залежностей (dependency injection).
Типова плутанина
DIP часто плутають із простим наслідуванням ("клас Dog залежить від Animal"). Це не та інверсія: тут і так модуль нижчого рівня залежить від модуля вищого рівня. DIP - про те, що бізнес-логіка (модуль вищого рівня) не повинна знати про конкретну реалізацію інфраструктури (БД, HTTP-клієнт, файлова система); вона залежить лише від абстрактного інтерфейсу, а конкретну реалізацію підставляють ззовні. Так само й сама інфраструктура реалізує цей інтерфейс - тобто обидва залежать від абстракції, а не одне від одного.
Реалізація
from abc import ABC, abstractmethod
class OrderRepository(ABC): # abstraction (interface)
@abstractmethod
def save(self, order): ...
class PostgresOrderRepository(OrderRepository):
def save(self, order):
# SQL INSERT ...
...
class MongoOrderRepository(OrderRepository):
def save(self, order):
# collection.insert_one(...)
...
class CheckoutService: # high-level module
def __init__(self, orders: OrderRepository):
self._orders = orders # depends on abstraction, not concrete DB
def checkout(self, order):
# business logic ...
self._orders.save(order)
CheckoutService нічого не знає про Postgres чи Mongo - він приймає будь-який OrderRepository. Перехід з однієї БД на іншу зводиться до підстановки іншої реалізації; бізнес-логіка не змінюється.
Інтерфейс належить модулю вищого рівня. Найчастіша помилка - вважати, що DIP - це просто "залежати від абстракцій". Сама по собі абстракція нічого не інвертує: проєкт може бути весь на інтерфейсах і не мати жодної інверсії. Інверсія відбувається лише тоді, коли інтерфейс оголошений у модулі вищого рівня (належить його межі), а модуль нижчого рівня цей інтерфейс реалізує - тоді стрілка залежності від інфраструктури розвертається й указує на бізнес-логіку. Якщо ж інтерфейс належить модулю нижчого рівня, бізнес-логіка як залежала від нього, так і залежить - назва "інверсія" не виправдана. Звідси перевірка DIP: визначити межі модулів і впевнитися, що погранична абстракція лежить поруч зі споживачем (OrderRepository - у доменному модулі поряд із CheckoutService, а не в модулі Postgres).
Зв'язок з DI
DIP - принцип, куди має йти залежність (на абстракцію, не на конкретний клас). Dependency Injection - механізм, як конкретну реалізацію передати у клас, що залежить від абстракції (через конструктор, сетер чи параметр виклику). DIP можна виконати без DI-контейнера - достатньо передавати залежність вручну, як у прикладі вище.
typing.Protocol і DI через аргументи. Абстракцію зручно задати typing.Protocol-ом замість abc.ABC - тоді конкретним реалізаціям не треба наслідуватися:
from typing import Protocol
class OrderStorage(Protocol):
def get_order(self, order_id: int) -> dict: ...
class OrderProcessor:
def __init__(self, storage: OrderStorage): # depends on protocol, not concrete DB
self.storage = storage
У проді передають справжню БД (OrderProcessor(PostgresStorage())), у тестах - заглушку із захардкодженим словником ("DI для бідних"). Коли компонентів десятки, є dependency-injector; а FastAPI з системою Depends вбудував DI у сам фреймворк і зробив його звичним у своїй екосистемі.
Ціна DIP. Інверсія має вартість: без неї одразу видно реалізацію, а з нею її ще треба знайти за абстракцією. Якщо абстракція виділена погано - неповно описує вимоги або навпаки забруднена зайвими деталями - платять ціну DIP, не отримуючи його переваг (гнучкості, ізоляції змін, простоти тестування). Тому інверсію застосовують там, де абстракція справді осмислена, а не до кожної залежності.
Links
77. Що таке Docker?⚑
💡 27/100 · infrastructure/docker.md
Docker - це інструмент для для створення, розгортання та управління контейнерами, який дозволяє упаковувати додаток і всі його залежності в єдиний контейнер, який може бути запущений на будь-якій платформі, що підтримує Docker, забезпечуючи ізоляцію, переносимість та консистентність роботи додатків у різних середовищах.
Контейнери не знають, що поруч розгорнуті інші контейнери з додатками, вони повністю ізольовані один від одного. У кожному контейнері можна налаштувати середовище, необхідне саме для цього додатка.
Контейнери Docker засновані на образах, які включають все необхідне для запуску програми: код, бібліотеки, системні інструменти та налаштування.
Docker надає наступні переваги в процесі розробки
- Підвищення портативності додатків. Docker дозволяє упаковувати додаток і всі його залежності в єдиний контейнер, який може бути запущений на будь-якій платформі, що підтримує Docker. Це підвищує портативність додатків і прискорює їх розгортання.
- Прискорення процесу розробки. Docker дозволяє швидко створювати та запускати контейнери, що прискорює процес розробки та тестування додатків.
- Покращення безпеки. Docker забезпечує ізоляцію контейнерів, що дозволяє уникнути конфліктів між додатками та покращує безпеку додатків.
- Збільшення масштабованості. Docker дозволяє швидко масштабувати додатки, додаючи нові контейнери при необхідності.
78. Що робить SELECT FOR UPDATE?⚑
💡 27/100 · infrastructure/sql.md
SELECT FOR UPDATE – це SQL-конструкція, яка використовується для блокування рядків, що вибираються під час виконання запиту, щоб уникнути конкурентних змін даних. Ця конструкція зазвичай застосовується в транзакціях для забезпечення консистентності даних.
Вона виконує ексклюзивне блокування рядків, які відповідають умовам запиту, і дозволяє тільки одному процесу або транзакції читати або змінювати ці рядки. Інші транзакції, що спробують виконати SELECT FOR UPDATE для цих же рядків, будуть чекати, поки поточна транзакція завершиться (коміт або ролбек).
Зазвичай використовується, коли є потреба прочитати дані, провести певні обчислення та потім оновити ці дані без ризику, що інша транзакція зможе змінити їх між читанням і записом. Це важливо для уникнення проблем, таких як race conditions.
Недоліком є те, що використання SELECT FOR UPDATE може призвести до зменшення швидкодії за рахунок блокування ресурсів та можливих deadlock-ів. Тому його треба застосовувати з обережністю в системах з високою конкуренцією запитів.
BEGIN;
-- Selecting rows for update
SELECT * FROM orders
WHERE status = 'pending'
FOR UPDATE;
-- Performing update based on previous select
UPDATE orders
SET status = 'processing'
WHERE status = 'pending';
COMMIT;
Сценарії застосування
- Грошові перекази, робота з рахунками. Щоб уникнути "подвійного списання", блокується рядок балансу до коміту.
- Редагування критичних бізнес-сутностей (контракт, налаштування), які не можна змінювати паралельно.
- Розбір черг у БД - типовий патерн
SELECT ... FOR UPDATE SKIP LOCKEDдля воркерів, які беруть наступне необроблене завдання.
Обмеження
- Якщо читається багато рядків, а змінюється лише один - блокувати все буде надмірно.
- Якщо конфлікти трапляються часто і система масштабується - краще оптимістична блокування через
version/updated_at(WHERE ... AND version = ?). - Якщо важлива висока паралельність -
FOR UPDATEможе спричиняти дедлоки і простої.
Типи локів
FOR UPDATE- X-lock (exclusive): блокує і читанняFOR UPDATE/FOR SHARE, і запис іншими транзакціями. ЗвичайнийSELECTбез локу далі читає снапшот через MVCC.FOR SHARE- S-lock (shared): дозволяє іншим читати зFOR SHARE, але блокуєFOR UPDATEта запис. Кілька shared-локів співіснують.FOR NO KEY UPDATE- слабший заFOR UPDATE: не заважає FK-перевіркам в інших транзакціях.FOR KEY SHARE- слабший заFOR SHARE: блокує лише зміну ключа (саме цей режим автоматично використовують FK-перевірки), дозволяючи звичайніUPDATEнеключових полів. Як і інші row-locks, тримається до кінця транзакції.
Опції при конфлікті
- За замовчуванням транзакція чекає, поки lock зніметься.
NOWAIT- одразу повертає помилку замість чекати.SKIP LOCKED- пропускає вже заблоковані рядки.
Концептуально оптимістичне vs песимістичне блокування (з порівнянням trade-off-ів, MVCC і retry з exponential backoff + jitter) - див. architecture/system_design.md.
79. Що таке гонка (race condition)? Як з нею боротися?⚑
💡 27/100 · python/gil_threads_processes.md
Гонка (race condition) — це помилка в програмі, яка виникає, коли результат виконання коду залежить від того, як чергуються операції кількох потоків чи процесів, що одночасно звертаються до спільного ресурсу. Такі ситуації можуть призводити до некоректної поведінки програми, непередбачуваних результатів або навіть до аварійного завершення роботи.
Гонка виникає, якщо:
- Декілька потоків чи процесів мають спільний доступ до ресурсу (наприклад, змінної або файлу).
- Відсутні засоби синхронізації, які гарантують коректний порядок виконання операцій над ресурсом.
Саме цей приклад з інкрементом на Python 3.10+ зазвичай дає коректний результат, але це не означає, що гонки зникли - вони лишаються можливими у складніших, багатокрокових операціях над спільним станом, які GIL не робить атомарними. У Python 3.10 було введено оптимізацію, яка змінила спосіб роботи GIL. Раніше GIL міг звільнятися і захоплюватися на будь-якій байткод-інструкції, але тепер це відбувається лише на певних "спеціальних" байткод-інструкціях, які називаються eval breakers.
import threading
class Counter:
def __init__(self):
self.value = 0
def increase(self):
self.value += 1
def work(counter: Counter, operations_count: int):
for _ in range(operations_count):
counter.increase()
def run_threads(counter: Counter, count: int, operations_count: int):
threads = []
for _ in range(count):
t = threading.Thread(target=work, args=(counter, operations_count))
t.start()
threads.append(t)
for t in threads:
t.join()
if __name__ == '__main__':
threads_count = 10
operations_per_thread_count = 1_000_000
counter = Counter()
run_threads(counter, threads_count, operations_per_thread_count)
expected_result = threads_count * operations_per_thread_count
actual_result = counter.value
print(f'Expected: {expected_result}, actual: {actual_result}')
Коли Python виконує код, він перекладає його у набір байткод-інструкцій. Якщо спростити, для нашого прикладу це виглядає приблизно так:
LOAD_ATTR— завантажує значення змінноїvalueLOAD_CONST— завантажує константу 1BINARY_OP— додає 1 до значенняvalueSTORE_ATTR— зберігає результат у зміннуvalueJUMP_BACKWARD— перемикає на наступну ітерацію
У 1-4 інструкціях немає eval breakers, тобто інструкцій, які змушують GIL звільнятися і захоплюватися знову. Це означає, що вся ця послідовність (тобто операція додавання) виконується атомарно — без втручання інших потоків. Таким чином, кожна ітерація виконується (майже) без гонки потоків.
Інструкції, які можуть (але не обов’язково) викликати звільнення GIL, це JUMP_BACKWARD (або JUMP_ABSOLUTE, залежно від версій Python), яка виконує перехід на початок циклу і може перевіряти GIL у довгих циклах, або CALL (у версіях до Python 3.11 — CALL_FUNCTION), що може звільнити GIL у випадках, коли викликається функція C, I/O або складні обчислення. Байт-код інструкції для програми можна побачити за допомогою модуля dis у Python, що використовується для дизасемблювання (розбору) байткоду, який виконує інтерпретатор Python.
Але якщо в коді між 1-4 інструкціями додасться нова, що викличе eval breakers, який може звільнити GIL, то проблема розірвання цілісної операції додавання перемиканням потоків повернеться.
Як боротися з гонкою
- Використовувати блокування (
threading.Lock): Блокування дозволяє гарантувати, що лише один потік чи процес може змінювати ресурс у певний момент.
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
with lock: # Ensure only one thread modifies the counter
counter += 1
threads = [threading.Thread(target=increment) for _ in range(2)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter) # Will be always 200000
- Використовувати атомарні операції: У деяких випадках можна використовувати спеціальні функції або модулі для виконання атомарних операцій. Наприклад,
multiprocessing.Valueз atomic lock. - Семафори: Якщо потрібно контролювати доступ до ресурсу для певної кількості потоків чи процесів.
- Queue: Для передачі даних між потоками чи процесами краще використовувати безпечні черги (
queue.Queueабоmultiprocessing.Queue), що автоматично забезпечують синхронізацію. - Переосмислення архітектури програми: Уникнення спільного доступу до змінних, використання іммутабельних структур даних та передачі повідомлень замість спільного ресурсу.
- Глобальний блок інтерпретатора (GIL): У CPython GIL частково запобігає деяким видам гонок, але його наявність не означає, що гонки не можуть виникнути.
Links
- Thinking about Concurrency, Raymond Hettinger, Python core developer
- https://habr.com/ru/articles/764420/
80. Що таке JWT (JSON Web Token)⚑
💡 27/100 · web_development/http.md
JSON Web Token (JWT) - ("джот") - це стандартизований, у деяких випадках підписаний і/або зашифрований формат для передачі інформації між сторонами у вигляді JSON-об'єкта. Він широко використовується для автентифікації та авторизації в сучасних веб-додатках.
JWT складається з трьох частин, розділених крапками: Header, Payload, Signature.
- Header: містить тип токена (JWT) та алгоритм підпису (наприклад, HS256 - це алгоритм підпису, а не шифрування).
- Payload: містить інформацію про користувача або сесію (claims). Може включати стандартні claims (наприклад,
sub,exp) і кастомні. - Signature: забезпечує цілісність токена, підписуючи header і payload за допомогою секретного ключа.
JWT не шифрується, а кодується у форматі Base64. Тому інформація у payload доступна будь-кому, хто має токен, якщо не використовується додаткове шифрування.
JWT може використовуватись як токен авторизації (Access Token) або для передачі інформації, що не потребує додаткового запиту до сервера. Токен має обмежений термін дії, вказаний у claim exp. Після його закінчення необхідно оновити токен за допомогою механізмів, таких як Refresh Token.
Переваги JWT
- Самодостатність: містить всю необхідну інформацію.
- Незалежність від стану: не потребує збереження на сервері (на відміну від сесій).
- Компактність: легко передається у HTTP-заголовках.
Приклад створення та перевірки JWT
import jwt
from datetime import datetime, timedelta
SECRET_KEY = "your_secret_key" # Secret key for signing the token
def create_jwt(user_id): # Function to create a JWT
payload = {
"user_id": user_id,
"exp": datetime.utcnow() + timedelta(minutes=30), # Token valid for 30 minutes
"iat": datetime.utcnow() # Issued at time
}
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
return token
def verify_jwt(token): # Function to decode and verify JWT
try:
decoded = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
return decoded
except jwt.ExpiredSignatureError:
return "Token has expired"
except jwt.InvalidTokenError:
return "Invalid token"
jwt_token = create_jwt(123)
print("Generated JWT:", jwt_token)
decoded_data = verify_jwt(jwt_token)
print("Decoded JWT:", decoded_data)
Відкликання токена (logout from all devices)
JWT самодостатній і не зберігається на сервері, тому видалити його до закінчення exp просто так не можна - вихід, бан, зміна пароля чи "вийти з усіх пристроїв" потребують відкликання (revocation). Для цього вводять серверний стан (частково жертвуючи stateless-моделлю): у payload кладуть унікальний jti (id токена), а сервер тримає список відкликаних/активних jti.
- Blocklist (denylist). За замовчуванням токен дійсний; у сховище кладуть
jtiсаме відкликаних токенів і перевіряють кожен запит. Простіше, запис лише при відкликанні. - Allowlist. У сховищі -
jtiусіх виданих активних токенів; відкликання = видалити запис. Токен дійсний, лише якщо присутній. Дає повний контроль, але запис на кожну видачу. - Сховище - Redis із TTL, рівним залишку життя токена: записи самоочищаються, не треба окремої таблиці й cron-задачі на чистку. Головне - щоб TTL у Redis збігався з
expтокена, інакше можлива розбіжність "у сховищі вже немає, а токен ще валідний". - Практичний компроміс: короткоживучий access-токен (1-5 хв, не перевіряють у сховищі)
- revocable refresh-токен (перевіряють). Так на кожен запит немає звернення до сховища, але відкликання все одно працює - максимум через час життя access-токена.
- Logout from all devices: відкликати всі
jtiкористувача (зберігатиuser_id → jti) або тримати в payload версію/epoch користувача, яку звіряють і піднімають при виході з усіх пристроїв - тоді старі токени стають недійсними без поіменного списку.
Refresh-токен зберігають у HttpOnly/Secure-кукі, а не в localStorage (захист від XSS).
Підпис проти шифрування (JWT проти JWE)
Стандартний JWT підписується, а не шифрується. Payload закодований лише Base64URL, тож будь-хто з токеном може його прочитати - конфіденційні дані туди класти не можна. Захист дає не таємність, а цілісність: сервер перераховує підпис над header+payload своїм ключем і звіряє з полем Signature.
Тому атака "декодувати → змінити user_id → закодувати назад" не проходить, навіть якщо підставлений user_id реально існує в БД: підпис, перерахований з новим payload, не збігається з ключем сервера, і токен відхиляється (InvalidSignatureError). Зловмисник не може підробити валідний підпис, не знаючи секрет (HS*) чи приватний ключ (RS*/ES*).
Якщо payload потрібно ще й приховати, застосовують JWE (JSON Web Encryption) - той самий токен, але додатково зашифрований: вміст недоступний для читання. Ціна - витрати CPU на шифрування/розшифрування при кожній перевірці.
Де зберігати токен у браузерному клієнті. Поширене питання для SPA. Ключова теза: localStorage, sessionStorage, не-HttpOnly cookie та IndexedDB однаково вразливі - усе, що читає код розробника, читає й код зловмисника при XSS. Тому "localStorage чи cookie" - хибна дилема; різниця між ними тут несуттєва. Стратегії зниження ризику:
- Не тримати токен у JS взагалі. Stateful-сесія в
HttpOnly/Secure/SameSite-кукі (+ захист від CSRF, бо кукі шлеться автоматично). Найпростіше; ціна - відмова від переваг stateless-токенів. - BFF (Backend-for-Frontend) як token-proxy. Тонкий бекенд - confidential client: він тримає access-токен у себе, а клієнту видає лише
HttpOnly-сесійну кукі й проксує запити до API, підставляючи реальний токен. Токен ніколи не потрапляє в JS. Нині це рекомендований патерн автентифікації SPA (потребує захисту від CSRF). Відрізняється від BFF-агрегації відповідей (див.architecture/microservices.md) лише призначенням. - Service worker як проксі. SW працює в окремому контексті без доступу до DOM/JS-сторінки; токен ізольований у ньому. Має зареєструватися до будь-якого auth-flow, інакше XSS встигне ініціювати свій flow в обхід. Імунний і до XSS-крадіжки, і до CSRF.
- Тільки в пам'яті (closure-змінна). Токен у замиканні token-manager'а, не персиститься. Втрачається при перезавантаженні, і зловмисник усе одно може перехопити запит через monkey-patching
fetch. Частковий захист. - Розщеплення токена. Половину тримають у
HttpOnly-кукі: напр. підпис JWT - у кукі, header+payload - на клієнті (Two-Cookie JWT); або в payload кладуть хеш випадкового рядка, сам рядок - уHttpOnly-кукі. Сам по собі викрадений токен стає недостатнім.
PKCE тут не допомагає: воно захищає обмін authorization code на токен, а не сам токен у сховищі - поширена плутанина. Викрадення токена ≠ єдиний наслідок XSS: маючи XSS, зловмисник усе одно може слати легітимні запити з браузера жертви, тож захист сховища - лише частина.
Links
81. CQRS⚑
💡 26/100 · architecture/architecture_patterns.md
Summary
CQRS (Command Query Responsibility Segregation) - розділення моделей запису (commands) і читання (queries) у системі. Команди змінюють стан, запити лише читають; обидві сторони можуть мати окремі моделі даних, окремі сховища і масштабуватися незалежно.
CQS проти CQRS. Це різні рівні одного принципу, які часто плутають. CQS (Command-Query Separation, Бертран Меєр) - правило рівня методу: метод є або командою (змінює стан, нічого корисного не повертає), або запитом (повертає дані без побічних ефектів), але не одночасно. CQRS піднімає цю ідею на рівень архітектури: окремі моделі (а часто й сховища) для запису і читання. CQS не вимагає окремих сховищ; це дешевий дисциплінарний прийом усередині однієї моделі. Можна застосовувати CQS, не вдаючись до CQRS.
Принцип роботи
Замість єдиного коду й сховища, що обслуговує і запис, і читання, система ділиться на дві сторони:
- Command side - приймає команди (
CreateOrder,CancelPayment), валідує бізнес-правила, змінює стан у write-сховищі. Оптимізована під нормалізовану модель, транзакції, цілісність. - Query side - обслуговує читання за окремою read-моделлю, оптимізованою під конкретні запити (денормалізовані view, пошукові індекси, агрегати). Read-сховище може бути зовсім іншим: ClickHouse для аналітики, Elasticsearch для пошуку, Redis для кешу.
Між сторонами потрібен механізм синхронізації: команди публікують події (через Transactional Outbox або CDC), read-side оновлює свою модель.
Мотивація розділення
- Різні патерни доступу. Запис - одиничні цілісні операції; читання - агрегати, повнотекстовий пошук, аналітика. Одна модель добре обслуговує лише одне з двох.
- Незалежне масштабування. Read-side зазвичай має на порядки більше трафіку; його масштабують репліками читання або окремим сховищем, не зачіпаючи write-side.
- Оптимізація під запити. Read-модель денормалізована саме під запити користувачів - без
JOIN'ів через десятки таблиць.
Реалізації
- Найпростіший варіант: read-репліки тієї ж БД. Master приймає
INSERT/UPDATE, slaves обслуговуютьSELECT. CQRS лише на рівні маршрутизації запитів, без окремих моделей. - Окрема read-модель у тому ж сховищі: materialized view, що періодично оновлюються командними подіями. Простіше за окрему БД, але обмежено можливостями materialized view.
- Окреме read-сховище: аналітичні запити йдуть у ClickHouse, наповнений через CDC або WAL-стрім з операційної Postgres. Пошук - в Elasticsearch, наповнений Outbox'ом. Це повноцінний CQRS.
Eventual consistency
Read-модель оновлюється асинхронно і відстає від write-моделі на час реплікації / процесингу подій. Це означає, що користувач, який щойно виконав команду, може не побачити свого результату у наступному GET-запиті.
Способи пом'якшити:
- Read-your-writes для UI: після успішної команди UI оновлює локальний стан без додаткового GET'а.
- Версіонування ресурсів: клієнт надсилає очікувану версію, query side чекає її появи або повертає stale-мітку.
- Sticky read для першого запиту після команди: маршрутизація на write-side або read-репліку з мінімальним лагом.
Дві проблеми загострюються, коли read-сторона - окреме сховище з асинхронною проєкцією:
- Дублювання даних через відсутність read-your-writes. За асинхронної проєкції користувач після команди не бачить результату у read-моделі, припускає, що дані не збереглися, і повторює введення - утворюється дублікат. Проявляється лише під навантаженням: на dev-машині лагу між сторонами немає, тож цей дефект не виявляють у тестуванні. Пом'якшують ті самі прийоми, що й вище.
- Відставання проєкції під навантаженням. Read-модель оптимізована під читання, але оновлювати її доводиться з частотою надходження команд. На піку проєкція не встигає, і read-side відстає на хвилини - стає непридатним саме тоді, коли навантаження максимальне.
Зв'язок з Event Sourcing
CQRS часто йде разом з Event Sourcing (стан - похідна від послідовності подій), але це не є обов'язковою умовою. CQRS можна побудувати поверх звичайної реляційної БД, не зберігаючи історії подій як джерела істини. Event Sourcing без CQRS теж зустрічається, проте рідше.
Обмеження застосування
- Простий CRUD-сервіс без специфічних аналітичних запитів. Розділення лише додасть синхронізаційного коду. Накладати на CRUD саме eventually-consistent варіант особливо шкідливо - породжує описане дублювання даних.
- Команди й читання працюють з тими ж даними у тих самих структурах. Read-модель не дає виграшу.
CQRS - інвестиція у складність; виправдовується лише там, де патерни запису й читання справді розходяться. Це архітектурний стиль, не універсальний best practice: інструмент, який застосовують там, де доречно, а не за замовчуванням.
Links
82. Dependency Injection - DI - Впровадження залежності⚑
💡 26/100 · architecture/architecture_patterns.md
Dependency Injection (DI) - шаблон проектування, який полягає в передачі залежностей об'єктам під час їх створення. Тобто ми передаємо залежність, а не створюємо її в класі. Залежність - це інший клас, який потрібен нам в нашому поточному класі (наприклад, клас для доступу в БД).
Це робить код більш гнучким, оскільки об'єкти не прив'язані до конкретних реалізацій залежностей, і їх можна легко замінити або модифікувати.
class DatabaseConnection:
def __init__(self, db_url):
self.db_url = db_url
def connect(self): # Logic to establish a database connection
pass
class UserRepository:
def __init__(self, db_connection):
self.db_connection = db_connection
def get_user(self, user_id): # Logic to retrieve user from the database using db_connection
pass
db_connection = DatabaseConnection("mysql://username:password@localhost/db_name")
user_repository = UserRepository(db_connection)
У цьому прикладі db_connection є залежністю для UserRepository. Dependency Injection дозволяє замінити конкретну реалізацію DatabaseConnection, не змінюючи логіку UserRepository.
Три форми впровадження.
Залежність передають у клас чи функцію одним із трьох способів:
- Через параметр функції/методу - передати залежність як ще один аргумент.
- Через параметр конструктора - найпоширеніший варіант: залежність зберігають у
__init__. - Через атрибут екземпляра - присвоїти залежність після створення об'єкта (застосовують рідко, зокрема при двофазній ініціалізації).
# 1. Function/method parameter
def clear_users(cursor):
cursor.execute("TRUNCATE users;")
# 3. Instance attribute, assigned after construction
dao = UsersDAO()
dao.cursor = connection.cursor()
Підміна залежності в тестах. Передану ззовні залежність у тесті підмінюють фейковою, тож перевірка логіки UserRepository не торкається реальної БД чи мережі (за умови, що get_user делегує виклик у self.db_connection, а не створює з'єднання сам):
class FakeConnection: # in-memory stub instead of a real DB
def get_user(self, user_id):
return {"id": user_id, "name": "Alice"}
repo = UserRepository(FakeConnection()) # inject the fake via the constructor
assert repo.get_user(1) == {"id": 1, "name": "Alice"} # no DB, no network
Без DI залежність створювалася б усередині UserRepository, і підмінити її в тесті без патчингу було б неможливо.
Поширені хибні уявлення:
- DI - це щось зі світу Java, у моїй мові його немає. Хибно: достатньо мати можливість передати посилання на функцію чи об'єкт. Може застосовуватись і до Python, і до JavaScript, C, Go.
- DI потрібен лише великим enterprise-застосункам. Радше критичний для великих. Малі застосунки без автотестів можуть існувати без нього, але з ускладненням потреба зростає.
- Для DI потрібен спеціальний фреймворк/IoC-контейнер. Не потрібен: DI - можливість самої мови. Контейнери лише спрощують побудову графа залежностей; необережне їх використання навпаки може позбавити DI.
DI (механізм передачі залежності) не слід плутати з принципом інверсії залежностей (DIP) - вони служать різним цілям. DIP описаний у computer_science/solid.md.
83. Моноліт vs Мікросервіси⚑
💡 26/100 · architecture/microservices.md
Summary
Моноліт підходить для простих рішень і швидкого старту, тоді як мікросервіси необхідні для великих, складних проєктів, які вимагають масштабованості та модульності.
Вибір між монолітною архітектурою та мікросервісами залежить від масштабів проєкту, команди та бізнес-вимог. Кожен підхід має свої сильні сторони, які слід враховувати залежно від конкретної ситуації.
Сценарії для Моноліту
- Проєкт невеликий, із чітко визначеною функціональністю та обмеженими масштабами.
- Команда розробників мала, і всі члени працюють над одним кодом.
- Потрібно швидко створити MVP (мінімально життєздатний продукт).
- Інфраструктурні ресурси обмежені, немає необхідності в складній оркестрації.
- Немає значних вимог до незалежного масштабування різних компонентів.
- Основний фокус на простоті розробки, підтримки та розгортання.
Сценарії для Мікросервісів
- Проєкт великий і передбачає постійне зростання як за функціональністю, так і за навантаженням.
- Є чітка потреба в масштабуванні окремих частин системи, наприклад, модуля оплати чи пошуку.
- У проєкті працює велика команда, яка може бути розділена на кілька груп, кожна з яких відповідає за окремий сервіс.
- Проєкт потребує високої гнучкості у виборі технологій для різних частин системи.
- Необхідно забезпечити високу надійність: збої в одному сервісі не повинні впливати на роботу інших.
- Бізнес-вимоги швидко змінюються, і потрібна модульність для швидкого впровадження нових функцій.
Розрізнення мікросервісів і модульного моноліту
Поширена помилка на співбесіді - назвати ознакою інкапсуляцію або взаємозамінність ("кожен сервіс замкнений у собі", "сервіс можна підмінити іншою реалізацією"). Це не відмітні ознаки: добре спроєктований модуль моноліту так само інкапсульований, а взаємозамінність реалізацій - це LSP, який однаково діє і для класів. Відрізняють архітектури не властивості коду, а спосіб розгортання та володіння ресурсами - те, що видно ззовні, зі схем і конфігурації:
- Окремий процес і незалежний деплой. Модулі моноліту виконуються в одному процесі і деплояться разом; мікросервіси - окремі процеси/контейнери з власним циклом релізу. Це необхідна ознака: без неї це моноліт незалежно від того, наскільки "сервісним" виглядає код.
- Власна БД на сервіс (див. Database per Service). Якщо кожен компонент має своє сховище й не ходить у чужі таблиці - це майже напевно мікросервіси; модулі моноліту зазвичай ділять одну БД.
- Поліглотний рантайм. Компоненти на різних мовах (Python + Go + JVM) - достатня, хоча й не необхідна ознака: класи моноліту живуть в одному рантаймі, тож різні мови майже завжди означають окремі сервіси за мережевою межею.
Жодна ознака окремо не є абсолютною (мікросервіси бувають одномовними; спільна БД - відомий антипатерн distributed monolith), але разом вони дають надійне розрізнення.
84. O-велике при оцінці складності. Як і для чого використовується⚑
💡 26/100 · computer_science/algorithmes.md
O-велике описує швидкість роботи алгоритму (не час).
85. Які бувають види тестів?⚑
💡 25/100 · computer_science/testing.md
Юніт-тести (Unit Tests) – це тести, які перевіряють окремі модулі коду (функції, методи, класи) на коректність роботи. Ціль таких тестів - переконатися, що кожен модуль працює правильно і не впливає на інші модулі.
Інтеграційні тести (Integration Tests) – це тести, які перевіряють, як різні модулі взаємодіють один з одним. Ціль таких тестів - переконатися, що всі модулі працюють разом правильно і не виникають конфлікти.
Системні тести (System Tests) - це тести, які перевіряють всю систему в цілому, включаючи всі модулі та взаємодії між ними. Мета таких тестів – переконатися, що система працює правильно та відповідає вимогам.
Приймальні тести (Acceptance Tests) – це тести, які виконуються замовником або користувачем для перевірки, чи відповідає система його вимогам та очікуванням. Мета таких тестів – переконатися, що система повністю відповідає вимогам замовника.
Навантажувальні тести (Load Tests) - це тести, які перевіряють, як система працює при різних навантаженнях та обсягах даних. Ціль таких тестів - переконатися, що система здатна обробляти великі обсяги даних і не ламається при великому навантаженні. У Python-екосистемі популярний інструмент - Locust (сценарії навантаження пишуться кодом Python: кількість користувачів, частота запитів); також використовують k6, Apache JMeter, wrk. Окрім перевірки "чи витримає", навантажувальні тести допомагають у capacity planning - підібрати потрібні ресурси/розмір інстансу під очікуваний трафік.
Смоук тести (Smoke Tests) – це тести, які виконуються перед кожним релізом для перевірки, що система працює та не виникають критичні помилки. Мета таких тестів – переконатися, що система готова до релізу та не містить критичних помилок.
Тестування продуктивності (Performance testing) - визначає наскільки тестований застосунок стабільний, а також досліджує показники швидкості реакції програми на зовнішні впливи при різному за характером та інтенсивністю навантаження. Метою тестування є виявлення недоліків і вразливостей в системі, визначення швидкості завантаження даних і їх обробки, надійності програми.
Links
- Пирамида тестов на практике
- Тесты, которые должен писать разработчик
- Различные виды тестирования и их особенности
86. Coroutines - Корутини - Співпрограми⚑
💡 25/100 · python/async.md
Корутина - це спеціальний вид асинхронних функцій, яка може призупиняти своє виконання, і продовжити виконання з місця зупинки. На відміну від звичайних функцій, які виконуються наново з початку. Поведінка корутин схожа на поведінку генераторів. Для визначення корутини починаючи з Python 3.5+ використовують ключове слово async. Вони використовуються для виконання асинхронних операцій, де важливо чекати на відповідь без блокування основного потоку виконання.
Інше визначення - співпрограма — це термін, який позначає завдання, яке планується циклом подій у програмі замість операційної системи.
У корутині можна використовувати ключове слово await, що вказує на асинхронне очікування результату виконання іншої корутини чи асинхронної функції.
Корутини мають багато спільного з потоками, але на відміну від потоків, вони віддають керування лише тоді, коли викликають іншу корутину, і вони не використовують так багато пам'яті.
Корутини є основою для асинхронного програмування в Python і дозволяють виконувати багатозадачні операції без необхідності великої кількості потоків чи процесів.
Корутини можна визначати і створювати у звичайному Python-коді, але запускати їх можна лише в циклі подій.
87. Варіанти реалізації шаблону Singleton у Python⚑
💡 25/100 · python/general.md
Декоратор
def singleton(class_):
instances = {}
def getinstance(*args, **kwargs):
if class_ not in instances:
instances[class_] = class_(*args, **kwargs)
return instances[class_]
return getinstance
@singleton
class MyClass(BaseClass):
pass
Переваги
- Декоратори часто є більш інтуїтивно зрозумілими, ніж множинне успадкування. Недоліки
- Хоча об'єкти, створені за допомогою
MyClass(), будуть справжніми singleton об'єктами, самMyClassє функцією, а не класом, тому ви не можете викликати методи класу з нього. - Ускладнює тестування.
Базовий клас
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if not isinstance(cls._instance, cls):
cls._instance = object.__new__(cls, *args, **kwargs)
return cls._instance
class MyClass(Singleton, BaseClass):
pass
Переваги
- Це справжній клас. Недоліки
- Множинне успадкування ускладнює код.
__new__може бути перезаписаний під час успадкування від другого базового класу
Метакласи
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class MyClass(BaseClass, metaclass=Singleton):
pass
Переваги
- Це справжній клас.
- Автоматично застосовується при наслідуванні.
- Використовує метакласи за їхнім прямим призначенням. Недоліки
- Немає
Модуль
Найпростіший спосіб у Python - винести об'єкт на рівень модуля. Сам модуль уже є singleton: інтерпретатор імпортує його лише раз і кешує в sys.modules, тож скільки б разів його не імпортували, повертається той самий об'єкт зі станом.
Переваги
- Не потрібні ні метакласи, ні
__new__, ні декоратор. Недоліки - Об'єкт створюється при імпорті (eager), а не ліниво.
- Змінний глобальний об'єкт зв'язує віддалені частини коду: тести перестають бути незалежними, бо поділяють спільний стан (приклади у stdlib -
os.environ,logging.root). - I/O при імпорті - анти-патерн: помилка під час імпорту критичніша за помилку під час виконання (логування й головний
try/exceptще не активні), а модуль можуть імпортувати, але так і не викликати. Тому side-effect'и (відкриття файлів, сокетів) відкладають до першого виклику.
Singleton проти "well-known object". Класичний singleton - це клас, що завжди повертає той самий екземпляр. Об'єкти на кшталт None, True, False - не зовсім singleton'и, а "well-known objects": це не клас, а конкретний унікальний об'єкт, гарантовано один на процес.
Monostate / Borg. Альтернатива до Singleton (Alex Martelli): замість обмеження кількості екземплярів дозволяють створювати скільки завгодно об'єктів, але всі вони поділяють спільний стан. Досягається підміною self.__dict__ на спільний для класу словник - усі атрибути читаються й пишуться в одну й ту саму таблицю.
class Borg:
_shared_state = {}
def __init__(self):
self.__dict__ = self._shared_state # every instance shares one __dict__
a, b = Borg(), Borg()
a.x = 42
print(b.x) # 42 - different objects (a is not b), shared state
На відміну від Singleton, тут a is not b (identity різна), що буває зручніше: код, який очікує звичайні екземпляри, працює без змін, а стан лишається спільним. Недолік той самий, що в будь-якого глобального стану - неявне зв'язування й проблеми з тестами.
Links
88. Що таке компільована мова? Що таке інтерпретована мова?⚑
💡 25/100 · python/general.md
Компільована мова програмування - мова програмування, вихідний код якої перетворюється компілятором в машинний код і записує в файл з особливим заголовком/розширенням, для подальшої ідентифікації цього файлу, як виконуваного операційною системою.
Компіляція - збірка програми, яка включає трансляцію всіх модулів програми, написаних на одному чи кількох вихідних мовах програмування в еквівалентні програмні модулі на низькорівневу мову, близьку до машинного коду або на машинному коді.
Інтерпретована мова - це мова програмування, код якої перетворюється на виконувані команди безпосередньо під час виконання програми (не перетворюється на машинний код перед виконанням), методом інтерпретації: оператори програми окремо транслюються і відразу виконуються (інтерпретуються) з допомогою інтерпретатора.
Інтерпретація - порядковий аналіз, обробка і виконання вихідного коду програми або запиту.
Машинний код - система команд, яка інтерпретується безпосередньо процесором.
Процесор - електронний блок, який виконує машинні інструкції (код команди).
Трансляція в байт-код - проміжний по ефективності між прямою інтерпретацією і компіляцією.
Компільована vs інтерпретована
- Швидкість виконання програми, скомпільованої в машинний код, перевищує швидкість інтерпретованої програми
- При використанні компілятора, при внесенні змін в вихідний код програми, перед тим, як зміни можна побачити, необхідно виконати компіляцію вихідного коду.
89. Що таке ідемпотентність?⚑
💡 24/100 · architecture/architecture_patterns.md
Summary
Ідемпотентність - властивість операції, при якій повторний виклик з тими ж вхідними даними дає той самий результат, що і одноразовий виклик, без додаткових ефектів. "Можна викликати скільки завгодно разів - результат не змінюється".
Приклад:
- Перший виклик - видаляє користувача.
- Повторний виклик - нічого не змінює, користувач уже видалений.
- Обидва виклики повертають однаковий статус (наприклад,
204 No Content).
Області застосування
- HTTP-методи:
GET,PUT,DELETE- ідемпотентні за стандартом.POST- не ідемпотентний, бо створює новий ресурс при кожному виклику. - Фінансові операції - повторний запит на списання не повинен списати двічі.
- Обробка повідомлень з черги - at-least-once доставка означає, що дублі можливі; обробник має бути ідемпотентним.
- Retry-логіка - якщо запит безпечно повторити, retry на тимчасові помилки робиться без ризику.
Способи забезпечення
- Idempotency key - клієнт передає
Idempotency-Key: <uuid>у заголовку; сервер запам'ятовує результат першого виклику з цим ключем і повертає його ж на повторах. - Версіонування / умовні оновлення -
UPDATE ... WHERE version = ?ігнорує застарілі апдейти. - Унікальні бізнес-ключі -
INSERT ... ON CONFLICT DO NOTHINGгарантує, що дубль не створить запис. - Перевірка стану перед дією - "якщо вже виконано - повернути попередній результат".
90. Як event loop визначає, якій корутині передати управління?⚑
💡 24/100 · python/async.md
Між event loop і селекторами (select/poll/epoll) є прямий зв'язок - саме через них loop вирішує, кому передати управління далі.
Послідовність:
- Корутина робить
await- призупиняється, повертає управління в event loop. Якщо цеawaitна I/O (наприклад,await socket.recv()), корутина прив'язується до файлового дескриптора, який ще не готовий. - Event loop реєструє цей дескриптор у селекторі (
selectors.DefaultSelector()) - підписується на події "готовий до читання/запису". - Event loop викликає
selector.select(timeout)і чекає сигналу: "які з зареєстрованих дескрипторів готові". - Селектор повертає події - наприклад, сокет №1 готовий до читання → корутина, що чекала на нього, може бути відновлена.
- Event loop ставить цю корутину в чергу готових задач. У
Futureкожної корутини виставляється результат - він вважається завершеним. - Event loop бере з черги першу готову задачу і продовжує її з місця останнього
await.
Важливо:
- Event loop сам нічого не знає про "майбутнє" корутин - він лише реагує на сигнали селекторів.
- Корутина не вибирається "просто так" - лише якщо вона була додана в чергу (
call_soon()) або селектор повідомив про готовність ресурсу (сокет, таймер).
91. Що робить метод __init__?⚑
💡 24/100 · python/class_and_object.md
Метод __init__ є конструктором класу в Python. Він автоматично викликається при створенні нового об'єкта або екземпляра класу і використовується для ініціалізації початкових значень атрибутів цього об'єкта. У методі __init__ можна вказати параметри, які передаються при створенні об'єкта, і встановити їх як атрибути об'єкта. Це дозволяє підготувати об'єкт до подальшої роботи, встановити початкові стани та значення.
92. Спискові і словникові вирази⚑
💡 24/100 · python/coding.md
A0 = dict(zip(('a', 'b', 'c', 'd', 'e'), (1, 2, 3, 4, 5)))
A1 = range(10)
A2 = sorted([i for i in A1 if i in A0])
A3 = sorted([A0[s] for s in A0])
A4 = [i for i in A1 if i in A3]
A5 = {i: i * i for i in A1}
A6 = [[i, i * i] for i in A1]
A7 = [i if i % 2 else 0 for i in A1 if 2 < i < 8]
A8 = ','.join(str(j ** 2) for j in range(10))
---
A0 = {'a': 1, 'c': 3, 'b': 2, 'e': 5, 'd': 4}
A1 = range(0, 10)
A2 = []
A3 = [1, 2, 3, 4, 5]
A4 = [1, 2, 3, 4, 5]
A5 = {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
A6 = [[0, 0], [1, 1], [2, 4], [3, 9], [4, 16], [5, 25], [6, 36], [7, 49], [8, 64], [9, 81]]
A7 = [3, 0, 5, 0, 7]
A8 = '0,1,4,9,16,25,36,49,64,81'
93. Що таке фабрика декораторів⚑
💡 24/100 · python/decorators.md
Це функція, яка повертає декоратор. Наприклад, вам потрібен декоратор для перевірки прав. Логіка перевірки однакова, але прав може бути багато. Щоб не копіювати код, можна використати фабрику декораторів.
from functools import wraps
def has_perm(perm):
def decorator(view):
@wraps(view)
def wrapper(request):
if perm in request.user.permissions:
return view(request)
else:
return HTTPRedirect('/login')
return wrapper
return decorator
@has_perm('view_user')
def users(request):
...
Інший спосіб написання фабрики декораторів - використати об'єкт в як декоратор.
_DEFAULT_RETRIES_LIMIT = 3
class WithRetry:
def __init__(
self,
retries_limit: int = _DEFAULT_RETRIES_LIMIT,
allowed_exceptions: Optional[Sequence[Exception]] = None,
) -> None:
self.retries_limit = retries_limit
self.allowed_exceptions = allowed_exceptions or (ControlledException,)
def __call__(self, operation):
@wraps(operation)
def wrapped(*args, **kwargs):
last_raised = None
for _ in range(self.retries_limit):
try:
return operation(*args, **kwargs)
except self.allowed_exceptions as e:
logger.warning(
"retrying %s due to %s", operation.__qualname__, e
)
last_raised = e
raise last_raised
return wrapped
@WithRetry(retries_limit=5)
def run_with_custom_retries_limit(task):
return task.run()
94. Як працює підрахунок посилань (Reference Counting)?⚑
💡 24/100 · python/interpreter.md
Підрахунок посилань (Reference Counting) - механізм, який допомагає очищувати пам'ять інтерпретатору Python.
Приклад роботи Reference Counting
- Коли ми присвоюємо змінній якесь значення
a = 1000, Python не зберігає число1000у зміннійа, натомість він створює об’єкт (у цьому випадку ціле число1000) в пам’яті та зберігає посилання на цей об’єкт у зміннійа. - Якщо ми присвоїмо
aнове значення, наприклада = 2000, Python створить новий об’єкт (число2000) і змінить посилання ва, щоб воно вказувало на цей новий об’єкт. Попередній об’єкт (число1000) залишається в пам’яті, поки на нього будуть існувати інші посилання. Як тільки посилань не залишиться, його пам’ять може бути звільнена. - Якщо ми присвоїмо
b = a, то зміннаbтепер теж буде посилатися на той самий об’єкт, на який посилаєтьсяa. Тобто іa, іbвказуватимуть на одне і те ж число2000в пам’яті.
У Python кожен об’єкт у пам’яті містить поля ob_refcnt та ob_type. Вони є частинами внутрішньої структури об’єктів і відіграють ключову роль в управлінні пам’яттю та визначенні типу об’єкта.
ob_refcnt(Reference Count) — це лічильник посилань на об’єкт. Він зберігає кількість посилань, які вказують на цей об’єкт. Коли ми створюємо нове посилання на об'єкт (наприклад, присвоюємо його іншій змінній),ob_refcntінкрементується. Коли одне з посилань видаляється або змінюється (наприклад, коли змінна, яка посилалася на об’єкт, змінює своє значення),ob_refcntзменшується. Коли лічильник досягає нуля (тобто на об’єкт більше немає посилань), Python автоматично звільняє пам’ять, зайняту цим об’єктом. Цей процес є частиною механізму збирання сміття (garbage collection).ob_type(Type Information) — це вказівник на структуру, яка описує тип об’єкта. Цей вказівник посилається на відповідний тип об’єкта, наприклад ціле число, рядок, список тощо.ob_typeвизначає, які операції можна виконувати над об’єктом і як ці операції слід реалізовувати. Наприклад, для числа і рядка визначаються різні способи обчислення, копіювання та перетворення.
ob_refcnt забезпечує автоматичне керування пам’яттю, допомагаючи Python звільняти пам’ять, коли вона більше не потрібна. ob_type гарантує, що кожен об’єкт має правильний тип і що всі операції з ним виконуються відповідно до цього типу.
Перевірити скільки посилань має той чи інший об’єкт у пам’яті можна за допомогою бібліотеки sys.
import sys
print(sys.getrefcount('new object')) # 3
print(sys.getrefcount(0)) #1000000791
a = 0
b = 'new object'
print(sys.getrefcount('new object')) # 4
print(sys.getrefcount(0)) # 1000000792
Функція sys.getrefcount сама посилається на об’єкти, тому на тільки-но створений об’єкт маємо одразу три посилання. На невеликі числа часто посилається сам Python-інтерпретатор, тому кількість посилань на них буде досить великою.
95. Навіщо використовувати метакласи⚑
💡 24/100 · python/metaclass.md
Основне застосування метакласів - це створення фреймфорків. Типовий приклад - Django ORM.
class Person(models.Model):
name = models.CharField(max_length=30)
age = models.IntegerField()
guy = Person(name='bob', age='35')
print(guy.age)
age - не IntegerField, а int, при цьому значення може бути отримано прямо з бази даних.
Це можливо, оскільки models.Model визначає __metaclass__, який здійснює деяку магію і перетворює клас Person, який визначений простим виразом, на складне зв'язування з базою даних.
Django робить щось складне, що виглядає простим, надаючи просте API та використовуючи метакласи, які відтворюють код з API і непомітно виконують усю роботу.
Межі застосування. Метакласи - рідкісний інструмент: у переважній більшості задач їх замінюють простішими механізмами - __init_subclass__ для логіки при створенні підкласів, __set_name__ і дескриптори для поведінки атрибутів, декоратори класів або делегування. Класична настанова Тіма Пітерса: "Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't." Потреба в метакласах у прикладному (а не фреймворковому) коді частіше є сигналом про проблему в архітектурі, ніж про реальну потребу.
96. Робота в конкурентному середовищі⚑
💡 23/100 · architecture/system_design.md
Щоб забезпечити цілісність даних (Data Consistency) у розподілених системах потрібно контролювати конкурентність у системі. У більшості вебсистем конкурентність реально контролюється саме на рівні бази даних, бо саме БД гарантує атомарність, ізоляцію та цілісність даних, тоді як аплікейшен лише правильно користується цими механізмами.
Основні підходи до контролю конкурентного доступу — песимістичне та оптимістичне блокування, кожне з яких має свої trade-off’и.
Вибір між песимістичним і оптимістичним підходом завжди залежить від:
- частоти конфліктів
- вимог до latency
- допустимості повторних спроб
- характеру бізнес-операцій
У вебзастосунках часто застосовують оптимістичний підхід, бо він краще масштабується і зменшує кількість блокувань. Також часто на практиці використовується гібридний підхід: оптимістичне блокування для більшості операцій і песимістичне — для критичних фінансових або транзакційних сценаріїв.
Песимістичне блокування - коли блокуються певні рядки, й інші користувачі, які намагаються щось зробити з ними, пропускаються. Такий вид блокування може не підходити у випадку великої кількості запитів до одного ресурсу, оскільки може створювати затримки. Песимістичне блокування гарантує, що жоден інший процес не змінить рядок, поки він заблокований. Це підходить для сценаріїв із високою критичністю даних (наприклад, фінансові транзакції), але може створювати затримки при великій кількості запитів. Також потрібно використовувати коли конкуренція за запис настільки велика, що оптимістичний підхід призведе до постійних retry циклів.
Оптимістичне блокування - блокування за допомогою версіонування, таймстемпів, логічних годинників. Базується на припущенні, що конфлікти трапляються рідко. Ми дозволяємо паралельні операції, але перевіряємо версію або таймстемп перед записом. Якщо версія змінилася — транзакція відхиляється, і користувач має повторити операцію. Краще використовувати у системах з інтенсивним читанням (наприклад, бронювання квитків, редагування статей, e-commerce каталоги), де ймовірність того, що два користувачі редагують один запис одночасно, є низькою.
Песимістичне блокування (Pessimistic Locking)
- Блокує рядки або таблиці наперед, ще до зміни даних (рядкове блокування, наприклад,
SELECT FOR UPDATEв PostgreSQL). Поки транзакція А тримає блокування, транзакція Б, яка намагається вибрати ті самі рядки черезFOR UPDATE, буде чекати завершення транзакції А (або впаде з таймаутом, якщо налаштованоNOWAIT). - Ідеально підходить для критичних даних (фінансові транзакції), де не можна допустити "race conditions".
- Мінімізує race condition, але погіршує масштабованість, знижує пропускну здатність системи (throughput), тримає з'єднання з БД відкритим довше.
- Може призводити до дедлоків (deadlocks) при складних транзакціях.
- Погано підходить для систем із великою кількістю паралельних запитів до одного ресурсу.
# Django example with pessimistic locking
with transaction.atomic():
order = Order.objects.select_for_update().get(id=42)
order.status = "paid"
order.save()
Оптимістичне блокування (Optimistic Locking)
- Не блокує рядки наперед, а перевіряє, чи не були дані змінені кимось іншим перед збереженням.
- Реалізується через
version,updated_at,timestampабо логічні лічильники. Транзакція читає дані без блокувань, а при оновленні додається умоваWHERE, перевіряючи, чи версія в базі збігається з тією, яку прочитали (якщо версія не співпадає — конфлікт, транзакція скасовується). - Дає кращу пропускну здатність під навантаженням - висока швидкодія при читанні (read-heavy systems).
- Потребує обробки конфліктів під час save/update, вимагає реалізації логіки повторних спроб (retry mechanism) на рівні коду застосунку.
- Найкраще працює, коли конфлікти трапляються рідко - при високій конкуренції (high contention) багато транзакцій будуть скасовуватися і перезапускатися, що витрачає ресурси CPU.
# Optimistic locking example
rows_updated = Order.objects.filter(id=42, version=3).update(
status="paid",
version=F("version") + 1,
)
if rows_updated == 0:
raise ConcurrencyError("Order was modified by another transaction")
Реалізація на Postgres
- Дефолт
READ COMMITTEDне оптимістичний - конфліктніUPDATEчекають на блокування й перечитують оновлений рядок. Оптимістична поведінка зserialization_failure(SQLSTATE40001) з'являється лише наREPEATABLE READ(помилка на самомуUPDATE/DELETE) абоSERIALIZABLE(SSI - може спрацювати на читанні, записі або коміті). У SQLAlchemy цеOperationalErrorзorig.pgcode == "40001". Перехоплення лишеOperationalErrorбез перевіркиpgcodeохоплює також deadlock (40P01) і збої з'єднання, тому фільтрація за кодом обов'язкова. - Песимістичне в Postgres -
SELECT ... FOR UPDATE(X-lock) абоFOR SHARE(S-lock); деталі синтаксису та інших варіантів (FOR NO KEY UPDATE,NOWAIT,SKIP LOCKED) - див.infrastructure/sql.md. - Retry для оптимістичного підходу робиться з exponential backoff + jitter - очікування подвоюють при кожній спробі плюс додають випадкове відхилення, щоб конкуруючі клієнти не зверталися до БД синхронно.
from sqlalchemy.exc import OperationalError
import random, time
# Without REPEATABLE READ / SERIALIZABLE this retry is a no-op:
# READ COMMITTED (default) never raises 40001.
session = Session(engine.execution_options(isolation_level="REPEATABLE READ"))
def update_with_retry(session, order_id, max_retries=5):
for attempt in range(max_retries):
try:
order = session.get(Order, order_id)
order.status = "processing"
session.commit()
return
except OperationalError as e:
session.rollback()
# Retry only on serialization_failure; deadlock / disconnect propagate up.
if getattr(e.orig, "pgcode", None) != "40001" or attempt == max_retries - 1:
raise
time.sleep((2 ** attempt) * 0.1 + random.uniform(0, 0.1))
Links
97. L - Liskov Substitution Principle⚑
💡 23/100 · computer_science/solid.md
Summary
Нащадка можна скрізь підставити замість базового класу, і код працюватиме так само - тож нащадок має поводитися як базовий, а не лише мати ті самі методи.
L - Liskov Substitution Principle - LSP - Принцип підстановки Лісков Функції, які використовують базовий тип, повинні мати можливість використовувати підтипи базового типу, не знаючи про це. Тобто поведінка в похідних класах не повинна суперечити поведінці, заданій базовим класом.
Об'єкти в програмі можуть бути заміненими їх нащадками без зміни коду програми. Клас-нащадок повинен доповнювати, а не змінювати базовий. Це вимагає суворого дотримання контрактів та інтерфейсів, визначених у базовому класі, у всіх його похідних класах.
- Принцип про правильне наслідування.
- Проектування конктракту - контракт інформує авторів клієнтського коду про бажану поведінку класу. Контракт визначає вхідні і вихідні параметри.
Канонічний контрприклад
Класична ілюстрація порушення LSP - "гумова качка":
class Duck:
def fly(self):
return "flying"
class RubberDuck(Duck): # rubber duck "is-a" duck by inheritance
def fly(self):
raise NotImplementedError("rubber ducks cannot fly")
Тип-наслідник формально "є" Duck, але звужує контракт батьківського методу. Будь-який код, що приймає Duck і викликає .fly(), ламається при підстановці RubberDuck:
def migrate_south(ducks: list[Duck]):
for d in ducks:
d.fly() # crashes for RubberDuck
migrate_south([Duck(), RubberDuck()]) # NotImplementedError
Сам факт спадкування не доводить взаємозамінності - LSP вимагає зберігати контракт батьківського методу, а не лише сигнатуру. Якщо тип не може його виконати, його не треба успадковувати - правильніше винести спільну частину у вужчий інтерфейс (див. I - Interface Segregation Principle - прибирає саме такі випадки).
Специфіка Python. LSP - передусім про поведінку (контракт: інваріанти, пред- і постумови), і її не ловить жоден статичний інструмент, лише тести та рев'ю. Приклад з RubberDuck вище саме такий: сигнатура fly() збігається, тож і компілятор, і mypy його пропустять - падіння буде тільки в рантаймі. Інструменти перевіряють лише типову частину контракту: статичний компілятор ловить невідповідність типів аргументів чи результату на збірці; у Python це робить mypy (підсвітить Incompatible return value type, якщо нащадок поверне не той тип) - але теж лише типи, не поведінку. Тому в Python контракт фіксують abc.ABC плюс анотаціями і обов'язково покривають тестами - на типізацію тут покладатися не можна.
П'ять рівнів перевизначення методу. Перевизначаючи метод базового класу, нащадок може відхилятися від нього по-різному - від повністю сумісного до несумісного:
- Та сама зовнішня поведінка (параметри, результат, побічні ефекти), інша лише реалізація й нефункціональні характеристики (швидкодія). Класи повністю взаємозамінні.
- Та сама поведінка плюс додаткова робота чи зміна полів, відсутніх у базовому класі. Підстановка безпечна, а в інших місцях з'являються додаткові можливості.
- Змінена поведінка без порушення важливих характеристик. Допустимо розширювати область значень параметрів (контраваріантність) і звужувати область результату (коваріантність), зберігаючи контракт.
- Збережені формальні характеристики (сигнатура, тип результату), але сильно змінена поведінка. Зазвичай це помилка: інструменти вважають класи сумісними, хоча це не так (приклад - гумова качка вище).
- Несумісна зміна навіть сигнатури (довільний тип результату чи параметрів). Клас однозначно не можна підставити замість базового, і це виявляють автоматичні інструменти.
Перші три рівні сумісні з LSP, останні два його порушують. Коваріантність результату й контраваріантність параметрів - саме та свобода, що лишає простір для поліморфізму, не ламаючи взаємозамінності.
Links
98. Чи можна оголошувати функцію всередині іншої функції. Де вона буде видима⚑
💡 23/100 · python/functions.md
Так, можна. Така функція буде видимою лише всередині першої функції.
99. Є кортеж з трьох елементів. Призначити змінним a, b, c його значення⚑
💡 23/100 · python/sequences.md
100. API Gateway⚑
💡 22/100 · architecture/microservices.md
Summary
API Gateway - єдина точка входу для зовнішніх клієнтів у систему мікросервісів. Виконує маршрутизацію, агрегацію відповідей, балансування навантаження, аутентифікацію та rate limiting, звільняючи окремі сервіси від цих cross-cutting обов'язків.
Призначення
У системі з десятками мікросервісів зовнішньому клієнту незручно знати адресу кожного сервіса і дублювати в коді логіку авторизації, ретраїв, балансування. Gateway виносить цю логіку на один шар перед сервісами.
Принцип роботи
- Маршрутизація. Зовнішній запит
GET /api/v1/orders/42потрапляє на gateway, який за конфігурацією маршрутів пересилає його до сервіса замовлень. - Агрегація відповідей. Один зовнішній запит (
GET /api/v1/checkout/summary) може транслюватися в кілька внутрішніх викликів (профіль, кошик, доставка) і повертати клієнту склеєну відповідь. Цей варіант роботи gateway інколи називають Backend-for-Frontend (BFF). Узагальнення цього підходу - composite UI: коли дані одного екрана розпорошені по кількох bounded context'ах (хибний агрегат), кожен контекст віддає й рендерить свій зріз; на запис один зовнішній POST навпаки розкладається (decomposition) на виклики до контекстів-власників, а кожен контекст кешує свій зріз із власним TTL (ціну - хвилини, назву й опис - місяці, наявність - без кешу). - Аутентифікація і авторизація. Gateway перевіряє JWT або сесійний токен, після чого передає внутрішнім сервісам уже довірений
user_idу заголовку. Самі сервіси не повторюють перевірку підпису. - Rate limiting. Обмеження частоти запитів per-user або per-IP реалізується на gateway, а не в кожному сервісі окремо. Деталі алгоритмів - у секції Rate Limiter.
- Спостережуваність. Структуровані логи, metrics і trace-id додаються у вхідному пункті і прокидаються далі по системі.
Балансування навантаження на gateway
Балансування буває двох рівнів:
- L4 (транспортний). Балансер працює на рівні TCP/UDP, не розбирає HTTP. Розподіл - за вихідним IP/портом, round-robin, hash. Дешевший і швидший, але не вміє роздавати запити за HTTP-заголовками чи cookie.
- L7 (прикладний). Балансер розбирає HTTP-запит і розподіляє його за заголовками, шляхом, cookie. Дорожчий за CPU, але потрібен для sticky sessions, A/B-тестування, canary deployments.
Окремо розрізняють reverse proxy і load balancer: reverse proxy корисний навіть з одним бекендом (термінація TLS, стиснення, кеш статики, приховування внутрішніх серверів), тоді як load balancer розподіляє запити між кількома інстансами.
Алгоритми розподілу. Незалежно від рівня, балансер обирає інстанс за алгоритмом:
- Round-robin - по колу, порівну. Просто; ігнорує реальне навантаження інстанса.
- Weighted round-robin - як round-robin, але потужнішим інстансам дають більшу вагу (більше запитів). Враховує різну ємність машин, але ваги статичні й не відбивають навантаження в реальному часі.
- Least connections - на інстанс із найменшою кількістю активних з'єднань. Динамічно враховує поточне навантаження; корисно, коли з'єднання мають різну тривалість.
- Hash-based - інстанс обирається хешем від ключа (IP клієнта,
user_id). Той самий клієнт стабільно потрапляє на той самий інстанс (основа sticky sessions); зміна складу пулу ламає прив'язку - звідси consistent hashing нижче.
Sticky sessions і consistent hashing
Якщо сесія користувача збережена в пам'яті конкретного інстанса сервіса (anti-pattern для stateless систем, але реальність для legacy), потрібно скеровувати наступні запити цього користувача на той самий інстанс - sticky session, зазвичай через cookie на L7.
Простий hash за IP має проблему: при додаванні чи видаленні інстанса перерозподіляється майже весь трафік. Consistent hashing мінімізує зсуви - при зміні складу пулу переходить лише пропорційна частка ключів. Деталі - стандартна тема system design.
Обмеження
- Single point of failure. Gateway сам потребує кластеризації і резервування - інакше один збій кладе всю систему.
- Додаткова латентність. Один зайвий мережевий хоп для кожного зовнішнього запита.
- Антипатерн "товстого gateway". Перенесення бізнес-логіки у gateway перетворює його на моноліт, на заміну якому й проектувалася мікросервісна архітектура.
Реалізації
- Готові: Kong, Traefik, Envoy + Istio, AWS API Gateway, Cloudflare Gateway.
- Self-hosted: nginx з Lua-модулями, HAProxy.
Зв'язок із Sidecar pattern / Service Mesh
Gateway обробляє трафік north-south (зовнішній клієнт ↔ система). Sidecar і Service Mesh обробляють трафік east-west (сервіс ↔ сервіс всередині системи). Це різні шари, які доповнюють одне одного, а не замінюють.