---
title: "Python для Java-розробників"
url: "https://romankryvolapov.com/uk/python-for-java-developers/"
description: "Python для тих, хто знає Java: синтаксис і основні методи поруч із Java, рядки, колекції, генератори, потоки, asyncio та мікросервіс на стандартній бібліотеці."
language: uk
published: 2026-11-06
updated: 2026-11-06
---
Ця стаття для тих, хто вже знає Java і хоче писати на Python. Майже всюди спочатку йде код на Java, а одразу під ним те саме на Python, тому пояснення здебільшого написані просто в коментарях до коду.

Порядок такий: спочатку синтаксис, потім основні типи та їхні методи, потім те, що влаштовано не так, як у Java: змінні, декоратори, генератори, потоки та asyncio. Наприкінці з усього цього [зібрано мікросервіс](#Мікросервіс-на-стандартній-бібліотеці). Стаття лише про мову та стандартну бібліотеку, FastAPI, Django і Flask будуть окремими статтями.

Коротко про сам Python. Його створив Гвідо ван Россум, перша версія вийшла 1991 року, тобто він старший за Java. Версія 3.0 у 2008 році зламала сумісність із другою, і зараз живе лише третя гілка. Приклади запускалися на Python 3.14 і 3.15 та на Java 26, крім тих, де потрібні сторонні бібліотеки Java. Там, де можливість з'явилася нещодавно, версію вказано.

**Плюси:** коду набагато менше, ніж на Java, швидкий старт програми та величезна екосистема, особливо для даних і машинного навчання.

**Мінуси:** обчислення помітно повільніші, ніж на JVM, потоки зазвичай не пришвидшують обчислення, а помилки типів без окремого інструмента видно лише під час запуску.

## Синтаксис: Java і Python поруч

### Який вигляд має програма

У Java будь-який код живе всередині класу, а запуск починається з методу `main`. У Python файл просто виконується згори донизу.

```java
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello");
    }
}
```

```python
# Увесь файл: ні класу, ні main, ні крапки з комою.
print("Hello")  # Hello
```

Якщо файл можна і запустити, і імпортувати з іншого файлу, код запуску ховають у перевірку. Це найближче до `main` у Java.

```python
def main():
    print("Hello")  # Hello


# __name__ — ім'я поточного модуля. Якщо файл запустили напряму,
# там рядок "__main__". Якщо його імпортували — ім'я файлу,
# і main() не викликається.
if __name__ == "__main__":
    main()
```

### Як записуються блоки та коментарі

Замість фігурних дужок у Python відступи. Це не оформлення, а синтаксис: неправильний відступ не дасть програмі запуститися.

```java
int price = 150;

// Однорядковий коментар
if (price > 100) {
    System.out.println("expensive");  // expensive
} else {
    System.out.println("cheap");
}

/* Багаторядкового коментаря в Python немає */
```

```python
price = 150

# Однорядковий коментар
# Дужки навколо умови не потрібні, наприкінці рядка двокрапка,
# а тіло блоку зсунуте на 4 пробіли.
if price > 100:
    print("expensive")  # expensive
else:
    print("cheap")
```

Довгий вираз переносять усередині дужок. Замість Javadoc пишуть рядок документації одразу під оголошенням, він називається docstring.

```java
/**
 * Рахує суму з податком.
 */
int total(int price, int tax) {
    return price
        + tax;
}

System.out.println(total(100, 20));  // 120
```

```python
def total(price, tax):
    """Рахує суму з податком."""

    # Усередині дужок перенесення рядка дозволене.
    return (price
            + tax)


print(total(100, 20))  # 120

# Docstring доступна і під час роботи програми.
print(total.__doc__)  # Рахує суму з податком.
```

Заглушка для порожнього блоку в Java — це `{}`. У Python порожній блок записати не можна, тому є слово `pass`, яке нічого не робить.

```java
void todo() {
}

class Empty {
}
```

```python
def todo():
    pass


class Empty:
    pass
```

### Як оголосити змінну

У Python немає оголошення. Змінна з'являється під час першого присвоювання, і тип є не в неї, а в значення, яке в ній лежить. Це називають динамічною типізацією. Докладніше про те, що таке змінна, розказано [нижче](#Що-таке-змінна).

```python languages="Java, Python"
int age = 30;  # Java
age = 30  # Python

var name = "Ann";  # Java
name = "Ann"  # Python

# Слова final немає. ВЕРХНІЙ_РЕГІСТР означає «не змінюйте», але це домовленість.
final int MAX_SIZE = 100;  # Java
MAX_SIZE = 100  # Python

# У Java це помилка компіляції, у Python тепер в age лежить рядок.
age = "thirty"  # Python

# Присвоїти одразу кілька значень.
int a = 1, b = 2;  # Java
a, b = 1, 2  # Python

# Поміняти місцями: у Java потрібна тимчасова змінна.
int tmp = a; a = b; b = tmp;  # Java
a, b = b, a  # Python

a + " " + b  # Java: 2 1
f"{a} {b}"  # Python: 2 1
```

Якщо тип усе ж хочеться вказати, для цього є підказка типу. Python її не перевіряє, але її перевіряють IDE та окремий інструмент mypy.

```python languages="Java, Python"
int count = 0;  # Java
count: int = 0  # Python

String title = "Report";  # Java
title: str = "Report"  # Python

# Python не заперечує: підказка — лише позначка для інструментів.
count = "many"  # Python
```

### Як прийнято називати

Правила описані в документі PEP 8, це аналог Java Code Conventions. Головна відмінність — слова в іменах змінних і функцій розділяються підкресленням.

| Що | Java | Python |
|---|---|---|
| Змінна | `userName` | `user_name` |
| Метод, функція | `findUser()` | `find_user()` |
| Клас | `OrderService` | `OrderService` |
| Константа | `MAX_SIZE` | `MAX_SIZE` |
| Пакет | `com.shop.orders` | `orders` |
| Файл | `OrderService.java` | `order_service.py` |
| Закрите поле | `private int size` | `_size` |
| Відступ | 4 пробіли | 4 пробіли |

### Які є базові типи

Примітивів у Python немає, усе є об'єктом, навіть число. Тому немає і пари `int` та `Integer`. Різних цілих типів теж немає, один `int` зберігає число будь-якого розміру.

| Java | Python | Приклад |
|---|---|---|
| `byte`, `short`, `int`, `long`, `BigInteger` | `int` | `42` |
| `float`, `double` | `float` | `3.14` |
| `boolean` | `bool` | `True`, `False` |
| `char`, `String` | `str` | `"a"`, `"Ann"` |
| `null` | `None` | `None` |
| `byte[]` | `bytes` | `b"abc"` |
| `Object` | `object` | `object()` |
| `BigDecimal` | `decimal.Decimal` | `Decimal("9.99")` |

Зверніть увагу: `True`, `False` і `None` пишуться з великої літери. Окремого типу для символу немає, символ — це рядок довжини 1.

```python languages="Java, Python"
int number = 42;  # Java
number = 42  # Python

Object text = "Ann";  # Java
text = "Ann"  # Python

# Дізнатися клас значення. У числа в Python він теж є.
text.getClass().getSimpleName()  # Java: String
type(text).__name__  # Python: str
type(number).__name__  # Python: int

# Перевірити тип.
text instanceof String  # Java: true
isinstance(text, str)  # Python: True

# Число має методи, дужки потрібні лише в літерала.
Integer.bitCount(255)  # Java: 8
(255).bit_count()  # Python: 8
```

### Як звести один тип до іншого

Зведення в дужках немає. Замість нього викликають клас потрібного типу як функцію.

```python languages="Java, Python"
(int) 3.7  # Java: 3
int(3.7)  # Python: 3

(double) 3  # Java: 3.0
float(3)  # Python: 3.0

Integer.parseInt("42")  # Java: 42
int("42")  # Python: 42

Double.parseDouble("3.5")  # Java: 3.5
float("3.5")  # Python: 3.5

String.valueOf(42)  # Java: 42
str(42)  # Python: 42

# Символ і його код.
(int) 'A'  # Java: 65
ord("A")  # Python: 65

(char) 65  # Java: A
chr(65)  # Python: A

# Пастка: у Python непорожній рядок завжди істинний, навіть "false".
Boolean.parseBoolean("false")  # Java: false
bool("false")  # Python: True
```

Рядок і число самі не склеюються. Там, де Java мовчки викличе `String.valueOf`, Python видасть помилку.

```python languages="Java, Python"
int age = 30;  # Java
age = 30  # Python

"Age: " + age  # Java: Age: 30

# У Python "Age: " + age дасть TypeError. Про f-рядки розказано нижче.
"Age: " + str(age)  # Python: Age: 30
f"Age: {age}"  # Python: Age: 30
```

### Які є арифметичні оператори

Найчастіша помилка тут — ділення. У Python `/` завжди дає дробове число, а цілочисельне ділення записується як `//`. Операторів `++` і `--` немає.

```python languages="Java, Python"
7 / 2  # Java: 3
7 // 2  # Python: 3

7.0 / 2  # Java: 3.5
7 / 2  # Python: 3.5

7 % 2  # Java: 1
7 % 2  # Python: 1

# У Python для степеня є оператор, а результат лишається цілим.
Math.pow(2, 10)  # Java: 1024.0
2 ** 10  # Python: 1024

int counter = 0;  # Java
counter = 0  # Python

# counter++ у Python не спрацює.
counter++;  # Java
counter += 1  # Python
```

### Як порівнювати значення

Тут у Python усе навпаки. `==` порівнює вміст, як `equals` у Java. А `is` перевіряє, що це той самий об'єкт, як `==` для посилань у Java.

```python languages="Java, Python"
var first = new ArrayList<>(List.of(1, 2));  # Java
first = [1, 2]  # Python

var second = new ArrayList<>(List.of(1, 2));  # Java
second = [1, 2]  # Python

# Чи рівний вміст.
first.equals(second)  # Java: true
first == second  # Python: True

# Чи це той самий об'єкт.
first == second  # Java: false
first is second  # Python: False

# Порожнє значення. None перевіряють лише через is.
String name = null;  # Java
name = None  # Python

name == null  # Java: true
name is None  # Python: True

name != null  # Java: false
name is not None  # Python: False

# Порівняння можна писати ланцюжком, як у математиці.
int age = 30;  # Java
age = 30  # Python

age >= 18 && age < 65  # Java: true
18 <= age < 65  # Python: True
```

### Як записати логічні умови

Замість `&&`, `||` і `!` використовуються слова. Крім того, умовою може бути будь-яке значення, а не лише `boolean`: порожні колекції, порожній рядок, нуль і `None` вважаються хибою.

```python languages="Java, Python"
boolean active = true;  # Java
active = True  # Python

boolean admin = false;  # Java
admin = False  # Python

active && !admin  # Java: true
active and not admin  # Python: True

active || admin  # Java: true
active or admin  # Python: True
```

```python
active = True
admin = False
print(active and not admin)  # True
print(active or admin)  # True

# Звичайний спосіб перевірити, що колекція порожня.
items = []
if not items:
    print("no items")  # no items

# Одна умова замінює перевірку і на None, і на порожнечу.
name = None
if name:
    print(name)
```

`and` і `or` повертають не `True` чи `False`, а один зі своїх операндів. На цьому тримається короткий запис значення за замовчуванням.

```python languages="Java, Python"
String name = "";  # Java
name = ""  # Python

# or поверне перше істинне значення, тут це "guest".
name == null || name.isEmpty() ? "guest" : name  # Java: guest
name or "guest"  # Python: guest

# Обережно: 0 теж хиба, і справжнє значення буде втрачено.
0 or 100  # Python: 100
```

### Як записати умову й тернарний оператор

`else if` скорочується до `elif`. Тернарний оператор читається як звичайна фраза: спочатку значення, потім умова.

```java
int score = 75;
String grade;

if (score >= 90) {
    grade = "A";
} else if (score >= 60) {
    grade = "B";
} else {
    grade = "C";
}
System.out.println(grade);  // B

String result = score >= 60 ? "pass" : "fail";
System.out.println(result);  // pass
```

```python
score = 75

if score >= 90:
    grade = "A"
elif score >= 60:
    grade = "B"
else:
    grade = "C"
print(grade)  # B

# значення_якщо_так if умова else значення_якщо_ні
result = "pass" if score >= 60 else "fail"
print(result)  # pass
```

### Чим замінити `switch`

Замість нього є `match`, він з'явився в Python 3.10. Як і новий `switch` у Java, він порівнює не лише з константами, а й зі зразками. Провалювання з гілки в гілку немає, тому `break` не потрібен.

```java
String httpName(int code) {
    return switch (code) {
        case 200 -> "OK";
        case 301, 302 -> "Redirect";
        case 404 -> "Not Found";
        default -> "Unknown";
    };
}

System.out.println(httpName(302));  // Redirect
System.out.println(httpName(500));  // Unknown
```

```python
def http_name(code):
    match code:
        case 200:
            return "OK"

        # Вертикальна риска означає «або».
        case 301 | 302:
            return "Redirect"
        case 404:
            return "Not Found"

        # Підкреслення підходить під усе, це default.
        case _:
            return "Unknown"


print(http_name(302))  # Redirect
print(http_name(500))  # Unknown
```

Зіставлення за типом працює так само, як pattern matching у Java 21, тільки зразок уміє заразом розбирати значення на частини. Приклади з `dataclass` і словниками будуть далі.

```java
String describe(Object value) {
    return switch (value) {
        case Integer n when n > 100 -> "big number";
        case Integer n -> "number " + n;
        case String s -> "text " + s;
        case List<?> l -> "list of " + l.size();
        default -> "other";
    };
}

System.out.println(describe(500));  // big number
System.out.println(describe(List.of(1, 2)));  // list of 2
```

```python
def describe(value):
    match value:

        # int(n) перевіряє тип і кладе значення в n, if — це when.
        case int(n) if n > 100:
            return "big number"
        case int(n):
            return f"number {n}"
        case str(s):
            return f"text {s}"
        case list(items):
            return f"list of {len(items)}"
        case _:
            return "other"


print(describe(500))  # big number
print(describe([1, 2]))  # list of 2

# А так зразок розбирає значення: список рівно з двох елементів.
match ["move", 10]:
    case ["move", distance]:
        print(distance)  # 10
```

### Як написати цикл `for`

У Python є лише один вид `for`, це аналог `for (x : items)` з Java. Там, де в Java потрібен лічильник, використовується `range`: функція, яка видає числа по порядку.

```java
var names = List.of("Ann", "Bob");

for (String name : names) {
    System.out.print(name + " ");  // Ann Bob
}

for (int i = 0; i < 3; i++) {
    System.out.print(i + " ");  // 0 1 2
}

for (int i = 10; i > 0; i -= 3) {
    System.out.print(i + " ");  // 10 7 4 1
}
```

```python
names = ["Ann", "Bob"]

for name in names:
    print(name)  # Ann Bob

# range(3) видає 0, 1, 2. Права межа не входить.
for i in range(3):
    print(i)  # 0 1 2

# range(від, до, крок)
for i in range(10, 0, -3):
    print(i)  # 10 7 4 1
```

Індекс потрібен рідше, ніж здається. Для частих випадків є готові функції, а `for i in range(len(names))` вважається поганим стилем.

```java
var names = List.of("Ann", "Bob");
var ages = List.of(30, 25);

// Значення та індекс
for (int i = 0; i < names.size(); i++) {
    System.out.print(i + ":" + names.get(i) + " ");  // 0:Ann 1:Bob
}

// Два списки паралельно
for (int i = 0; i < names.size(); i++) {
    System.out.print(names.get(i) + "=" + ages.get(i) + " ");  // Ann=30 Bob=25
}

// У зворотному порядку
for (int i = names.size() - 1; i >= 0; i--) {
    System.out.print(names.get(i) + " ");  // Bob Ann
}
```

```python
names = ["Ann", "Bob"]
ages = [30, 25]

# enumerate видає пари (індекс, значення).
for i, name in enumerate(names):
    print(f"{i}:{name}")  # 0:Ann 1:Bob

# zip іде двома списками одразу й видає пари.
for name, age in zip(names, ages):
    print(f"{name}={age}")  # Ann=30 Bob=25

for name in reversed(names):
    print(name)  # Bob Ann
```

### Як написати `while` і вийти з циклу

`while`, `break` і `continue` працюють так само, як у Java. Циклу `do-while` немає, і міток для виходу із вкладених циклів теж немає.

```java
int n = 0;
while (n < 10) {
    n++;
    if (n % 2 == 0) {
        continue;
    }
    if (n > 5) {
        break;
    }
    System.out.print(n + " ");  // 1 3 5
}

int tries = 0;
do {
    tries++;
} while (tries < 3);
System.out.println(tries);  // 3
```

```python
n = 0
while n < 10:
    n += 1
    if n % 2 == 0:
        continue
    if n > 5:
        break
    print(n)  # 1 3 5

# do-while записують як вічний цикл із перевіркою наприкінці.
tries = 0
while True:
    tries += 1
    if tries >= 3:
        break
print(tries)  # 3
```

У циклу може бути гілка `else`, якої немає в Java. Вона виконується, якщо цикл дійшов до кінця й не вийшов через `break`. Зазвичай це замінює прапорець `found`.

```java
var ids = List.of(1, 2, 3);
boolean found = false;

for (int id : ids) {
    if (id == 7) {
        found = true;
        break;
    }
}
if (!found) {
    System.out.println("not found");  // not found
}
```

```python
ids = [1, 2, 3]

for id_ in ids:
    if id_ == 7:
        break
else:

    # Сюди потрапляємо, лише якщо break не спрацював.
    print("not found")  # not found
```

Оператор `:=` присвоює значення просто всередині виразу. У Java присвоювання і так є виразом, а в Python для цього потрібен окремий оператор.

```java
var names = List.of("Ann", "Bob");
int count;

if ((count = names.size()) > 1) {
    System.out.println(count);  // 2
}
```

```python
names = ["Ann", "Bob"]

# Порахували й одразу перевірили. Звичайне = тут дало б SyntaxError.
if (count := len(names)) > 1:
    print(count)  # 2
```

### Як оголосити функцію

У Java метод завжди лежить у класі. У Python функція може лежати просто у файлі, тому класи зі статичними методами тут не потрібні. Тип результату й параметрів можна вказати підказками, але це необов'язково.

```java
int add(int a, int b) {
    return a + b;
}

void greet(String name) {
    System.out.println("Hi, " + name);  // Hi, Ann
}

System.out.println(add(2, 3));  // 5
greet("Ann");
```

```python
def add(a, b):
    return a + b


# Те саме з підказками типів. None тут означає void.
def greet(name: str) -> None:
    print(f"Hi, {name}")  # Hi, Ann


print(add(2, 3))  # 5
greet("Ann")

# Функція без return насправді повертає None.
print(greet("Ann"))  # None
```

### Чим замінити перевантаження методів

Перевантаження в Python немає: другий `def` з тим самим іменем просто замінить перший. Замість нього використовують значення параметрів за замовчуванням. Під час виклику аргументи можна називати за іменем, що замінює і патерн Builder.

```java
String connect(String host, int port, boolean secure) {
    return host + ":" + port + " " + secure;
}

String connect(String host, int port) {
    return connect(host, port, false);
}

String connect(String host) {
    return connect(host, 80);
}

System.out.println(connect("a"));  // a:80 false
System.out.println(connect("a", 443, true));  // a:443 true
```

```python
def connect(host, port=80, secure=False):
    return f"{host}:{port} {secure}"


print(connect("a"))  # a:80 False

# За порядком, як у Java.
print(connect("a", 443, True))  # a:443 True

# За іменем: порт пропущено, і зрозуміло, що означає True.
print(connect("a", secure=True))  # a:80 True
```

### Як передати довільну кількість аргументів

`int... numbers` з Java записується як `*numbers`. У Python є й те, чого немає в Java: `**options` збирає іменовані аргументи у словник.

```java
int sum(int... numbers) {
    return Arrays.stream(numbers).sum();
}

System.out.println(sum(1, 2, 3));  // 6

int[] values = {1, 2, 3};
System.out.println(sum(values));  // 6
```

```python
# У numbers приходить кортеж (1, 2, 3). Кортеж — це незмінний список.
def total(*numbers):
    return sum(numbers)


print(total(1, 2, 3))  # 6

# Зірочка під час виклику, навпаки, розкладає список на аргументи.
values = [1, 2, 3]
print(total(*values))  # 6


# В options приходить словник {"port": 80, "secure": True}.
def connect(host, **options):
    print(host, options)  # a {'port': 80, 'secure': True}


connect("a", port=80, secure=True)
```

Зірочкою у списку параметрів можна заборонити передачу за порядком. Усе, що стоїть після неї, можна передати лише за іменем. Так роблять для прапорців, щоб не з'являлися виклики на кшталт `send(msg, True, False)`.

```python
def send(message, *, retry=False):
    return f"{message} {retry}"


print(send("hi", retry=True))  # hi True

# TypeError: так передавати не можна.
# send("hi", True)
```

### Як повернути кілька значень

У Java для цього потрібен окремий клас або `record`. У Python функція просто повертає кілька значень через кому, і вони пакуються в кортеж.

```java
record MinMax(int min, int max) {
}

MinMax minMax(List<Integer> items) {
    return new MinMax(Collections.min(items), Collections.max(items));
}

var result = minMax(List.of(3, 1, 2));
System.out.println(result.min() + " " + result.max());  // 1 3
```

```python
def min_max(items):
    return min(items), max(items)


# Кортеж одразу розкладається по двох змінних.
low, high = min_max([3, 1, 2])
print(low, high)  # 1 3

# Зайве значення прийнято складати у змінну _ .
low, _ = min_max([3, 1, 2])

# Зірочка забирає «усе інше».
head, *rest = [1, 2, 3]
print(head, rest)  # 1 [2, 3]
```

### Як записати лямбду й посилання на метод

Функція в Python — звичайний об'єкт: її можна покласти у змінну й передати в іншу функцію. Тому функціональні інтерфейси на кшталт `Function` і `Comparator` не потрібні, а замість `.apply()` функцію просто викликають. Лямбда може містити лише один вираз.

```python languages="Java, Python"
Function<Integer, Integer> twice = x -> x * 2;  # Java
twice = lambda x: x * 2  # Python

twice.apply(4)  # Java: 8
twice(4)  # Python: 8

# Посилання на метод — це просто ім'я функції без дужок.
Function<String, Integer> size = String::length;  # Java
size = len  # Python

size.apply("Ann")  # Java: 3
size("Ann")  # Python: 3

# Кілька параметрів.
BinaryOperator<Integer> plus = (a, b) -> a + b;  # Java
plus = lambda a, b: a + b  # Python

plus.apply(2, 3)  # Java: 5
plus(2, 3)  # Python: 5
```

Найчастіше лямбду застосовують як ключ сортування. Замість `Comparator`, який порівнює два об'єкти, у Python передають функцію, яка за одним об'єктом повертає те, за чим сортувати.

```python languages="Java, Python"
var names = new ArrayList<>(List.of("Bob", "Al", "Cindy"));  # Java
names = ["Bob", "Al", "Cindy"]  # Python

names.sort(Comparator.comparingInt(String::length));  # Java
names.sort(key=len)  # Python

names  # Java: [Al, Bob, Cindy]
names  # Python: ['Al', 'Bob', 'Cindy']

names.sort(Comparator.comparing(s -> s.charAt(s.length() - 1)));  # Java
names.sort(key=lambda s: s[-1])  # Python

names  # Java: [Bob, Al, Cindy]
names  # Python: ['Bob', 'Al', 'Cindy']
```

### Як оголосити клас

Конструктор називається `__init__`. Поля заздалегідь не оголошуються: вони з'являються, коли їм присвоюють значення в конструкторі. Слова `new` немає, об'єкт створюється викликом класу. Першим параметром кожного методу йде `self`. Це те саме, що `this` у Java, тільки записується явно.

```java
class User {
    private String name;
    private int age;

    User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    String greet() {
        return "Hi, " + this.name;
    }
}

User user = new User("Ann", 30);
System.out.println(user.greet());  // Hi, Ann
```

```python
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # self під час виклику передавати не потрібно, Python підставить його сам.
    def greet(self):
        return f"Hi, {self.name}"


user = User("Ann", 30)
print(user.greet())  # Hi, Ann

# Точно те саме, тільки self передано вручну.
print(User.greet(user))  # Hi, Ann
```

Метод із подвійними підкресленнями з боків, такий як `__init__`, називають dunder-методом (скорочення від double underscore). Його не викликають напряму: його викликає сам Python, коли потрібно створити об'єкт, надрукувати його чи порівняти. Далі таких методів буде багато.

### Як зробити статичні поля й методи

Змінна, оголошена в тілі класу, належить класу, як `static` поле в Java. Для методів є два декоратори. `@staticmethod` — це звичайний статичний метод. `@classmethod` отримує першим параметром сам клас, і на ньому зазвичай роблять фабричні методи. Декоратор — це позначка над функцією, схожа на анотацію в Java. Як він працює, [розказано нижче](#Що-таке-декоратор).

```java
class Money {
    static final String CURRENCY = "USD";
    int cents;

    Money(int cents) {
        this.cents = cents;
    }

    static boolean isValid(int cents) {
        return cents >= 0;
    }

    static Money ofDollars(int dollars) {
        return new Money(dollars * 100);
    }
}

System.out.println(Money.CURRENCY);  // USD
System.out.println(Money.isValid(-1));  // false
System.out.println(Money.ofDollars(2).cents);  // 200
```

```python
class Money:
    CURRENCY = "USD"

    def __init__(self, cents):
        self.cents = cents

    @staticmethod
    def is_valid(cents):
        return cents >= 0

    # cls — це сам клас Money. У нащадка тут буде нащадок.
    @classmethod
    def of_dollars(cls, dollars):
        return cls(dollars * 100)


print(Money.CURRENCY)  # USD
print(Money.is_valid(-1))  # False
print(Money.of_dollars(2).cents)  # 200
```

### Як зробити поле закритим і що замість гетерів

Модифікаторів доступу в Python немає. Одне підкреслення на початку імені означає «це внутрішнє, ззовні не чіпайте», але заборони немає, це домовленість.

| Java | Python | Що означає |
|---|---|---|
| `public int size` | `size` | відкрите |
| `protected int size` | `_size` | внутрішнє, за домовленістю |
| `private int size` | `_size` | те саме, окремого рівня немає |

Гетери й сетери заздалегідь не пишуть, поле просто роблять відкритим. Якщо пізніше знадобиться перевірка, поле замінюють на `@property`: ззовні код лишається тим самим, `account.balance`, але викликається метод.

```java
class Account {
    private int balance;

    int getBalance() {
        return balance;
    }

    void setBalance(int balance) {
        if (balance < 0) {
            throw new IllegalArgumentException("negative");
        }
        this.balance = balance;
    }
}

var account = new Account();
account.setBalance(100);
System.out.println(account.getBalance());  // 100
```

```python
class Account:
    def __init__(self):
        self._balance = 0

    # Гетер: читається як поле, account.balance, без дужок.
    @property
    def balance(self):
        return self._balance

    # Сетер: спрацьовує на account.balance = 100.
    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("negative")
        self._balance = value


account = Account()
account.balance = 100
print(account.balance)  # 100
```

### Чим замінити `toString`, `equals` і `hashCode`

Це теж dunder-методи. Є й відмінність від Java: рядкових представлень два. `__str__` — для користувача, його використовує `print`. `__repr__` — для розробника, його показують налагоджувач і контейнери. Якщо оголошено лише `__repr__`, він працює і за `__str__`, тому зазвичай пишуть його.

```java
class Point {
    int x, y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public String toString() {
        return "Point(" + x + ", " + y + ")";
    }

    @Override
    public boolean equals(Object other) {
        return other instanceof Point p && p.x == x && p.y == y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
}

System.out.println(new Point(1, 2));  // Point(1, 2)
System.out.println(new Point(1, 2).equals(new Point(1, 2)));  // true
```

```python
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point({self.x}, {self.y})"

    # NotImplemented означає «я не вмію порівнювати з цим типом».
    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

    def __hash__(self):
        return hash((self.x, self.y))


print(Point(1, 2))  # Point(1, 2)
print(Point(1, 2) == Point(1, 2))  # True
```

### Чим замінити `record`

Попередній клас майже ніхто так не пише: для нього є `dataclass`. Цей декоратор сам створює `__init__`, `__repr__` і `__eq__` за списком полів. З параметром `frozen=True` об'єкт стає незмінним, як `record`, і отримує `__hash__`.

```java
record Point(int x, int y) {
    public Point {
        if (x < 0) {
            throw new IllegalArgumentException("x");
        }
    }

    Point withX(int newX) {
        return new Point(newX, y);
    }
}

var point = new Point(1, 2);
System.out.println(point);  // Point[x=1, y=2]
System.out.println(point.x());  // 1
System.out.println(point.withX(5));  // Point[x=5, y=2]
```

```python
from dataclasses import dataclass, field, replace


# Тут підказки типів обов'язкові: за ними декоратор знаходить поля.
@dataclass(frozen=True)
class Point:
    x: int
    y: int = 0

    # Компактний конструктор record: викликається після __init__.
    def __post_init__(self):
        if self.x < 0:
            raise ValueError("x")


point = Point(1, 2)
print(point)  # Point(x=1, y=2)
print(point.x)  # 1

# replace робить копію зі зміненим полем, це аналог withX.
print(replace(point, x=5))  # Point(x=5, y=2)

# point.x = 5 дав би FrozenInstanceError.


# Змінюване значення за замовчуванням задають через field: список на об'єкт свій.
@dataclass
class Cart:
    items: list = field(default_factory=list)


print(Cart())  # Cart(items=[])
```

### Як успадкувати клас

Батьківський клас вказується в дужках. Виклик батьківського конструктора необов'язковий і сам не відбувається, тому `super().__init__()` легко забути. Усі методи можна перевизначати, слова `@Override` немає. Батьківських класів може бути кілька, про це [нижче](#Як-працює-множинне-успадкування).

```java
class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }

    String speak() {
        return name + " makes a sound";
    }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
    }

    @Override
    String speak() {
        return super.speak() + ": woof";
    }
}

Animal pet = new Dog("Rex");
System.out.println(pet.speak());  // Rex makes a sound: woof
System.out.println(pet instanceof Animal);  // true
```

```python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} makes a sound"


class Dog(Animal):
    def __init__(self, name):
        super().__init__(name)

    def speak(self):
        return f"{super().speak()}: woof"


pet = Dog("Rex")
print(pet.speak())  # Rex makes a sound: woof
print(isinstance(pet, Animal))  # True
```

### Чим замінити абстрактний клас та інтерфейс

Абстрактний клас оголошується через `ABC`. Створити його екземпляр не можна, а нащадок мусить реалізувати абстрактні методи. Відмінність від Java в тому, що помилка з'явиться не під час компіляції, а під час створення об'єкта.

```java
abstract class Shape {
    abstract double area();

    String describe() {
        return "area " + area();
    }
}

class Square extends Shape {
    double side = 2;

    @Override
    double area() {
        return side * side;
    }
}

System.out.println(new Square().describe());  // area 4.0
```

```python
from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    def describe(self):
        return f"area {self.area()}"


class Square(Shape):
    side = 2.0

    def area(self):
        return self.side * self.side


print(Square().describe())  # area 4.0

# Shape() дав би TypeError: абстрактний клас створити не можна.
```

Інтерфейсу найближчий `Protocol`. Він відрізняється важливою річчю: класу не потрібно писати `implements`. Підходить будь-який клас, у якого є такі самі методи. Це називають качиною типізацією: якщо об'єкт уміє те, що потрібно, його тип неважливий. Перевіряє відповідність не Python, а mypy.

```java
interface Notifier {
    String send(String text);
}

class Email implements Notifier {
    public String send(String text) {
        return "email: " + text;
    }
}

String alert(Notifier notifier) {
    return notifier.send("down");
}

System.out.println(alert(new Email()));  // email: down
```

```python
from typing import Protocol


class Notifier(Protocol):
    def send(self, text: str) -> str:
        ...


# Ні implements, ні успадкування: збігу методів досить.
class Email:
    def send(self, text: str) -> str:
        return f"email: {text}"


def alert(notifier: Notifier) -> str:
    return notifier.send("down")


print(alert(Email()))  # email: down
```

### Як оголосити `enum`

Перелік — це клас-нащадок `Enum`. Як і в Java, кожен елемент існує в одному екземплярі, тому їх можна порівнювати через `is`. Якщо значення — рядки, зручний `StrEnum`: його елементи і є рядками.

```java
enum Status {
    NEW("new"),
    PAID("paid");

    final String code;

    Status(String code) {
        this.code = code;
    }
}

System.out.println(Status.PAID.name());  // PAID
System.out.println(Status.PAID.code);  // paid
System.out.println(Status.valueOf("NEW").code);  // new
System.out.println(Status.values().length);  // 2
```

```python
from enum import Enum, StrEnum


class Status(Enum):
    NEW = "new"
    PAID = "paid"


print(Status.PAID.name)  # PAID
print(Status.PAID.value)  # paid

# Знайти за іменем і за значенням.
print(Status["NEW"].value)  # new
print(Status("paid").name)  # PAID
print(len(Status))  # 2
print(Status.PAID is Status("paid"))  # True


# Елемент StrEnum дорівнює рядку, це зручно для JSON і бази.
class Color(StrEnum):
    RED = "red"


print(Color.RED == "red")  # True
```

### Чим замінити анонімні та вкладені класи

Анонімних класів немає, і зазвичай вони не потрібні, бо можна передати функцію. Вкладений клас буває, але він схожий на `static class` у Java: доступу до об'єкта зовнішнього класу він не має.

```java
Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("run");  // run
    }
};
task.run();
```

```python
def run_task(task):
    task()


# Замість анонімного класу — функція, оголошена там, де потрібна.
def task():
    print("run")  # run


run_task(task)
```

### Як написати дженерик

Підказка типу колекції пишеться у квадратних дужках. Власні узагальнені класи з Python 3.12 оголошуються так само, як у Java, тільки дужки інші. Як і в Java, під час роботи від параметра типу нічого не лишається. Докладніше в розділі про [підказки типів](#Підказки-типів).

```python languages="Java, Python"
List<String> names = new ArrayList<>();  # Java
names: list[str] = []  # Python

Map<String, Integer> ages = new HashMap<>();  # Java
ages: dict[str, int] = {}  # Python

# Значення або null.
String nickname = null;  # Java
nickname: str | None = None  # Python
```

```java
class Box<T> {
    private final T value;

    Box(T value) {
        this.value = value;
    }

    T get() {
        return value;
    }
}

<T extends Comparable<T>> T maxOf(T a, T b) {
    return a.compareTo(b) > 0 ? a : b;
}

System.out.println(new Box<>("x").get());  // x
System.out.println(maxOf(1, 2));  // 2
```

```python
class Box[T]:
    def __init__(self, value: T) -> None:
        self._value = value

    def get(self) -> T:
        return self._value


# T: int | str — це обмеження, як T extends у Java.
def max_of[T: int | str](a: T, b: T) -> T:
    return a if a > b else b


print(Box("x").get())  # x
print(max_of(1, 2))  # 2
```

### Як обробляти винятки

`catch` називається `except`, а `throw` називається `raise`. Перевірюваних винятків немає: слова `throws` не існує, і перехоплювати нічого не обов'язково.

```java
int parse(String text) {
    try {
        return Integer.parseInt(text);
    } catch (NumberFormatException | NullPointerException e) {
        System.out.println("bad: " + text);  // bad: abc
        return 0;
    } finally {
        System.out.println("done");  // done
    }
}

parse("abc");
```

```python
def parse(text):
    try:
        return int(text)

    # Кілька типів перелічуються в дужках.
    except (ValueError, TypeError) as e:
        print(f"bad: {text}")  # bad: abc
        return 0
    finally:
        print("done")  # done


parse("abc")
```

Власний виняток оголошують як клас. Причина, `cause` з Java, вказується через `from`. Ще є гілка `else`: вона виконується, лише якщо в `try` не було помилки.

```java
class OrderException extends RuntimeException {
    OrderException(String message, Throwable cause) {
        super(message, cause);
    }
}

try {
    try {
        Integer.parseInt("abc");
    } catch (NumberFormatException e) {
        throw new OrderException("bad id", e);
    }
} catch (OrderException e) {
    System.out.println(e.getMessage());  // bad id
    System.out.println(e.getCause() instanceof NumberFormatException);  // true
}
```

```python
class OrderError(Exception):
    pass


try:
    try:
        int("abc")
    except ValueError as e:
        raise OrderError("bad id") from e
except OrderError as e:
    print(e)  # bad id

    # __cause__ — те саме, що getCause().
    print(isinstance(e.__cause__, ValueError))  # True

try:
    value = int("42")
except ValueError:
    print("bad")
else:

    # Лише якщо помилки не було.
    print(value)  # 42
```

Назви стандартних винятків інші, але відповідність проста:

| Java | Python |
|---|---|
| `Throwable` | `BaseException` |
| `Exception` | `Exception` |
| `IllegalArgumentException`, `NumberFormatException` | `ValueError` |
| `ClassCastException` | `TypeError` |
| `NullPointerException` | `AttributeError` або `TypeError` |
| `IndexOutOfBoundsException` | `IndexError` |
| немає ключа в `Map` | `KeyError` |
| `IOException` | `OSError` |
| `FileNotFoundException` | `FileNotFoundError` |
| `ArithmeticException` | `ZeroDivisionError` |
| `UnsupportedOperationException` | `NotImplementedError` |
| `StackOverflowError` | `RecursionError` |
| `TimeoutException` | `TimeoutError` |

### Чим замінити try-with-resources

Замість нього є оператор `with`. Об'єкт для нього називають менеджером контексту. Замість методу `close` з `AutoCloseable` у нього два методи: `__enter__` викликається на вході, а `__exit__` на виході, навіть у разі помилки. Порядок виконання показано цифрами.

```java
class Resource implements AutoCloseable {
    Resource() {
        System.out.println("open");  // 1. open
    }

    @Override
    public void close() {
        System.out.println("close");  // 3. close
    }
}

try (var resource = new Resource()) {
    System.out.println("work");  // 2. work
}
```

```python
class Resource:
    def __enter__(self):
        print("open")  # 1. open

        # Те, що повертає __enter__, потрапляє у змінну після as.
        return self

    def __exit__(self, exc_type, exc, tb):
        print("close")  # 3. close


with Resource() as resource:
    print("work")  # 2. work
```

Найчастіше `with` трапляється під час роботи з файлами:

```java
Path path = Files.createTempFile("demo", ".txt");

try (var writer = Files.newBufferedWriter(path)) {
    writer.write("hello");
}

System.out.println(Files.readString(path));  // hello
```

```python
import tempfile
from pathlib import Path

path = Path(tempfile.mkdtemp()) / "demo.txt"

# Файл закриється сам. Кодування вказуйте завжди: інакше береться системне.
with open(path, "w", encoding="utf-8") as file:
    file.write("hello")

print(path.read_text(encoding="utf-8"))  # hello
```

### Як влаштовані імпорти та пакети

Рядка `package` у Python немає: файл сам є модулем, а його ім'я дорівнює імені файлу. Каталог із модулями називається пакетом. В один файл можна покласти скільки завгодно класів і функцій, а окремий файл на кожен клас тут не прийнятий. Імпортувати можна як сам модуль, так і окреме ім'я з нього.

```python languages="Java, Python"
# Імпорт одного імені.
import java.time.LocalDate;  # Java
from datetime import date  # Python

# Статичний імпорт.
import static java.lang.Math.sqrt;  # Java
from math import sqrt  # Python

# Імпорт усього підряд. У Python так робити не прийнято.
import java.util.*;  # Java
from math import *  # Python

# Лише в Python: увесь модуль, а потім звернення через його ім'я.
import math  # Python
math.floor(2.5)  # Python: 2

# Лише в Python: власне коротке ім'я.
import datetime as dt  # Python
dt.date(2026, 1, 2).year  # Python: 2026
```

```text
orders/                 пакет, як package orders
├── __init__.py         ознака пакета, зазвичай порожній
├── domain.py           модуль orders.domain
└── api/
    ├── __init__.py
    └── routes.py       модуль orders.api.routes
```

```python
# З іншого файлу проєкту.
from orders.domain import Order

# Із сусіднього файлу того самого пакета: крапка означає «цей пакет».
from .domain import Order

# На рівень вище.
from ..domain import Order
```

### Шпаргалка із синтаксису

| Java | Python |
|---|---|
| `int a = 1;` | `a = 1` |
| `final int MAX = 1;` | `MAX = 1` |
| `null` | `None` |
| `true`, `false` | `True`, `False` |
| `a.equals(b)` | `a == b` |
| `a == b` для посилань | `a is b` |
| `&&`, `\|\|`, `!` | `and`, `or`, `not` |
| `x > 0 ? a : b` | `a if x > 0 else b` |
| `7 / 2` | `7 // 2` |
| `Math.pow(2, 3)` | `2 ** 3` |
| `i++` | `i += 1` |
| `else if` | `elif` |
| `switch` | `match` |
| `for (int i = 0; i < n; i++)` | `for i in range(n):` |
| `for (T x : items)` | `for x in items:` |
| `do { } while (x);` | `while True:` і `break` |
| `void f(int x)` | `def f(x):` |
| `int... xs` | `*xs` |
| `x -> x * 2` | `lambda x: x * 2` |
| `String::length` | `len` |
| `new User("Ann")` | `User("Ann")` |
| `this` | `self` |
| `class B extends A` | `class B(A):` |
| `super.run()` | `super().run()` |
| `static int f()` | `@staticmethod` |
| `record Point(int x)` | `@dataclass(frozen=True)` |
| `interface` | `Protocol` |
| `abstract class` | `class A(ABC):` |
| `List<String>` | `list[str]` |
| `throw` | `raise` |
| `catch (E e)` | `except E as e:` |
| `try (var r = ...)` | `with ... as r:` |
| `toString()` | `__repr__`, `__str__` |
| `equals()`, `hashCode()` | `__eq__`, `__hash__` |
| `System.out.println(x)` | `print(x)` |

## Числа: основні операції

### Чим цілі числа відрізняються від Java

`int` у Python не переповнюється, він росте, поки вистачає пам'яті. Ділення з мінусом теж працює інакше: Java відкидає дробову частину, а Python округлює вниз.

```python languages="Java, Python"
Integer.MAX_VALUE + 1  # Java: -2147483648
2147483647 + 1  # Python: 2147483648

BigInteger.TWO.pow(70)  # Java: 1180591620717411303424
2 ** 70  # Python: 1180591620717411303424

# Ділення з мінусом: результати різні.
-7 / 2  # Java: -3
-7 // 2  # Python: -4

-7 % 3  # Java: -1
-7 % 3  # Python: 2

# Підкреслення в числах, двійковий і шістнадцятковий запис — як у Java.
1_000_000  # Java: 1000000
1_000_000  # Python: 1000000

0xFF  # Java: 255
0xFF  # Python: 255

0b101  # Java: 5
0b101  # Python: 5

# Переведення в іншу систему числення. Python додає префікс.
Integer.toBinaryString(10)  # Java: 1010
bin(10)  # Python: 0b1010

Integer.toHexString(255)  # Java: ff
hex(255)  # Python: 0xff

Integer.parseInt("ff", 16)  # Java: 255
int("ff", 16)  # Python: 255

# Бітові операції ті самі, тільки >>> немає: число не має фіксованої довжини.
5 & 3  # Java: 1
5 & 3  # Python: 1

1 << 4  # Java: 16
1 << 4  # Python: 16
```

### Де шукати `Math`

Найчастіші функції вбудовані в мову й доступні без імпорту, а решта лежить у модулі `math`.

```python languages="Java, Python"
import math  # Python

Math.abs(-5)  # Java: 5
abs(-5)  # Python: 5

Math.max(1, 2)  # Java: 2
max(1, 2)  # Python: 2

# min і max у Python приймають і кілька значень, і список.
Collections.min(List.of(3, 1))  # Java: 1
min(3, 1)  # Python: 1

# floor і ceil у Python повертають ціле, а не дробове.
Math.floor(2.7)  # Java: 2.0
math.floor(2.7)  # Python: 2

Math.ceil(2.1)  # Java: 3.0
math.ceil(2.1)  # Python: 3

Math.sqrt(16)  # Java: 4.0
math.sqrt(16)  # Python: 4.0

Math.PI  # Java: 3.141592653589793
math.pi  # Python: 3.141592653589793

# Пастка: round у Python округлює половину до парного.
Math.round(2.5)  # Java: 3
round(2.5)  # Python: 2

Math.round(3.5)  # Java: 4
round(3.5)  # Python: 4

# Округлити до двох знаків.
Math.round(3.14159 * 100) / 100.0  # Java: 3.14
round(3.14159, 2)  # Python: 3.14

# Частка й остача одразу.
divmod(7, 2)  # Python: (3, 1)

Double.isNaN(0.0 / 0)  # Java: true
math.isnan(float("nan"))  # Python: True

# У Java ділення дробового на нуль дає Infinity, у Python ZeroDivisionError.
1.0 / 0  # Java: Infinity
float("inf")  # Python: inf
```

### Як рахувати гроші

`float` у Python — це той самий `double`, з тими самими помилками округлення. Для грошей є `Decimal`, аналог `BigDecimal`. Створювати його потрібно з рядка. Звичайні оператори працюють, тому `add` і `multiply` не потрібні.

```python languages="Java, Python"
from decimal import Decimal, ROUND_HALF_UP  # Python

0.1 + 0.2  # Java: 0.30000000000000004
0.1 + 0.2  # Python: 0.30000000000000004

new BigDecimal("0.1").add(new BigDecimal("0.2"))  # Java: 0.3
Decimal("0.1") + Decimal("0.2")  # Python: 0.3

new BigDecimal("19.99").multiply(new BigDecimal("3"))  # Java: 59.97
Decimal("19.99") * 3  # Python: 59.97

# Округлити до копійок.
new BigDecimal("2.675").setScale(2, RoundingMode.HALF_UP)  # Java: 2.68
Decimal("2.675").quantize(Decimal("0.01"), ROUND_HALF_UP)  # Python: 2.68

# Порівняння: у Java equals враховує масштаб, у Python == лише значення.
new BigDecimal("1.0").equals(new BigDecimal("1.00"))  # Java: false
Decimal("1.0") == Decimal("1.00")  # Python: True
```

### Як отримати випадкове число

Зверніть увагу: у `randint` увімкнено обидві межі. Модуль `random` не підходить для паролів і токенів, для них є `secrets`, як `SecureRandom` у Java.

```python languages="Java, Python"
import random  # Python
import secrets  # Python

# Кубик: від 1 до 6.
int dice = ThreadLocalRandom.current().nextInt(1, 7);  # Java
dice = random.randint(1, 6)  # Python

# Дробове від 0 до 1.
double share = Math.random();  # Java
share = random.random()  # Python

# Випадковий елемент і перемішування.
var cards = new ArrayList<>(List.of(1, 2, 3));  # Java
cards = [1, 2, 3]  # Python

Collections.shuffle(cards);  # Java
random.shuffle(cards)  # Python

int card = cards.get(new Random().nextInt(cards.size()));  # Java
card = random.choice(cards)  # Python

# Токен для безпеки.
var token = new java.security.SecureRandom().nextLong();  # Java
token = secrets.token_hex(16)  # Python
```

## Рядки: основні методи

### Як записати рядок

Як і `String` у Java, `str` незмінний і зберігає текст в Unicode. Одинарні й подвійні лапки означають те саме. Крім звичайного рядка є кілька видів літералів: літера перед лапкою змінює сенс.

| Запис | Що це | Аналог у Java |
|---|---|---|
| `"text"`, `'text'` | звичайний рядок | `"text"` |
| `"""text"""` | багаторядковий рядок | `"""text"""` |
| `f"{name}"` | рядок із підстановкою значень | `String.format` |
| `r"\d+"` | сирий рядок: зворотна коса риска не екранує | немає |
| `b"abc"` | байти, а не текст | `byte[]` |
| `t"{name}"` | шаблон: значення окремо від тексту (3.14) | немає |

Літери можна поєднувати: `rf"\d{count}"` або `rb"\x00"`.

```python languages="Java, Python"
"it's"  # Java: it's
'it\'s'  # Python: it's

# Зручно, коли всередині є лапки іншого виду.
"say \"hi\""  # Java: say "hi"
'say "hi"'  # Python: say "hi"

# Сирий рядок заощаджує зворотні косі риски в регулярних виразах.
"\\d+"  # Java: \d+
r"\d+"  # Python: \d+

# Символ Unicode за кодом.
"é"  # Java: é
"é"  # Python: é
```

Багаторядковий рядок записується так само, як текстовий блок у Java, але відступи Python сам не прибирає. Для цього є `textwrap.dedent`.

```java
String query = """
    SELECT *
    FROM users
    """;
System.out.print(query);
```

```python
from textwrap import dedent

# Зворотна коса риска після лапок прибирає перше перенесення рядка.
query = dedent("""\
    SELECT *
    FROM users
    """)
print(query)
```

У сусідніх літералів є пастка. Python склеює їх без жодного плюса. Це зручно для довгих рядків, але якщо забути кому у списку, помилки не буде.

```python
message = (
    "first part, "
    "second part"
)
print(message)  # first part, second part

# Забута кома: у списку два елементи, а не три.
names = [
    "Ann",
    "Bob"
    "Cid",
]
print(names)  # ['Ann', 'BobCid']
```

### Які методи є в рядка

Тут зібрано ті методи, що потрібні щодня. Рядок незмінний, тому всі вони, як і в Java, повертають новий рядок.

```python languages="Java, Python"
String s = "Hello World";  # Java
s = "Hello World"  # Python

# Довжина — функція, а не метод.
s.length()  # Java: 11
len(s)  # Python: 11

s.charAt(0)  # Java: H
s[0]  # Python: H

# Від'ємний індекс рахує з кінця.
s.charAt(s.length() - 1)  # Java: d
s[-1]  # Python: d

s.toUpperCase()  # Java: HELLO WORLD
s.upper()  # Python: HELLO WORLD

s.toLowerCase()  # Java: hello world
s.lower()  # Python: hello world

s.contains("World")  # Java: true
"World" in s  # Python: True

s.startsWith("He")  # Java: true
s.startswith("He")  # Python: True

s.endsWith("ld")  # Java: true
s.endswith("ld")  # Python: True

s.indexOf("o")  # Java: 4
s.find("o")  # Python: 4

s.lastIndexOf("o")  # Java: 7
s.rfind("o")  # Python: 7

# Не знайшли: -1. Метод s.index() у цьому разі дасть ValueError.
s.indexOf("z")  # Java: -1
s.find("z")  # Python: -1

s.replace("World", "Ann")  # Java: Hello Ann
s.replace("World", "Ann")  # Python: Hello Ann

# Замінити лише перше входження.
s.replaceFirst("l", "L")  # Java: HeLlo World
s.replace("l", "L", 1)  # Python: HeLlo World

# Скільки разів трапляється. Готового методу в Java немає.
s.chars().filter(c -> c == 'l').count()  # Java: 3
s.count("l")  # Python: 3
```

```python languages="Java, Python"
# Пробіли по краях.
"  a  ".strip()  # Java: a
"  a  ".strip()  # Python: a

"  a".stripLeading()  # Java: a
"  a".lstrip()  # Python: a

"a  ".stripTrailing()  # Java: a
"a  ".rstrip()  # Python: a

# У Python strip уміє прибирати і задані символи.
"--a--".strip("-")  # Python: a

# Прибрати префікс, лише якщо він є.
"v1.2".removeprefix("v")  # Python: 1.2

"ab".repeat(2)  # Java: abab
"ab" * 2  # Python: abab

# Перевернути рядок.
new StringBuilder("abc").reverse()  # Java: cba
"abc"[::-1]  # Python: cba

# Чи порожній рядок.
"".isEmpty()  # Java: true
not ""  # Python: True

"  ".isBlank()  # Java: true
not "  ".strip()  # Python: True

# Перевірки символів. У Python вони працюють і для всього рядка.
Character.isDigit('5')  # Java: true
"5".isdigit()  # Python: True

Character.isLetter('a')  # Java: true
"a".isalpha()  # Python: True

"123".chars().allMatch(Character::isDigit)  # Java: true
"123".isdigit()  # Python: True

# Перша літера велика.
"ann".substring(0, 1).toUpperCase() + "ann".substring(1)  # Java: Ann
"ann".capitalize()  # Python: Ann

# Порівняння: compareTo у Python замінюють звичайні оператори.
"a".compareTo("b") < 0  # Java: true
"a" < "b"  # Python: True

"ANN".equalsIgnoreCase("ann")  # Java: true
"ANN".casefold() == "ann".casefold()  # Python: True
```

### Що таке зріз рядка

Замість `substring` використовується зріз, записується він як `s[початок:кінець:крок]`. Права межа не входить, як і в `substring`. Будь-яку частину можна пропустити. Індекси зручно уявляти як номери під літерами. Так само зрізи працюють зі списками.

```text
 рядок:     H   e   l   l   o
 індекс:     0   1   2   3   4
 з кінця:   -5  -4  -3  -2  -1
```

```python languages="Java, Python"
String s = "Hello";  # Java
s = "Hello"  # Python

s.substring(1, 3)  # Java: el
s[1:3]  # Python: el

# Без початку: від першого символу.
s.substring(0, 2)  # Java: He
s[:2]  # Python: He

# Без кінця: до останнього символу.
s.substring(2)  # Java: llo
s[2:]  # Python: llo

# Останні три символи.
s.substring(s.length() - 3)  # Java: llo
s[-3:]  # Python: llo

# Кожен другий символ. У Java це робиться циклом.
s[::2]  # Python: Hlo

# На відміну від substring, вихід за межі не дає помилки.
s[3:100]  # Python: lo
```

### Як розбити й склеїти рядок

Тут найбільше відмінностей. `split` у Java приймає регулярний вираз і викидає порожні рядки наприкінці, а `split` у Python нічого не викидає й шукає звичайний текст. Без аргументів він ділить за будь-якими пробілами. `join` у Python викликають у роздільника.

```python languages="Java, Python"
Arrays.toString("a,b,,".split(","))  # Java: [a, b]
"a,b,,".split(",")  # Python: ['a', 'b', '', '']

# У Java крапка — регулярний вираз, а в Python звичайний символ.
Arrays.toString("a.b".split("\\."))  # Java: [a, b]
"a.b".split(".")  # Python: ['a', 'b']

Arrays.toString(" a  b ".trim().split("\\s+"))  # Java: [a, b]
" a  b ".split()  # Python: ['a', 'b']

# Обмежити кількість частин. У Python рахуються розрізи, а не частини.
Arrays.toString("a=b=c".split("=", 2))  # Java: [a, b=c]
"a=b=c".split("=", 1)  # Python: ['a', 'b=c']

# За рядками.
"a\nb".lines().toList()  # Java: [a, b]
"a\nb".splitlines()  # Python: ['a', 'b']

# Розділити на три частини: до, роздільник, після.
"key=value".partition("=")  # Python: ('key', '=', 'value')

String.join(", ", List.of("a", "b"))  # Java: a, b
", ".join(["a", "b"])  # Python: a, b

# У Python join приймає лише рядки, числа треба перетворити.
", ".join(str(n) for n in [1, 2])  # Python: 1, 2

# Перебрати символи й отримати їхній список.
"abc".toCharArray().length  # Java: 3
list("abc")  # Python: ['a', 'b', 'c']
```

### Як підставити значення в рядок

Основний спосіб — f-рядок: літера `f` перед лапкою та будь-які вирази у фігурних дужках. Після двокрапки йде формат, дуже схожий на той, що в `String.format`.

```python languages="Java, Python"
String name = "Ann";  # Java
name = "Ann"  # Python

int n = 42;  # Java
n = 42  # Python

double x = 3.14159;  # Java
x = 3.14159  # Python

String.format("%s is %d", name, n)  # Java: Ann is 42
f"{name} is {n}"  # Python: Ann is 42

# У дужках можна писати будь-який вираз.
String.format("%d", n * 2)  # Java: 84
f"{n * 2}"  # Python: 84

# Знаки після коми.
String.format(Locale.US, "%.2f", x)  # Java: 3.14
f"{x:.2f}"  # Python: 3.14

# Ширина й нулі.
String.format("%05d", n)  # Java: 00042
f"{n:05d}"  # Python: 00042

# Вирівнювання: > праворуч, < ліворуч, ^ по центру.
String.format("[%5s]", name)  # Java: [  Ann]
f"[{name:>5}]"  # Python: [  Ann]

String.format("[%-5s]", name)  # Java: [Ann  ]
f"[{name:<5}]"  # Python: [Ann  ]

f"[{name:^5}]"  # Python: [ Ann ]

# Роздільник тисяч.
String.format(Locale.US, "%,d", 1234567)  # Java: 1,234,567
f"{1234567:,}"  # Python: 1,234,567

# Шістнадцятковий запис і відсотки.
String.format("%x", 255)  # Java: ff
f"{255:x}"  # Python: ff

f"{0.25:.0%}"  # Python: 25%

# Лише в Python: знак = друкує і вираз, і значення. Зручно для налагодження.
f"{n=}"  # Python: n=42

# Фігурна дужка як символ пишеться двічі.
f"{{{n}}}"  # Python: {42}
```

У старому коді трапляються ще два способи, про них потрібно знати, щоб читати такий код. Формат `%` ще й живе в логуванні: `log.info("user %s", name)`. Так повідомлення збирається, лише якщо запис реально потрапить у лог, як і з `{}` у SLF4J. Тому f-рядки у викликах логера не використовують.

```python
name = "Ann"

# Як printf. Найстаріший спосіб.
print("Hi, %s" % name)  # Hi, Ann

# Як MessageFormat. Потрібен, коли шаблон задано заздалегідь, у налаштуваннях.
print("Hi, {}".format(name))  # Hi, Ann
print("Hi, {who}".format(who=name))  # Hi, Ann
```

### Що таке t-рядок

f-рядок одразу склеює все у звичайний текст, тому підставляти в нього дані від користувача в SQL чи HTML небезпечно: так з'являються ін'єкції. t-рядок з'явився в Python 3.14. Він нічого не склеює, а віддає текст і значення окремо, і ви самі вирішуєте, як їх з'єднати.

```python
name = "x' OR '1'='1"

# f-рядок: дані стали частиною SQL.
print(f"name = '{name}'")  # name = 'x' OR '1'='1'

# t-рядок: це об'єкт, а не рядок.
template = t"name = {name}"
print(template.strings)  # ('name = ', '')
print(template.values)  # ("x' OR '1'='1",)


# Текст іде в запит, значення окремо, як у PreparedStatement.
def to_sql(query):
    return "?".join(query.strings), query.values


print(to_sql(template))  # ('name = ?', ("x' OR '1'='1",))
```

### Як зібрати рядок із частин

Рядок незмінний, тому склеювання в циклі щоразу створює новий рядок. Як і в Java, на великому обсязі це повільно. Окремого `StringBuilder` у Python немає: частини складають у список і один раз склеюють через `join`.

```java
var builder = new StringBuilder();
for (int i = 0; i < 3; i++) {
    builder.append(i).append(",");
}
System.out.println(builder);  // 0,1,2,
```

```python
parts = []
for i in range(3):
    parts.append(f"{i},")
print("".join(parts))  # 0,1,2,

# Якщо текст збирається як потік, є io.StringIO, це StringWriter.
import io

buffer = io.StringIO()
buffer.write("a")
buffer.write("b")
print(buffer.getvalue())  # ab
```

### Чим `__str__` відрізняється від `__repr__` під час друку

Різницю добре видно на самому рядку. Функція `str` дає текст як є, а `repr` показує, як значення записане в коді: з лапками й керівними символами. Контейнери друкують свої елементи через `repr`, тому у списку рядки видно з лапками.

```python
text = "a\tb"

print(str(text))  # a b
print(repr(text))  # 'a\tb'

# В f-рядку !r означає «виклич repr». Зручно в повідомленнях про помилки.
print(f"bad value: {text!r}")  # bad value: 'a\tb'
print(["a", "b"])  # ['a', 'b']
```

### Чим текст відрізняється від байтів

`str` зберігає текст, `bytes` зберігає байти. Python їх ніколи не змішує: додати їх не можна, а перехід завжди йде явно, через кодування. Для роботи з байтами є три типи.

| Python | Змінюваний | Що це | Аналог у Java |
|---|---|---|---|
| `bytes` | ні | незмінні байти | `byte[]`, який ніхто не змінює |
| `bytearray` | так | змінюваний буфер | `byte[]` |
| `memoryview` | як у джерела | вікно в чужі байти без копіювання | `ByteBuffer.wrap` |

```python languages="Java, Python"
import base64  # Python

byte[] data = "héllo".getBytes(StandardCharsets.UTF_8);  # Java
data = "héllo".encode("utf-8")  # Python

new String(data, StandardCharsets.UTF_8)  # Java: héllo
data.decode("utf-8")  # Python: héllo

# Довжина в байтах: é займає два.
data.length  # Java: 6
len(data)  # Python: 6

# Байт: у Java він зі знаком, а в Python від 0 до 255.
data[1]  # Java: -61
data[1]  # Python: 195

HexFormat.of().formatHex(data)  # Java: 68c3a96c6c6f
data.hex()  # Python: 68c3a96c6c6f

Base64.getEncoder().encodeToString("hi".getBytes())  # Java: aGk=
base64.b64encode(b"hi").decode()  # Python: aGk=
```

```python
data = b"abc"

# Друкується з літерою b, щоб текст і байти не плуталися.
print(data)  # b'abc'

# "abc" + b"abc" дасть TypeError.

# bytes не можна змінювати, bytearray можна.
buffer = bytearray(b"abc")
buffer[0] = 65
print(buffer)  # bytearray(b'Abc')

# memoryview дивиться на ті самі байти: запис видно в джерелі.
view = memoryview(buffer)
view[1:2] = b"B"
print(buffer)  # bytearray(b'ABc')
```

### Чим довжина рядка відрізняється від Java

`String` у Java рахує одиниці UTF-16, тому емодзі займає два `char`. У Python `len` рахує символи Unicode. Сам Python вибирає, скільки байтів відвести на символ, тому індекс завжди працює швидко. Але один видимий символ усе одно може складатися з кількох символів Unicode, і перед порівнянням такий текст нормалізують.

```python languages="Java, Python"
import unicodedata  # Python

"😀".length()  # Java: 2
len("😀")  # Python: 1

"😀".codePointCount(0, 2)  # Java: 1
len("😀")  # Python: 1

# é як один символ і як e плюс знак наголосу.
"é".equals("é")  # Java: false
"é" == "é"  # Python: False

Normalizer.normalize("é", Normalizer.Form.NFC).equals("é")  # Java: true
unicodedata.normalize("NFC", "é") == "é"  # Python: True
```

### Як працювати з регулярними виразами

Синтаксис виразів майже той самий, а методи лежать у модулі `re`. Шаблон прийнято писати сирим рядком. Якщо збігу немає, методи повертають `None`.

```python languages="Java, Python"
import re  # Python

# Чи підходить увесь рядок.
"123".matches("\\d+")  # Java: true
bool(re.fullmatch(r"\d+", "123"))  # Python: True

"a1b2".replaceAll("\\d", "#")  # Java: a#b#
re.sub(r"\d", "#", "a1b2")  # Python: a#b#

Arrays.toString("a1b2c".split("\\d"))  # Java: [a, b, c]
re.split(r"\d", "a1b2c")  # Python: ['a', 'b', 'c']

# Усі збіги.
var found = Pattern.compile("\\d").matcher("a1b2").results();  # Java
found.map(r -> r.group()).toList()  # Java: [1, 2]
re.findall(r"\d", "a1b2")  # Python: ['1', '2']
```

```java
Pattern pattern = Pattern.compile("(?<key>\\w+)=(?<value>\\d+)");
Matcher matcher = pattern.matcher("size=42");

if (matcher.find()) {
    System.out.println(matcher.group("key"));  // size
    System.out.println(matcher.group("value"));  // 42
}
```

```python
import re

# Заздалегідь скомпільований шаблон, як Pattern. Ім'я групи: ?P<ім'я>.
pattern = re.compile(r"(?P<key>\w+)=(?P<value>\d+)")

# search шукає в будь-якому місці рядка, як find().
if match := pattern.search("size=42"):
    print(match["key"])  # size
    print(match["value"])  # 42
```

## Колекції: основні методи

### Які колекції чому відповідають

Основні колекції вбудовані в мову й мають власний синтаксис, тому `new` та імпорти для них не потрібні. Інтерфейсу `List` з різними реалізаціями теж немає: є один `list`, і всередині це динамічний масив, як `ArrayList`.

| Java | Python | Запис |
|---|---|---|
| `ArrayList` | `list` | `[1, 2]` |
| `List.of` | `tuple` | `(1, 2)` |
| `LinkedHashMap` | `dict` | `{"a": 1}` |
| `HashSet` | `set` | `{1, 2}` |
| `Set.of` | `frozenset` | `frozenset({1, 2})` |
| `ArrayDeque`, `LinkedList` | `collections.deque` | `deque([1, 2])` |
| `PriorityQueue` | `heapq` | функції над списком |
| `int[]` | `list` або `array.array` | `[0] * 3` |
| `TreeMap` | немає, зазвичай `sorted(d)` | |
| `ConcurrentHashMap` | `dict` і `Lock` | |

### Як працювати зі списком

```python languages="Java, Python"
var items = new ArrayList<>(List.of(3, 1, 2));  # Java
items = [3, 1, 2]  # Python

items.size()  # Java: 3
len(items)  # Python: 3

items.get(0)  # Java: 3
items[0]  # Python: 3

# Останній елемент.
items.getLast()  # Java: 2
items[-1]  # Python: 2

items.set(0, 9);  # Java
items[0] = 9  # Python

items.add(4);  # Java
items.append(4)  # Python

# Вставити за індексом.
items.add(0, 7);  # Java
items.insert(0, 7)  # Python

items.addAll(List.of(5, 6));  # Java
items.extend([5, 6])  # Python

items  # Java: [7, 9, 1, 2, 4, 5, 6]
items  # Python: [7, 9, 1, 2, 4, 5, 6]

# Видалити за індексом і отримати значення.
items.remove(0)  # Java: 7
items.pop(0)  # Python: 7

# Видалити за значенням.
items.remove(Integer.valueOf(9));  # Java
items.remove(9)  # Python

items.contains(1)  # Java: true
1 in items  # Python: True

# Якщо значення немає, index дає ValueError, а не -1.
items.indexOf(2)  # Java: 1
items.index(2)  # Python: 1

items.isEmpty()  # Java: false
not items  # Python: False

Collections.max(items)  # Java: 6
max(items)  # Python: 6

items.stream().mapToInt(i -> i).sum()  # Java: 18
sum(items)  # Python: 18

# Перевернути на місці.
Collections.reverse(items);  # Java
items.reverse()  # Python

items  # Java: [6, 5, 4, 2, 1]
items  # Python: [6, 5, 4, 2, 1]

# Частина списку. У Java це вигляд на вихідний список, у Python копія.
items.subList(0, 2)  # Java: [6, 5]
items[:2]  # Python: [6, 5]

# Копія всього списку.
var copy = new ArrayList<>(items);  # Java
copy = items.copy()  # Python

# Склеювання й повторення.
[1] + [2]  # Python: [1, 2]
[0] * 3  # Python: [0, 0, 0]

items.clear();  # Java
items.clear()  # Python
```

Зріз можна використовувати й ліворуч від присвоювання, а множення приховує пастку на вкладених списках.

```python
items = [1, 2, 3, 4]

# Замінити частину списку.
items[1:3] = [20, 30]
print(items)  # [1, 20, 30, 4]

# Видалити частину списку.
del items[1:3]
print(items)  # [1, 4]

# Пастка: два посилання на той самий внутрішній список.
grid = [[0] * 2] * 2
grid[0][0] = 1
print(grid)  # [[1, 0], [1, 0]]

# Правильно: кожен внутрішній список створюється окремо.
grid = [[0] * 2 for _ in range(2)]
grid[0][0] = 1
print(grid)  # [[1, 0], [0, 0]]
```

### Що таке кортеж

`tuple` — це незмінний список, найближчий аналог `List.of`. Його використовують для фіксованого набору значень, таких як координати чи рядок із бази. А ще його можна зробити ключем словника, а список не можна.

```python languages="Java, Python"
var point = List.of(10, 20);  # Java
point = (10, 20)  # Python

point.get(0)  # Java: 10
point[0]  # Python: 10

# point.set(0, 1) у Java і point[0] = 1 у Python дадуть помилку.

# Кортеж з одного елемента: кома обов'язкова.
var single = List.of(1);  # Java
single = (1,)  # Python

# Без коми це просто число в дужках.
type((1)).__name__  # Python: int
type((1,)).__name__  # Python: tuple

# Розкладання по змінних.
var x = point.get(0);  # Java
x, y = point  # Python

x  # Java: 10
y  # Python: 20

# Кортежі порівнюються поелементно.
(1, 2) < (1, 3)  # Python: True
```

### Як працювати зі словником

`dict` — це `Map`. Він зберігає порядок додавання, як `LinkedHashMap`. Найважливіша відмінність від Java: `ages["Bob"]` за відсутності ключа дає помилку `KeyError`, а не `null`. Поведінку як у `get` у Java дає метод `get`.

```python languages="Java, Python"
var ages = new LinkedHashMap<String, Integer>();  # Java
ages = {}  # Python

ages.put("Ann", 30);  # Java
ages["Ann"] = 30  # Python

ages.put("Bob", 25);  # Java
ages["Bob"] = 25  # Python

ages.get("Ann")  # Java: 30
ages["Ann"]  # Python: 30

# Ключа немає.
ages.get("Cid")  # Java: null
ages.get("Cid")  # Python: None

ages.getOrDefault("Cid", 0)  # Java: 0
ages.get("Cid", 0)  # Python: 0

ages.containsKey("Ann")  # Java: true
"Ann" in ages  # Python: True

ages.containsValue(25)  # Java: true
25 in ages.values()  # Python: True

ages.size()  # Java: 2
len(ages)  # Python: 2

ages.keySet()  # Java: [Ann, Bob]
list(ages)  # Python: ['Ann', 'Bob']

ages.values()  # Java: [30, 25]
list(ages.values())  # Python: [30, 25]

# Записати, лише якщо ключа ще немає.
ages.putIfAbsent("Cid", 40);  # Java
ages.setdefault("Cid", 40)  # Python: 40

# Видалити й отримати значення.
ages.remove("Cid")  # Java: 40
ages.pop("Cid")  # Python: 40

# Видалити, а якщо ключа немає, повернути значення за замовчуванням.
ages.pop("Cid", None)  # Python: None

# Ключі по порядку, замість TreeMap.
new TreeMap<>(ages).firstKey()  # Java: Ann
sorted(ages)[0]  # Python: Ann
```

Перебір записується через `items`, він віддає пари ключ і значення. Він замінює `entrySet`.

```java
var ages = new LinkedHashMap<>(Map.of("Ann", 30));

for (var entry : ages.entrySet()) {
    System.out.print(entry.getKey() + "=" + entry.getValue());  // Ann=30
}

for (String name : ages.keySet()) {
    System.out.print(name);  // Ann
}

ages.forEach((name, age) -> System.out.print(name + age));
```

```python
ages = {"Ann": 30}

for name, age in ages.items():
    print(f"{name}={age}")  # Ann=30

# Перебір самого словника йде за ключами.
for name in ages:
    print(name)  # Ann

# Об'єднання: за збігу ключів перемагає правий.
print({"a": 1} | {"a": 2, "b": 3})  # {'a': 2, 'b': 3}

# Дві зірочки розгортають словник усередині іншого.
print({**{"a": 1}, "b": 2})  # {'a': 1, 'b': 2}
```

Ключем може бути лише незмінне значення: рядок, число, кортеж, заморожений `dataclass`. Формально потрібен метод `__hash__`, це той самий `hashCode`. Список дав би помилку `TypeError: unhashable type`.

### Як накопичувати значення за ключем

У Java для цього є `computeIfAbsent` і `merge`. У Python є `setdefault` і два спеціальні словники. `defaultdict` сам створює значення для нового ключа. `Counter` рахує, скільки разів трапляється кожен елемент.

```java
var byCity = new TreeMap<String, List<String>>();
byCity.computeIfAbsent("Kyiv", k -> new ArrayList<>()).add("Ann");
byCity.computeIfAbsent("Kyiv", k -> new ArrayList<>()).add("Bob");
System.out.println(byCity);  // {Kyiv=[Ann, Bob]}

var counts = new TreeMap<String, Integer>();
for (String word : List.of("a", "b", "a")) {
    counts.merge(word, 1, Integer::sum);
}
System.out.println(counts);  // {a=2, b=1}
```

```python
from collections import Counter, defaultdict

# list тут — функція, яка створює значення для нового ключа.
by_city = defaultdict(list)
by_city["Kyiv"].append("Ann")
by_city["Kyiv"].append("Bob")
print(dict(by_city))  # {'Kyiv': ['Ann', 'Bob']}

counts = Counter(["a", "b", "a"])
print(dict(counts))  # {'a': 2, 'b': 1}
print(counts["a"])  # 2

# Найчастіші елементи: топ-1.
print(counts.most_common(1))  # [('a', 2)]
```

### Як працювати з множиною

Методи схожі на Java, а замість `retainAll` і `removeAll` є звичайні оператори. Вони повертають нову множину й не змінюють вихідні.

```python languages="Java, Python"
var seen = new TreeSet<>(List.of(1, 2));  # Java
seen = {1, 2}  # Python

seen.add(3);  # Java
seen.add(3)  # Python

seen.contains(3)  # Java: true
3 in seen  # Python: True

seen.remove(3);  # Java
seen.remove(3)  # Python

# У Python remove дасть KeyError, якщо значення немає. discard промовчить.
seen.discard(100)  # Python

# Порожня множина: {} створить словник.
var empty = new HashSet<Integer>();  # Java
empty = set()  # Python

# Об'єднання, перетин, різниця.
{1, 2} | {2, 3}  # Python: {1, 2, 3}
{1, 2} & {2, 3}  # Python: {2}
{1, 2} - {2, 3}  # Python: {1}

# Чи входять усі елементи, як containsAll.
seen.containsAll(List.of(1))  # Java: true
{1} <= seen  # Python: True
```

### Як зробити чергу й пріоритетну чергу

`list.pop(0)` працює за O(n), бо зсуває весь масив, тому для черги використовують `deque`. Пріоритетна черга зроблена не класом, а функціями з модуля `heapq`. Вони працюють зі звичайним списком, а нагорі завжди лежить мінімальний елемент.

```python languages="Java, Python"
from collections import deque  # Python
import heapq  # Python

var queue = new ArrayDeque<>(List.of(1, 2));  # Java
queue = deque([1, 2])  # Python

queue.addLast(3);  # Java
queue.append(3)  # Python

queue.addFirst(0);  # Java
queue.appendleft(0)  # Python

queue.pollFirst()  # Java: 0
queue.popleft()  # Python: 0

queue.pollLast()  # Java: 3
queue.pop()  # Python: 3

# Подивитися, не видаляючи.
queue.peekFirst()  # Java: 1
queue[0]  # Python: 1

# Пріоритетна черга.
var tasks = new PriorityQueue<>(List.of(3, 1));  # Java
tasks = [1, 3]  # Python

tasks.add(2);  # Java
heapq.heappush(tasks, 2)  # Python

tasks.poll()  # Java: 1
heapq.heappop(tasks)  # Python: 1

# З пріоритетом: кортеж порівнюється за першим елементом.
jobs = [(2, "later"), (1, "urgent")]; heapq.heapify(jobs)  # Python
heapq.heappop(jobs)  # Python: (1, 'urgent')

# N найбільших одразу.
heapq.nlargest(2, [1, 5, 3])  # Python: [5, 3]
```

### Як сортувати

`list.sort()` змінює сам список, а `sorted()` повертає новий і приймає будь-яку колекцію. Правила сортування задає функція `key`. Для сортування за кількома полями вона просто повертає кортеж.

```java
record User(String name, int age) {
}

var users = new ArrayList<>(List.of(
    new User("Ann", 30),
    new User("Bob", 25),
    new User("Cid", 30)
));

// За спаданням віку, потім за іменем.
users.sort(
    Comparator.comparingInt(User::age).reversed()
        .thenComparing(User::name)
);

System.out.println(users.getFirst().name());  // Ann
```

```python
from dataclasses import dataclass


@dataclass
class User:
    name: str
    age: int


users = [
    User("Ann", 30),
    User("Bob", 25),
    User("Cid", 30),
]

# За спаданням віку, потім за іменем. Мінус перевертає порядок для чисел.
users.sort(key=lambda u: (-u.age, u.name))
print(users[0].name)  # Ann

# sorted не змінює вихідний список, а reverse=True перевертає порядок.
print(sorted([2, 3, 1], reverse=True))  # [3, 2, 1]
print(sorted("bca"))  # ['a', 'b', 'c']
```

### Що буде, якщо змінювати колекцію під час перебору

У Java це дає `ConcurrentModificationException`. У словника й множини в Python поведінка така сама, а от список мовчки пропускає елементи.

```python
items = [1, 2, 2, 3]

# Помилки немає, але друга двійка залишилася.
for item in items:
    if item == 2:
        items.remove(item)
print(items)  # [1, 2, 3]

# Правильно: зібрати новий список. Запис пояснюється в наступному розділі.
items = [1, 2, 2, 3]
items = [item for item in items if item != 2]
print(items)  # [1, 3]

ages = {"a": 1}
try:
    for key in ages:
        ages["b"] = 2
except RuntimeError as e:
    print(e)  # dictionary changed size during iteration
```

### Яка складність операцій

Значення ті самі, що й у відповідних колекцій у Java.

| Операція | `list` | `dict`, `set` | `deque` |
|---|---|---|---|
| Читання за індексом чи ключем | O(1) | O(1) | O(n) |
| `x in c` | O(n) | O(1) | O(n) |
| Додати в кінець | O(1) | O(1) | O(1) |
| Додати чи прибрати на початку | O(n) | – | O(1) |
| `sorted` | O(n log n) | O(n log n) | O(n log n) |

## Stream API: що замість нього

### Що таке спискове включення

Замість ланцюжка `filter` і `map` у Python є особливий запис, який називається списковим включенням (list comprehension). Читається він як звичайна фраза: що покласти, звідки взяти, за якої умови. Цей запис замінює більшу частину Stream API.

```python
numbers = [1, 2, 3, 4]

# [ що покласти   звідки взяти   умова ]
squares = [n * n for n in numbers if n % 2 == 0]
print(squares)  # [4, 16]

# Те саме звичайним циклом.
squares = []
for n in numbers:
    if n % 2 == 0:
        squares.append(n * n)
print(squares)  # [4, 16]
```

### Чим замінити операції зі стрімами

Основні операції з результатом видно одразу. Зверніть увагу: комбінацію `map` і `filter` записано одним виразом, без окремих викликів.

```python languages="Java, Python"
import functools  # Python

var items = List.of(3, 1, 2, 3);  # Java
items = [3, 1, 2, 3]  # Python

# filter
items.stream().filter(i -> i > 1).toList()  # Java: [3, 2, 3]
[i for i in items if i > 1]  # Python: [3, 2, 3]

# map
items.stream().map(i -> i * 2).toList()  # Java: [6, 2, 4, 6]
[i * 2 for i in items]  # Python: [6, 2, 4, 6]

# sum, count, min, max
items.stream().mapToInt(i -> i).sum()  # Java: 9
sum(items)  # Python: 9

items.stream().filter(i -> i > 1).count()  # Java: 3
sum(1 for i in items if i > 1)  # Python: 3

items.stream().max(Integer::compare).get()  # Java: 3
max(items)  # Python: 3

# Середнє значення.
items.stream().mapToInt(i -> i).average().getAsDouble()  # Java: 2.25
sum(items) / len(items)  # Python: 2.25

# anyMatch, allMatch, noneMatch
items.stream().anyMatch(i -> i > 2)  # Java: true
any(i > 2 for i in items)  # Python: True

items.stream().allMatch(i -> i > 2)  # Java: false
all(i > 2 for i in items)  # Python: False

items.stream().noneMatch(i -> i > 5)  # Java: true
not any(i > 5 for i in items)  # Python: True

# findFirst
items.stream().filter(i -> i < 3).findFirst().orElse(null)  # Java: 1
next((i for i in items if i < 3), None)  # Python: 1

# sorted, limit, skip
items.stream().sorted().toList()  # Java: [1, 2, 3, 3]
sorted(items)  # Python: [1, 2, 3, 3]

items.stream().limit(2).toList()  # Java: [3, 1]
items[:2]  # Python: [3, 1]

items.stream().skip(2).toList()  # Java: [2, 3]
items[2:]  # Python: [2, 3]

# distinct зі збереженням порядку: у словника ключі унікальні.
items.stream().distinct().toList()  # Java: [3, 1, 2]
list(dict.fromkeys(items))  # Python: [3, 1, 2]

# Без порядку досить множини.
items.stream().collect(Collectors.toCollection(TreeSet::new))  # Java: [1, 2, 3]
{i for i in items}  # Python: {1, 2, 3}

# flatMap
Stream.of(List.of(1), List.of(2)).flatMap(List::stream).toList()  # Java: [1, 2]
[x for row in [[1], [2]] for x in row]  # Python: [1, 2]

# joining
var words = items.stream().map(String::valueOf);  # Java
words.collect(Collectors.joining(","))  # Java: 3,1,2,3
",".join(str(i) for i in items)  # Python: 3,1,2,3

# reduce живе у functools.
items.stream().reduce(1, (a, b) -> a * b)  # Java: 18
functools.reduce(lambda a, b: a * b, items)  # Python: 18
```

Круглі дужки замість квадратних у виразах на кшталт `sum(1 for i in items if i > 1)` створюють генератор. Він не будує список у пам'яті, а видає значення по одному, як лінивий стрім. Докладніше про це розказано [нижче](#Ітератори-та-генератори).

### Як зібрати словник і згрупувати значення

Є включення і для словника, і для множини. А от у `groupingBy` простого аналога немає: групування зазвичай пишуть циклом.

```java
record User(String name, String city, int age) {
}

var users = List.of(
    new User("Ann", "Kyiv", 30),
    new User("Bob", "Lviv", 25),
    new User("Cid", "Kyiv", 40)
);

var agesByName = users.stream()
    .collect(Collectors.toMap(User::name, User::age));
System.out.println(agesByName.get("Ann"));  // 30

var byCity = users.stream()
    .collect(Collectors.groupingBy(User::city));
System.out.println(byCity.get("Kyiv").size());  // 2

var countByCity = users.stream()
    .collect(Collectors.groupingBy(User::city, Collectors.counting()));
System.out.println(countByCity.get("Kyiv"));  // 2

var adults = users.stream()
    .collect(Collectors.partitioningBy(u -> u.age() >= 30));
System.out.println(adults.get(true).size());  // 2
```

```python
from collections import Counter, defaultdict
from dataclasses import dataclass


@dataclass
class User:
    name: str
    city: str
    age: int


users = [
    User("Ann", "Kyiv", 30),
    User("Bob", "Lviv", 25),
    User("Cid", "Kyiv", 40),
]

# toMap
ages_by_name = {u.name: u.age for u in users}
print(ages_by_name["Ann"])  # 30

# groupingBy
by_city = defaultdict(list)
for user in users:
    by_city[user.city].append(user)
print(len(by_city["Kyiv"]))  # 2

# groupingBy + counting
count_by_city = Counter(u.city for u in users)
print(count_by_city["Kyiv"])  # 2

# partitioningBy
adults = [u for u in users if u.age >= 30]
print(len(adults))  # 2
```

### Чим замінити `Optional`

`Optional` у Python не використовують. Відсутність значення — це `None`, а тип записують як `User | None`. Ланцюжки замінюються звичайними перевірками.

```java
record User(String name) {
}

Optional<User> findUser(int id) {
    return id == 1 ? Optional.of(new User("Ann")) : Optional.empty();
}

String name = findUser(2)
    .map(User::name)
    .orElse("guest");
System.out.println(name);  // guest
```

```python
from dataclasses import dataclass


@dataclass
class User:
    name: str


def find_user(user_id: int) -> User | None:
    return User("Ann") if user_id == 1 else None


user = find_user(2)
name = user.name if user is not None else "guest"
print(name)  # guest
```

## Часто потрібні класи стандартної бібліотеки

### Як працювати з датою та часом

Замість `java.time` є модуль `datetime`. Важлива деталь: `datetime` може бути з часовим поясом або без нього. Без поясу це аналог `LocalDateTime`, із поясом — `ZonedDateTime`. Порівняти такі значення між собою не можна, а виклик `datetime.now()` без аргументів віддає значення без поясу. У сервісах завжди пишіть `datetime.now(UTC)`.

```python languages="Java, Python"
from datetime import UTC, date, datetime, timedelta  # Python
from zoneinfo import ZoneInfo  # Python

import java.time.format.DateTimeFormatter;  # Java
import java.time.*;  # Java

var day = LocalDate.of(2026, 11, 6);  # Java
day = date(2026, 11, 6)  # Python

# Додати й відняти: замість plusDays — звичайні оператори.
day.plusDays(30)  # Java: 2026-12-06
day + timedelta(days=30)  # Python: 2026-12-06

day.getDayOfWeek().getValue()  # Java: 5
day.isoweekday()  # Python: 5

# Різниця між датами.
Duration.ofHours(36).toDays()  # Java: 1
timedelta(hours=36).days  # Python: 1

# У рядок і назад.
day.toString()  # Java: 2026-11-06
day.isoformat()  # Python: 2026-11-06

LocalDate.parse("2026-01-02").getYear()  # Java: 2026
date.fromisoformat("2026-01-02").year  # Python: 2026

day.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))  # Java: 06.11.2026
day.strftime("%d.%m.%Y")  # Python: 06.11.2026

# Поточний момент із поясом.
var now = ZonedDateTime.now(ZoneOffset.UTC);  # Java
now = datetime.now(UTC)  # Python

var kyiv = ZoneId.of("Europe/Kyiv");  # Java
var at = ZonedDateTime.of(2026, 1, 2, 12, 0, 0, 0, kyiv);  # Java
at = datetime(2026, 1, 2, 12, tzinfo=ZoneInfo("Europe/Kyiv"))  # Python

at.toEpochSecond()  # Java: 1767348000
int(at.timestamp())  # Python: 1767348000
```

### Як працювати з файлами

`Path` і `Files` у Python об'єднані в одному класі `pathlib.Path`. Частини шляху з'єднуються оператором `/`.

```python languages="Java, Python"
import tempfile  # Python
from pathlib import Path  # Python

Path dir = Files.createTempDirectory("demo");  # Java
dir = Path(tempfile.mkdtemp())  # Python

Path file = dir.resolve("logs").resolve("a.txt");  # Java
file = dir / "logs" / "a.txt"  # Python

file.getFileName()  # Java: a.txt
file.name  # Python: a.txt

file.getParent().getFileName()  # Java: logs
file.parent.name  # Python: logs

Files.exists(file)  # Java: false
file.exists()  # Python: False

# Створити каталоги разом із батьківськими.
Files.createDirectories(file.getParent());  # Java
file.parent.mkdir(parents=True, exist_ok=True)  # Python

Files.writeString(file, "one\ntwo\n");  # Java
file.write_text("one\ntwo\n", encoding="utf-8")  # Python

Files.readAllLines(file)  # Java: [one, two]
file.read_text(encoding="utf-8").splitlines()  # Python: ['one', 'two']

# Обійти каталог з усіма підкаталогами.
Files.walk(dir).filter(Files::isRegularFile).count()  # Java: 1
sum(1 for f in dir.rglob("*.txt"))  # Python: 1

Files.delete(file);  # Java
file.unlink()  # Python
```

Великий файл читають порядково. Об'єкт файлу в Python можна перебирати в циклі, а в пам'ять при цьому потрапляє один рядок за раз.

```java
Path path = Files.createTempFile("big", ".txt");
Files.writeString(path, "1\n2\n");

try (var lines = Files.lines(path)) {
    System.out.println(lines.mapToInt(Integer::parseInt).sum());  // 3
}
```

```python
import tempfile
from pathlib import Path

path = Path(tempfile.mkdtemp()) / "big.txt"
path.write_text("1\n2\n", encoding="utf-8")

with open(path, encoding="utf-8") as file:
    print(sum(int(line) for line in file))  # 3
```

### Як працювати з JSON

Модуль `json` входить до стандартної бібліотеки. На відміну від Jackson, він працює лише зі словниками й списками, а у ваші класи не перетворює. Для цього в реальних проєктах беруть Pydantic. Об'єкт `dataclass` можна перетворити на словник функцією `asdict`.

```java
ObjectMapper mapper = new ObjectMapper();

String text = mapper.writeValueAsString(new User("Ann", 30));
User user = mapper.readValue(text, User.class);
Map<String, Object> raw = mapper.readValue(text, Map.class);
```

```python
import json
from dataclasses import asdict, dataclass


@dataclass
class User:
    name: str
    age: int


# Об'єкт у JSON: спочатку словник, потім рядок.
text = json.dumps(asdict(User("Ann", 30)))
print(text)  # {"name": "Ann", "age": 30}

# JSON у словник.
raw = json.loads(text)
print(raw["name"])  # Ann

# Словник в об'єкт: дві зірочки розкладають його на іменовані аргументи.
print(User(**raw))  # User(name='Ann', age=30)

# Типи на кшталт datetime і Decimal json не знає, потрібна функція default.
from decimal import Decimal

print(json.dumps({"sum": Decimal("1.5")}, default=str))  # {"sum": "1.5"}
```

### Як писати логи

Модуль `logging` — це SLF4J і Logback в одному флаконі. Логер створюють у кожному модулі, а налаштовують один раз під час запуску. Змінна `__name__` містить ім'я поточного модуля. Повідомлення пишуть через `%s`, як `{}` у SLF4J.

```java
private static final Logger log = LoggerFactory.getLogger(OrderService.class);

log.info("order {} paid", id);

try {
    pay(id);
} catch (Exception e) {
    log.error("payment failed", e);
}
```

```python
import logging

log = logging.getLogger(__name__)

# Налаштування під час запуску програми, замість logback.xml.
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
)

log.info("order %s paid", 42)

try:
    1 / 0
except ZeroDivisionError:

    # exception пише повідомлення й весь стек, як log.error("...", e).
    log.exception("payment failed")
```

### Де шукати решту

```python languages="Java, Python"
import hashlib  # Python
import os  # Python
import sys  # Python
import time  # Python
import uuid  # Python

# Змінна оточення або значення за замовчуванням.
String port = System.getenv().getOrDefault("NO_SUCH_VAR", "80");  # Java
port = os.environ.get("NO_SUCH_VAR", "80")  # Python

# Аргументи командного рядка. sys.argv[0] — ім'я файлу.
String[] args = new String[0];  # Java
args = sys.argv[1:]  # Python

# Виміряти час.
long start = System.nanoTime();  # Java
start = time.perf_counter()  # Python

Thread.sleep(1);  # Java
time.sleep(0.001)  # Python

# Унікальний ідентифікатор.
UUID.randomUUID().toString().length()  # Java: 36
len(str(uuid.uuid4()))  # Python: 36

# Перші 8 символів хешу SHA-256.
var sha = java.security.MessageDigest.getInstance("SHA-256");  # Java
HexFormat.of().formatHex(sha.digest(new byte[0])).substring(0, 8)  # Java: e3b0c442
hashlib.sha256(b"").hexdigest()[:8]  # Python: e3b0c442
```

| Java | Python |
|---|---|
| `System.exit(1)` | `sys.exit(1)` |
| `System.err.println(x)` | `print(x, file=sys.stderr)` |
| `new Scanner(System.in)` | `input()` |
| `HttpClient` | `urllib.request`, на практиці `httpx` |
| `URLEncoder.encode` | `urllib.parse.quote` |
| `Properties` | `configparser`, `tomllib` |
| `@Cacheable` | `functools.lru_cache` |
| `Thread`, `ExecutorService` | `threading`, `concurrent.futures` |
| `CompletableFuture` | `asyncio` |
| `JUnit` | `unittest`, на практиці `pytest` |

## Чим Python влаштований інакше, ніж Java

### Як виконується код

Основна реалізація мови називається CPython. Як і `javac` з JVM, вона компілює вихідний код у байткод і виконує його на віртуальній машині, тільки обидва кроки йдуть однією командою. Байткод кешується в каталозі `__pycache__`, як файли `.class`. Головна відмінність від JVM у тому, що байткод зазвичай інтерпретується. JIT-компілятор, який на JVM перетворює гарячий код на машинний, у CPython лише з'являється і поки експериментальний. Звідси й різниця у швидкості.

| | Java | Python |
|---|---|---|
| Компіляція | `javac` заздалегідь | під час запуску |
| Помилку типу видно | під час компіляції | під час виконання рядка |
| JIT | C2, Graal | експериментальний |
| Старт програми | довший | швидший |
| Збирання сміття | трасувальне | лічильник посилань і збирач циклів |
| Паралельні потоки | так | зазвичай ні, через GIL |

Оголошення класу чи функції в Python — це звичайна команда, яка виконується, коли до неї дійшла черга. У Java класи завантажує JVM, а тут файл просто йде згори донизу. Цифри показують порядок.

```python
print("start")  # 1. start


# Тут функція лише створюється. Тіло не виконується.
def greet():
    print("hi")  # 4. hi


# А тіло класу виконується одразу.
class Config:
    print("class body")  # 2. class body


print("end")  # 3. end
greet()
```

Це пояснює багато що з подальшого. Імпорт файлу теж просто виконує його згори донизу, тому побічний код на верхньому рівні модуля спрацює під час першого імпорту. Тому й потрібна перевірка `if __name__ == "__main__"`.

### Що таке змінна

Усі змінні в Python влаштовані як посилальні змінні в Java. Примітивів немає, тому змінна завжди зберігає посилання на об'єкт, навіть для числа. Присвоювання `b = a` копіює лише посилання. Зручно уявляти змінну як ярлик, приклеєний до об'єкта.

```text
a = [1, 2]        a ──► [1, 2]

b = a             a ──► [1, 2] ◄── b

b.append(3)       a ──► [1, 2, 3] ◄── b

b = [9]           a ──► [1, 2, 3]
                  b ──► [9]
```

```java
var a = new ArrayList<>(List.of(1, 2));
var b = a;
b.add(3);
System.out.println(a);  // [1, 2, 3]

b = new ArrayList<>(List.of(9));
System.out.println(a);  // [1, 2, 3]
```

```python
a = [1, 2]
b = a
b.append(3)
print(a)  # [1, 2, 3]

# Ярлик b перевішено на інший об'єкт. Об'єкт a не змінився.
b = [9]
print(a)  # [1, 2, 3]

# id() — це адреса об'єкта, як System.identityHashCode.
c = a
print(id(c) == id(a))  # True
```

### Як передаються аргументи у функцію

Так само, як об'єкти в Java: функція отримує копію посилання. Вміст об'єкта вона змінити може, а замінити змінну у викликального коду не може.

```java
void change(List<Integer> items) {
    items.add(1);
    items = new ArrayList<>();
    items.add(99);
}

var data = new ArrayList<Integer>();
change(data);
System.out.println(data);  // [1]
```

```python
def change(items):

    # 1. Змінюємо спільний об'єкт: це видно ззовні.
    items.append(1)

    # 2. Перевішуємо локальний ярлик: ззовні не видно.
    items = []
    items.append(99)


data = []
change(data)
print(data)  # [1]
```

### Які типи змінювані, а які ні

| Незмінні | Змінювані |
|---|---|
| `int`, `float`, `bool`, `None` | `list` |
| `str`, `bytes` | `dict` |
| `tuple`, `frozenset` | `set`, `bytearray` |
| `@dataclass(frozen=True)` | звичайні об'єкти |

Звідси важлива пастка з `+=`. Для змінюваного об'єкта він змінює сам об'єкт, а для незмінного створює новий і перевішує ярлик.

```python
# Список: один об'єкт на двох, зміну видно через обидва ярлики.
a = [1]
b = a
a += [2]
print(b)  # [1, 2]

# Рядок: a тепер вказує на новий рядок, а b на старий.
a = "x"
b = a
a += "y"
print(b)  # x
```

### Як зробити копію об'єкта

`list(x)` або `x.copy()` — це поверхнева копія, як `new ArrayList<>(x)`: копіюється сам список, а вкладені об'єкти лишаються спільними. Модуль `copy` уміє копіювати все дерево об'єктів. Для цього в Java довелося б писати код або серіалізувати об'єкт.

```python
import copy

original = [[1], [2]]

# Поверхнева копія: новий зовнішній список, ті самі внутрішні.
shallow = original.copy()
shallow[0].append(9)
print(original)  # [[1, 9], [2]]

# Глибока копія: усе нове.
deep = copy.deepcopy(original)
deep[0].append(7)
print(original)  # [[1, 9], [2]]
```

### Чому числа не можна порівнювати через `is`

Це та сама історія, що з `Integer` у Java. Там кешуються значення від -128 до 127, тому `==` іноді працює, а іноді ні. CPython теж кешує невеликі числа, і межа залежить від версії. Тому `is` використовують лише з `None`, `True`, `False` і елементами `enum`.

```java
Integer small1 = 127, small2 = 127;
System.out.println(small1 == small2);  // true

Integer big1 = 1000, big2 = 1000;
System.out.println(big1 == big2);  // false
System.out.println(big1.equals(big2));  // true
```

```python
small1 = int("256")
small2 = int("256")
print(small1 is small2)  # True

big1 = int("100000")
big2 = int("100000")
print(big1 is big2)  # False
print(big1 == big2)  # True
```

### Де видно змінну

У Java змінна живе всередині свого блоку у фігурних дужках. У Python блоків видимості немає: `if` і `for` їх не створюють. Межі проводять лише функції, класи й модулі. Змінна шукається по черзі в чотирьох місцях, цей порядок називають LEGB: Local (своя функція), Enclosing (зовнішня функція), Global (файл), Built-in (вбудовані імена).

```java
for (int i = 0; i < 3; i++) {
    int square = i * i;
}

// Помилка компіляції: i та square тут не існують.
// System.out.println(i);
```

```python
for i in range(3):
    square = i * i

# Обидві змінні живі: цикл не створює своєї області.
print(i, square)  # 2 4

# Виняток: змінну всередині включення ззовні не видно.
squares = [n * n for n in range(3)]
```

Читання зовнішньої змінної працює саме. А от присвоювання всередині функції створює локальну змінну. Щоб записати в зовнішню, потрібно сказати про це словом `global` або `nonlocal`.

```python
count = 0


def broken():

    # Присвоювання нижче робить count локальною в усій функції,
    # тому читання падає: UnboundLocalError.
    count += 1


def works():
    global count
    count += 1


works()
print(count)  # 1


def counter():
    total = 0

    def add():

        # nonlocal — змінна зовнішньої функції.
        nonlocal total
        total += 1
        return total

    return add


add = counter()
add()
print(add())  # 2
```

### Що вважається істинним в умові

Хибою вважаються порожні й нульові значення, решта вважається істиною. Власний клас керує цим через методи `__bool__` або `__len__`.

```python
print(bool(0), bool(0.0), bool(""), bool(None))  # False False False False
print(bool([]), bool({}), bool(set()), bool(()))  # False False False False
print(bool(1), bool("0"), bool([0]))  # True True True

# bool — нащадок int, тому True дорівнює 1. Так рахують збіги.
print(True + True)  # 2
print(sum(n > 1 for n in [1, 2, 3]))  # 2
```

### Що означає «просити вибачення, а не дозволу»

У Java прийнято спочатку перевірити, а потім робити. У Python частіше роблять одразу й обробляють помилку. Такий стиль називають EAFP (easier to ask forgiveness than permission). Він ще й прибирає гонку між перевіркою та дією. Винятки тут не вважаються дорогою рідкістю, ними користуються звично.

```java
var config = new HashMap<>(Map.of("port", "80"));
int port = 0;

if (config.containsKey("port")) {
    port = Integer.parseInt(config.get("port"));
}
System.out.println(port);  // 80
```

```python
config = {"port": "80"}

try:
    port = int(config["port"])
except (KeyError, ValueError):
    port = 0
print(port)  # 80
```

### Як власний клас вбудовується в мову

У Java `for` працює з `Iterable`, а `+` працює лише з рядками й числами. У Python в усіх операторів і вбудованих функцій є відповідний dunder-метод. Оголосіть його у своєму класі, і оператор запрацює. Цей набір правил називають моделлю даних.

| Що пишете | Що викликається | Аналог у Java |
|---|---|---|
| `Cart()` | `__init__` | конструктор |
| `len(x)` | `__len__` | `size()` |
| `x[i]` | `__getitem__` | `get(i)` |
| `x[i] = v` | `__setitem__` | `set(i, v)` |
| `v in x` | `__contains__` | `contains(v)` |
| `for v in x` | `__iter__` | `iterator()` |
| `a + b` | `__add__` | метод `add` |
| `a == b` | `__eq__` | `equals` |
| `a < b` | `__lt__` | `compareTo` |
| `hash(x)` | `__hash__` | `hashCode` |
| `str(x)`, `repr(x)` | `__str__`, `__repr__` | `toString` |
| `x()` | `__call__` | метод функціонального інтерфейсу |
| `with x:` | `__enter__`, `__exit__` | `AutoCloseable` |

```java
class Cart implements Iterable<String> {
    private final List<String> items = new ArrayList<>();

    void add(String item) {
        items.add(item);
    }

    int size() {
        return items.size();
    }

    String get(int index) {
        return items.get(index);
    }

    public Iterator<String> iterator() {
        return items.iterator();
    }
}

var cart = new Cart();
cart.add("tea");
System.out.println(cart.size());  // 1
System.out.println(cart.get(0));  // tea
```

```python
class Cart:
    def __init__(self):
        self._items = []

    def add(self, item):
        self._items.append(item)

    def __len__(self):
        return len(self._items)

    def __getitem__(self, index):
        return self._items[index]

    def __iter__(self):
        return iter(self._items)

    def __contains__(self, item):
        return item in self._items


cart = Cart()
cart.add("tea")

# Об'єкт поводиться як вбудована колекція.
print(len(cart))  # 1
print(cart[0])  # tea
print("tea" in cart)  # True
print(list(cart))  # ['tea']

# Порожній кошик хибний, бо len дорівнює 0.
print(bool(Cart()))  # False
```

Так само перевантажують оператори. Порівняння зручно отримати з одного методу: `__lt__` замінює `Comparable`, а декоратор `total_ordering` добудує `<=`, `>` і `>=`.

```java
record Money(int cents) implements Comparable<Money> {
    Money add(Money other) {
        return new Money(cents + other.cents);
    }

    public int compareTo(Money other) {
        return Integer.compare(cents, other.cents);
    }
}

var a = new Money(100);
var b = new Money(50);
System.out.println(a.add(b));  // Money[cents=150]
System.out.println(a.compareTo(b) > 0);  // true
```

```python
from dataclasses import dataclass
from functools import total_ordering


@total_ordering
@dataclass(frozen=True)
class Money:
    cents: int

    def __add__(self, other):
        return Money(self.cents + other.cents)

    def __lt__(self, other):
        return self.cents < other.cents


a = Money(100)
b = Money(50)
print(a + b)  # Money(cents=150)
print(a > b)  # True

# min, max і sorted тепер працюють самі.
print(min(a, b))  # Money(cents=50)
```

## Функції: чого немає в Java

### Чому не можна писати список як значення за замовчуванням

Значення за замовчуванням обчислюється один раз, під час оголошення функції, і зберігається в ній. Якщо це список, усі виклики ділять той самий об'єкт. Це найвідоміша пастка Python.

```python
# 1. Тут один раз створюється список.
def add_bad(item, items=[]):
    items.append(item)
    return items


# 2. Перший виклик кладе в нього 1.
print(add_bad(1))  # [1]

# 3. Другий виклик отримує той самий список.
print(add_bad(2))  # [1, 2]


# Правильно: None за замовчуванням, а новий список усередині.
def add_good(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items


print(add_good(1))  # [1]
print(add_good(2))  # [2]
```

### Що таке замикання

Замикання — це функція, яка запам'ятала змінні з місця, де її створено. Java-лямбди теж так уміють, але захоплюють лише `final` значення. Python захоплює саму змінну, тому її можна змінювати, а видно буде її останнє значення.

```java
Function<Integer, Integer> makeAdder(int base) {
    return x -> x + base;
}

var addTen = makeAdder(10);
System.out.println(addTen.apply(5));  // 15

var tasks = new ArrayList<Supplier<Integer>>();
for (int i = 0; i < 3; i++) {

    // Без цієї копії не скомпілюється: i змінюється.
    int copy = i;
    tasks.add(() -> copy);
}
System.out.println(tasks.get(0).get());  // 0
```

```python
def make_adder(base):
    return lambda x: x + base


add_ten = make_adder(10)
print(add_ten(5))  # 15

# Пастка: усі лямбди бачать одну змінну i, а цикл уже закінчився.
tasks = [lambda: i for i in range(3)]
print(tasks[0]())  # 2

# Рішення: значення за замовчуванням обчислюється одразу й запам'ятовується.
tasks = [lambda i=i: i for i in range(3)]
print(tasks[0]())  # 0
```

### Що таке декоратор

Запис `@something` над функцією схожий на анотацію в Java, але працює інакше. Анотація — це позначка, яку потім читає хтось інший, наприклад Spring. Декоратор — це звичайна функція. Вона отримує вашу функцію й повертає іншу, яка замінить вихідну. Це схоже на проксі, які Spring створює для `@Transactional`. Ось як це влаштовано по кроках.

```python
def logged(func):

    # 1. Під час оголошення add викликається logged(add).
    # 2. Він створює wrapper і повертає його замість add.
    def wrapper(*args, **kwargs):
        print("before")  # 3. before
        result = func(*args, **kwargs)
        print("after")  # 5. after
        return result

    return wrapper


@logged
def add(a, b):
    print("add")  # 4. add
    return a + b


# Тепер ім'я add вказує на wrapper.
print(add(2, 3))  # 6. 5
```

Запис `@logged` — це лише коротка форма присвоювання. Ті самі два варіанти поруч:

```python
@logged
def add(a, b):
    return a + b


# Точно те саме:
def add(a, b):
    return a + b


add = logged(add)
```

У справжнього декоратора є ще одна деталь. `wrapper` підміняє функцію, і ім'я з документацією губляться. Їх повертає `functools.wraps`. Параметри в декоратора теж можливі, для цього потрібен ще один рівень вкладеності: `@retry(times=3)` спочатку викликає `retry(times=3)`, і вже результат працює декоратором.

```python
from functools import wraps


# 1. retry(times=3) створює й повертає decorator.
def retry(times):

    # 2. decorator(flaky) створює й повертає wrapper.
    def decorator(func):

        # 3. Виклик flaky() тепер виконує wrapper.
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(times):
                try:
                    return func(*args, **kwargs)
                except ConnectionError:
                    if attempt == times - 1:
                        raise

        return wrapper

    return decorator


calls = []


@retry(times=3)
def flaky():
    calls.append(1)
    if len(calls) < 3:
        raise ConnectionError
    return "ok"


print(flaky())  # ok
print(len(calls))  # 3

# Без wraps тут було б wrapper.
print(flaky.__name__)  # flaky
```

### Які декоратори є готові

Кеш результатів, як `@Cacheable` у Spring, є у стандартній бібліотеці. Розбір за типом першого аргументу частково замінює перевантаження.

```python
from functools import lru_cache, singledispatch


# Зберігає результати викликів за аргументами: максимум 128 останніх.
@lru_cache(maxsize=128)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)


print(fib(50))  # 12586269025
print(fib.cache_info().hits)  # 48


# Вибирає реалізацію за типом першого аргументу.
@singledispatch
def show(value):
    return "other"


@show.register
def _(value: int):
    return "number"


@show.register
def _(value: str):
    return "text"


print(show(1))  # number
print(show("a"))  # text
print(show(1.5))  # other
```

## Класи: що ще потрібно знати

### Чим поле класу відрізняється від поля об'єкта

Під час читання `user.role` Python спочатку шукає атрибут у самого об'єкта, а потім у класу. Під час запису атрибут завжди створюється в об'єкта. Через це поле класу зі списком виявляється спільним на всі об'єкти.

```python
class User:
    role = "guest"
    tags = []


a = User()
b = User()

# Читаємо поле класу через об'єкт.
print(a.role)  # guest

# Запис створив поле в a, а в класу і в b усе як раніше.
a.role = "admin"
print(b.role)  # guest

# Пастка: це не запис, а зміна спільного списку.
a.tags.append("x")
print(b.tags)  # ['x']

# Поля об'єкта лежать у словнику. Тому їх можна додавати на льоту.
a.city = "Kyiv"
print(a.__dict__)  # {'role': 'admin', 'city': 'Kyiv'}
```

Словник на кожен об'єкт займає пам'ять. Якщо об'єктів мільйони, поля оголошують заздалегідь через `__slots__`. Заразом стає неможливо додати випадкове поле з друкарською помилкою. У `dataclass` для цього є параметр `slots=True`.

```python
class Point:
    __slots__ = ("x", "y")

    def __init__(self, x, y):
        self.x = x
        self.y = y


point = Point(1, 2)
try:
    point.z = 3
except AttributeError as e:
    print(type(e).__name__)  # AttributeError
```

### Як працює множинне успадкування

У Java успадкування класів одиночне, а спільна поведінка виноситься в default-методи інтерфейсів. У Python батьківських класів може бути кілька. Порядок, у якому шукається метод, називається MRO (Method Resolution Order). Він лінійний, і його можна подивитися. Важливий момент: `super()` викликає не батьківський клас, а наступний клас у цьому порядку.

```python
class Base:
    def run(self):
        print("Base")  # 3. Base


class Left(Base):
    def run(self):
        print("Left")  # 1. Left
        super().run()


class Right(Base):
    def run(self):
        print("Right")  # 2. Right
        super().run()


class Both(Left, Right):
    pass


# Порядок: Both, Left, Right, Base, object. Після Left іде Right, а не Base.
names = [c.__name__ for c in Both.__mro__]
print(names)  # ['Both', 'Left', 'Right', 'Base', 'object']
Both().run()
```

На практиці множинне успадкування використовують для міксинів. Міксин — це маленький клас з однією можливістю, який підмішують до основного. Це найближчий аналог default-методів інтерфейсу.

```java
interface Jsonable {
    Map<String, Object> fields();

    default String toJson() {
        return fields().toString();
    }
}

record User(String name) implements Jsonable {
    public Map<String, Object> fields() {
        return Map.of("name", name);
    }
}

System.out.println(new User("Ann").toJson());  // {name=Ann}
```

```python
import json


class JsonMixin:
    def to_json(self):
        return json.dumps(self.__dict__)


class User(JsonMixin):
    def __init__(self, name):
        self.name = name


print(User("Ann").to_json())  # {"name": "Ann"}
```

### Як звернутися до поля за іменем

Рефлексія в Python проста й вбудована в мову. Для звернення до атрибута за іменем є функції `getattr`, `setattr` і `hasattr`.

```java
class User {
    public String name = "Ann";

    public String greet() {
        return "Hi, " + name;
    }
}

var user = new User();
var field = User.class.getField("name");
System.out.println(field.get(user));  // Ann

var method = User.class.getMethod("greet");
System.out.println(method.invoke(user));  // Hi, Ann
```

```python
class User:
    name = "Ann"

    def greet(self):
        return f"Hi, {self.name}"


user = User()
print(getattr(user, "name"))  # Ann
print(getattr(user, "age", 0))  # 0
print(hasattr(user, "greet"))  # True

# Метод — звичайний атрибут, його можна отримати й викликати.
print(getattr(user, "greet")())  # Hi, Ann

# Методи можна замінити просто під час роботи. Це monkey patching.
User.greet = lambda self: "Hello"
print(user.greet())  # Hello
```

Метод `__getattr__` викликається, якщо атрибут не знайдено звичайним шляхом. З його допомогою за кілька рядків пишуть проксі, для чого в Java потрібен `java.lang.reflect.Proxy` з інтерфейсом.

```python
class Logged:
    def __init__(self, target):
        self._target = target

    # 1. Атрибута append у Logged немає, тому викликано __getattr__.
    # 2. Він бере атрибут у справжнього об'єкта.
    def __getattr__(self, name):
        print(f"access {name}")  # access append
        return getattr(self._target, name)


items = Logged([])

# 3. Отриманий метод викликається з аргументом 1.
items.append(1)
```

### Що ще трапиться в чужому коді

Ці речі потрібні рідко, але в коді фреймворків вони є.

| Що | Що робить | Аналог у Java |
|---|---|---|
| `__new__` | створює об'єкт, а `__init__` його заповнює | немає |
| Дескриптор | об'єкт із методами `__get__` і `__set__` у полі класу. Так влаштовані `property` і методи | немає |
| Метаклас | клас, який створює класи. Так Django збирає моделі за їхніми полями | обробка анотацій і генерація байткоду |
| `__init_subclass__` | викликається під час оголошення нащадка, зручний для реєстрації | `ServiceLoader` |
| `__del__` | фіналізатор, покладатися на нього не можна, замість нього `with` | `finalize` |

## Підказки типів

### Чи перевіряє Python типи

Ні. Анотації зберігаються, але під час запуску ні на що не впливають. Перевіряє їх окрема програма, найчастіше mypy або pyright, яку запускають так само, як лінтер у CI. Виходить компілятор Java, тільки необов'язковий. У сервісах анотації пишуть усюди, а перевірку вмикають у строгому режимі.

```python
def double(value: int) -> int:
    return value * 2


# Python виконає це без помилок. Помилку покаже лише mypy.
print(double("ab"))  # abab

# Анотації можна прочитати. На цьому побудовані Pydantic і FastAPI.
print(double.__annotations__["value"].__name__)  # int
```

### Як записуються типи

| Java | Python |
|---|---|
| `int`, `String` | `int`, `str` |
| `List<String>` | `list[str]` |
| `Map<String, Integer>` | `dict[str, int]` |
| `Set<Long>` | `set[int]` |
| `int[]` | `list[int]` |
| пара значень | `tuple[str, int]` |
| незмінний список | `tuple[str, ...]` |
| `@Nullable String` | `str \| None` |
| `Object` | `object` |
| немає перевірки взагалі | `Any` |
| `Collection<? extends T>` | `Iterable[T]`, `Sequence[T]` |
| `Function<Integer, String>` | `Callable[[int], str]` |
| `Class<T>` | `type[T]` |
| `void` | `None` |
| `sealed interface` з варіантами | `A \| B` |
| обмежений набір рядків | `Literal["asc", "desc"]` |
| повертає this | `Self` |
| ніколи не повертає | `NoReturn` |
| `typealias`: немає в Java | `type UserId = int` |

У старому коді типи мають інший вигляд. Зустрінете таке — знайте, що це те саме.

| Старий запис | Новий запис | З якої версії |
|---|---|---|
| `List[str]`, `Dict[str, int]` | `list[str]`, `dict[str, int]` | 3.9 |
| `Optional[str]` | `str \| None` | 3.10 |
| `Union[int, str]` | `int \| str` | 3.10 |
| `T = TypeVar("T")`, `Generic[T]` | `class Box[T]:` | 3.12 |

Параметр `Sequence[T]` приймає і список, і кортеж, а змінювати його не можна. Це та сама ідея, що `List<? extends T>`: щоб приймати підтипи, параметр має бути лише для читання. Тому для параметрів функцій краще широкі типи на кшталт `Iterable` і `Sequence`, а для результату конкретний `list`.

```python
from collections.abc import Callable, Iterable


def total(items: Iterable[int]) -> int:
    return sum(items)


# Підходить будь-яка колекція, що перебирається.
print(total([1, 2]))  # 3
print(total({3, 4}))  # 7


def apply(func: Callable[[int], int], value: int) -> int:
    return func(value)


print(apply(lambda x: x + 1, 1))  # 2
```

## Ітератори та генератори

### Як влаштований цикл `for`

Це ті самі `Iterable` та `Iterator`, що в Java. Метод `__iter__` видає ітератор. Метод `__next__` видає наступне значення, а кінець повідомляє не через `hasNext`, а винятком `StopIteration`.

```java
var iterator = List.of("a", "b").iterator();

while (iterator.hasNext()) {
    System.out.print(iterator.next());  // ab
}
```

```python
# 1. for викликає iter(items).
iterator = iter(["a", "b"])

# 2. На кожному кроці він викликає next(iterator).
print(next(iterator))  # a
print(next(iterator))  # b

# 3. StopIteration завершує цикл for, ніхто її не бачить.
print(next(iterator, "end"))  # end
```

### Що таке генератор

Написати ітератор у Java — це клас із полями, що зберігають стан, і двома методами. У Python для цього є генератор: функція, у якій замість `return` стоїть `yield`. Вона видає значення й завмирає на цьому рядку, зберігши всі змінні, а за наступного запиту продовжує з того самого місця. Цифри показують порядок виконання.

```python
def numbers():
    print("start")  # 2. start
    yield 1
    print("resume")  # 4. resume
    yield 2
    print("end")  # 6. end


# 1. Виклик лише створює генератор, тіло ще не виконується.
gen = numbers()

# 2. next запускає тіло й зупиняється на першому yield.
print(next(gen))  # 3. 1

# 4. Наступний next продовжує з місця зупинки.
print(next(gen))  # 5. 2

# 6. Тіло дійшло до кінця: це StopIteration, отже, значень більше немає.
print(next(gen, "no more"))  # 7. no more
```

Порівняйте те саме завдання: видати числа від `from` до `to`.

```java
class Range implements Iterable<Integer> {
    final int from, to;

    Range(int from, int to) {
        this.from = from;
        this.to = to;
    }

    public Iterator<Integer> iterator() {
        return new Iterator<>() {
            int current = from;

            public boolean hasNext() {
                return current < to;
            }

            public Integer next() {
                return current++;
            }
        };
    }
}

for (int n : new Range(1, 3)) {
    System.out.print(n);  // 12
}
```

```python
def count_range(start, end):
    current = start
    while current < end:
        yield current
        current += 1


for n in count_range(1, 3):
    print(n)  # 1 2
```

### Навіщо потрібні генератори

Вони ліниві, як стріми в Java: значення з'являється, лише коли його запросили. Тому можна описати нескінченну послідовність, і тому великий файл проходить через ланцюжок функцій, не завантажуючись у пам'ять.

```java
var firstFive = Stream.iterate(0, n -> n + 2)
    .limit(5)
    .toList();
System.out.println(firstFive);  // [0, 2, 4, 6, 8]
```

```python
from itertools import islice


# Нескінченний цикл тут безпечний: код стоїть на yield, поки не попросять.
def evens():
    n = 0
    while True:
        yield n
        n += 2


# islice — це limit.
print(list(islice(evens(), 5)))  # [0, 2, 4, 6, 8]
```

```java
var lines = Stream.of("1", "x", "2");

int total = lines
    .filter(line -> line.matches("\\d+"))
    .mapToInt(Integer::parseInt)
    .sum();
System.out.println(total);  // 3
```

```python
lines = ["1", "x", "2"]

# Круглі дужки: це вираз-генератор. Кожен крок лінивий, як у стрімі.
digits = (line for line in lines if line.isdigit())
numbers = (int(line) for line in digits)

# Робота починається лише тут.
print(sum(numbers))  # 3
```

Різниця між квадратними й круглими дужками — головне, що тут треба запам'ятати. Перший варіант одразу будує весь список у пам'яті, а другий не будує нічого.

```python
import sys

as_list = [n for n in range(1_000_000)]
as_gen = (n for n in range(1_000_000))

print(sys.getsizeof(as_list) > 1_000_000)  # True
print(sys.getsizeof(as_gen) < 1_000)  # True
```

Генератор, як і стрім, можна пройти лише один раз. Другий прохід не дасть помилки, а просто буде порожнім. Те саме стосується результатів `map`, `filter` і `zip`.

```java
var stream = Stream.of(1, 2);
stream.toList();

// Повторно: IllegalStateException.
// stream.toList();
```

```python
gen = (n for n in [1, 2])
print(list(gen))  # [1, 2]

# Помилки немає, просто порожньо.
print(list(gen))  # []

doubled = map(lambda n: n * 2, [1, 2])
print(sum(doubled))  # 6
print(sum(doubled))  # 0
```

### Що ще вміють генератори

`yield from` передає значення іншого генератора. Метод `send` надсилає значення всередину, у місце зупинки на `yield`. На цьому механізмі раніше будувалася асинхронність, а сьогодні він трапляється рідко.

```python
def chain_all(first, second):

    # Замінює цикл із yield на кожне значення.
    yield from first
    yield from second


print(list(chain_all([1], [2])))  # [1, 2]


def averager():
    total = 0
    count = 0
    average = None
    while True:

        # Спочатку віддає значення назовні, потім отримує нове із send.
        value = yield average
        total += value
        count += 1
        average = total / count


avg = averager()

# Перший next доводить генератор до першого yield.
next(avg)
print(avg.send(10))  # 10.0
print(avg.send(20))  # 15.0
```

Готові ліниві операції лежать у модулі `itertools`.

```python languages="Java, Python"
from itertools import batched, chain, groupby, islice, takewhile  # Python

Stream.of(1, 2, 3).limit(2).toList()  # Java: [1, 2]
list(islice([1, 2, 3], 2))  # Python: [1, 2]

Stream.concat(Stream.of(1), Stream.of(2)).toList()  # Java: [1, 2]
list(chain([1], [2]))  # Python: [1, 2]

Stream.of(1, 2, 3).takeWhile(i -> i < 3).toList()  # Java: [1, 2]
list(takewhile(lambda i: i < 3, [1, 2, 3]))  # Python: [1, 2]

# Розбити на пачки по 2.
list(batched([1, 2, 3], 2))  # Python: [(1, 2), (3,)]

# Групує лише значення, що йдуть підряд, тому спочатку сортують.
[k for k, _ in groupby(sorted("abab"))]  # Python: ['a', 'b']
```

## Потоки та процеси

### Що таке GIL

GIL (Global Interpreter Lock) — це одне спільне блокування на весь процес. Виконувати код на Python може лише той потік, який його тримає, тому в один момент працює лише один потік, скільки б ядер не було. Потоки при цьому справжні, системні, як у Java. На час очікування мережі, диска чи `sleep` потік відпускає блокування, і працюють інші.

Висновок простий: потоки пришвидшують очікування, але не пришвидшують обчислення. Для обчислень потрібні процеси, у кожного з яких свій GIL.

| Завдання | Java | Python |
|---|---|---|
| Багато очікування мережі чи бази | потоки, віртуальні потоки | `asyncio` або потоки |
| Обчислення на всіх ядрах | потоки | процеси |
| Блокувальна бібліотека | потоки | потоки |

### Як запустити потік

Клас `Thread` майже такий самий. Замість реалізації `Runnable` у нього передають функцію та її аргументи.

```java
void work(String name) {
    System.out.println("work " + name);  // work a
}

var thread = new Thread(() -> work("a"), "worker");
thread.setDaemon(true);
thread.start();
thread.join();

System.out.println(thread.getName());  // worker
System.out.println(thread.isAlive());  // false
```

```python
import threading


def work(name):
    print(f"work {name}")  # work a


# args — кортеж аргументів, тому кома обов'язкова.
thread = threading.Thread(target=work, args=("a",), name="worker", daemon=True)
thread.start()
thread.join()

print(thread.name)  # worker
print(thread.is_alive())  # False
```

Ось що означає «потоки пришвидшують очікування». Чотири потоки сплять по 0,1 секунди, а загальний час теж близько 0,1 секунди. Якщо замінити `sleep` на підрахунок у циклі, вони виконуватимуться строго по черзі.

```python
import threading
import time


def wait():
    time.sleep(0.1)


threads = [threading.Thread(target=wait) for _ in range(4)]
start = time.perf_counter()

for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

print(time.perf_counter() - start < 0.3)  # True
```

### Чи потрібна синхронізація, якщо є GIL

Так. GIL захищає нутрощі самого інтерпретатора, а не ваш код. Операція `counter += 1` складається з кількох кроків, і потік може втратити GIL між ними. Як і в Java, для цього є блокування. Запису `with lock:` відповідає `synchronized`: блокування знімається під час виходу з блоку, навіть у разі помилки.

```text
Потік A                     Потік B
1. прочитав counter = 5
                            2. прочитав counter = 5
                            3. записав counter = 6
4. записав counter = 6
Два додавання, а лічильник виріс на 1.
```

```java
class Counter {
    private int value;

    synchronized void add() {
        value++;
    }

    synchronized int get() {
        return value;
    }
}

var counter = new Counter();
var threads = new ArrayList<Thread>();
for (int i = 0; i < 4; i++) {
    threads.add(Thread.ofPlatform().start(() -> {
        for (int j = 0; j < 10_000; j++) {
            counter.add();
        }
    }));
}
for (var thread : threads) {
    thread.join();
}
System.out.println(counter.get());  // 40000
```

```python
import threading


class Counter:
    def __init__(self):
        self.value = 0
        self._lock = threading.Lock()

    def add(self):
        with self._lock:
            self.value += 1


def run(counter):
    for _ in range(10_000):
        counter.add()


counter = Counter()
threads = [threading.Thread(target=run, args=(counter,)) for _ in range(4)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()
print(counter.value)  # 40000
```

Обережно: `Lock` у Python не реентерабельний, тобто той самий потік не може взяти його вдруге. `synchronized` і `ReentrantLock` у Java це дозволяють. Точний аналог — `RLock`.

```python
import threading

# Звичайний Lock: другий acquire у тому самому потоці завис би назавжди.
lock = threading.Lock()
lock.acquire()
print(lock.acquire(blocking=False))  # False
lock.release()

# RLock можна брати повторно.
rlock = threading.RLock()
with rlock:
    with rlock:
        print("inside")  # inside
```

### Чим замінити `ExecutorService`

Пул потоків працює так само. `submit` повертає `Future`, а `result()` — це `get()`. Замість `try` із `shutdown` використовується `with`: під час виходу з блоку пул дочекається всіх завдань і закриється.

```java
int square(int n) {
    return n * n;
}

try (var pool = Executors.newFixedThreadPool(4)) {
    Future<Integer> future = pool.submit(() -> square(3));
    System.out.println(future.get());  // 9

    var tasks = new ArrayList<Callable<Integer>>();
    for (int n : List.of(1, 2)) {
        tasks.add(() -> square(n));
    }
    for (var done : pool.invokeAll(tasks)) {
        System.out.print(done.get() + " ");  // 1 4
    }
}
```

```python
from concurrent.futures import ThreadPoolExecutor, as_completed


def square(n):
    return n * n


with ThreadPoolExecutor(max_workers=4) as pool:
    future = pool.submit(square, 3)
    print(future.result())  # 9

    # map: результати в порядку аргументів.
    print(list(pool.map(square, [1, 2])))  # [1, 4]

    # as_completed: у порядку готовності, як CompletionService.
    futures = [pool.submit(square, n) for n in [1, 2]]
    print(sorted(f.result() for f in as_completed(futures)))  # [1, 4]
```

Виняток із завдання поводиться як у Java: він зберігається у `Future` і викидається під час виклику `result()`. Якщо результат ніхто не запросив, помилка загубиться мовчки.

### Що чому відповідає в `java.util.concurrent`

| Java | Python |
|---|---|
| `Thread` | `threading.Thread` |
| `synchronized`, `ReentrantLock` | `threading.RLock` |
| `Semaphore` | `threading.Semaphore` |
| `CountDownLatch` | `threading.Event`, `Barrier` |
| `CyclicBarrier` | `threading.Barrier` |
| `Condition` | `threading.Condition` |
| `BlockingQueue` | `queue.Queue` |
| `ConcurrentHashMap` | `dict` і `Lock` |
| `AtomicInteger` | `int` і `Lock` |
| `ThreadLocal` | `threading.local` |
| `ExecutorService` | `ThreadPoolExecutor` |
| `ForkJoinPool` | `ProcessPoolExecutor` |
| `Future` | `concurrent.futures.Future` |
| `CompletableFuture` | `asyncio.Task` |
| `volatile` | не потрібен |

### Як зробити виробника і споживача

`queue.Queue` — це `BlockingQueue`: `put` чекає, якщо черга повна, а `get` чекає, якщо вона порожня. Цифри показують порядок роботи. Для завершення в чергу прийнято класти особливе значення, у Java його називають poison pill.

```java
var tasks = new ArrayBlockingQueue<Integer>(10);
var results = new ArrayList<Integer>();

var consumer = Thread.ofPlatform().start(() -> {
    try {
        int item;
        while ((item = tasks.take()) != -1) {
            results.add(item * 2);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

tasks.put(1);
tasks.put(2);
tasks.put(-1);
consumer.join();
System.out.println(results);  // [2, 4]
```

```python
import queue
import threading

tasks = queue.Queue(maxsize=10)
results = []


def consume():

    # 2. Беремо із черги; якщо порожньо, потік чекає.
    while (item := tasks.get()) is not None:
        results.append(item * 2)


consumer = threading.Thread(target=consume)
consumer.start()

# 1. Кладемо завдання.
tasks.put(1)
tasks.put(2)

# 3. None повідомляє, що роботи більше немає.
tasks.put(None)
consumer.join()
print(results)  # [2, 4]
```

### Як зупинити потік

Методу `interrupt` у Python немає, ззовні потік зупинити не можна. Потік сам перевіряє прапорець, і для цього є `Event`. Метод `wait` заразом замінює `sleep`, який прокидається одразу, щойно прапорець виставлено.

```java
var worker = Thread.ofPlatform().start(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            return;
        }
    }
});

worker.interrupt();
worker.join();
System.out.println(worker.isAlive());  // false
```

```python
import threading

stop = threading.Event()


def worker():

    # Чекаємо 10 секунд або сигналу зупинки, залежно від того, що настане раніше.
    while not stop.wait(timeout=10):
        pass


thread = threading.Thread(target=worker)
thread.start()

stop.set()
thread.join()
print(thread.is_alive())  # False
```

### Як рахувати на всіх ядрах

`ProcessPoolExecutor` має вигляд пулу потоків, але запускає окремі процеси. У кожного свій інтерпретатор, своя пам'ять і свій GIL, тому обчислення йдуть паралельно. За це доводиться платити: аргументи й результати копіюються між процесами через серіалізацію (вона називається pickle), тому лямбду передати не можна, а спільних змінних немає. Код запуску мусить лежати всередині `if __name__ == "__main__"`, бо новий процес імпортує файл заново.

```java
var result = IntStream.of(1, 2, 3, 4)
    .parallel()
    .map(n -> n * n)
    .sum();
System.out.println(result);  // 30
```

```python
from concurrent.futures import ProcessPoolExecutor


def square(n):
    return n * n


if __name__ == "__main__":
    with ProcessPoolExecutor() as pool:
        print(sum(pool.map(square, [1, 2, 3, 4])))  # 30
```

GIL поступово зникає. З версії 3.13 є окрема збірка інтерпретатора без GIL, з 3.14 вона офіційно підтримується, але типово не ввімкнена, і частина бібліотек до неї ще не готова. У ній потоки працюють паралельно, як у Java. З 3.14 є і `InterpreterPoolExecutor`, де в кожного потоку свій ізольований інтерпретатор і свій GIL.

## Асинхронність: asyncio

### Що таке asyncio

Це спосіб обслуговувати тисячі операцій, що чекають, в одному потоці. Потік у Python дорогий і впирається в GIL, тому сервіси зазвичай будують саме на ньому. У Java те саме завдання розв'язують Netty і WebFlux, а тепер і віртуальні потоки. Знадобляться чотири терміни:

- **Корутина** (coroutine) — функція, оголошена через `async def`. Вона вміє призупинятися й потім продовжувати роботу.
- **`await`** — точка призупинення. Корутина каже: «я чекаю, займіться поки іншими».
- **Цикл подій** (event loop) — диспетчер, один на потік. Він вибирає, яка корутина працює далі.
- **Завдання** (task) — корутина, поставлена в цикл подій на виконання. Це аналог `CompletableFuture`.

Порівняйте з віртуальними потоками. Код робить те саме: дві операції очікування йдуть одночасно.

```java
String fetch(String name) throws InterruptedException {
    Thread.sleep(100);
    return name + " done";
}

try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
    var user = pool.submit(() -> fetch("user"));
    var orders = pool.submit(() -> fetch("orders"));
    System.out.println(user.get());  // user done
    System.out.println(orders.get());  // orders done
}
```

```python
import asyncio


async def fetch(name):
    await asyncio.sleep(0.1)
    return f"{name} done"


async def main():

    # gather запускає обидві корутини й чекає на обидві.
    user, orders = await asyncio.gather(fetch("user"), fetch("orders"))
    print(user)  # user done
    print(orders)  # orders done


# asyncio.run створює цикл подій і запускає в ньому main.
asyncio.run(main())
```

### Як це працює по кроках

Потік один, і вся робота йде по черзі. Перемикання відбувається лише на `await`. Цифри показують реальний порядок виведення.

```python
import asyncio


async def first():
    print("first starts")  # 1. first starts

    # first віддає керування циклу, і він запускає second.
    await asyncio.sleep(0.2)
    print("first ends")  # 4. first ends


async def second():
    print("second starts")  # 2. second starts

    # Тепер чекають обидві, і потік вільний.
    await asyncio.sleep(0.1)
    print("second ends")  # 3. second ends


async def main():
    await asyncio.gather(first(), second())


asyncio.run(main())
```

### Чому виклик `async def` нічого не робить

Виклик `fetch()` не запускає код, а лише створює об'єкт корутини. Код запускається через `await` або через `create_task`. Друга пастка в тому, що сам по собі `await` не створює одночасності: два `await` підряд виконуються по черзі. Щоб операції йшли одночасно, потрібні завдання.

```java
CompletableFuture<String> fetch(String name) {
    return CompletableFuture.supplyAsync(() -> name);
}

var user = fetch("user");
var orders = fetch("orders");
System.out.println(user.join() + " " + orders.join());  // user orders
```

```python
import asyncio
import time


async def fetch(name):
    await asyncio.sleep(0.1)
    return name


async def one_by_one():

    # Друга операція почнеться лише після першої: 0,2 секунди.
    start = time.perf_counter()
    await fetch("user")
    await fetch("orders")
    print(round(time.perf_counter() - start, 1))  # 0.2


async def at_once():
    start = time.perf_counter()

    # create_task одразу ставить корутину в чергу, як supplyAsync.
    user = asyncio.create_task(fetch("user"))
    orders = asyncio.create_task(fetch("orders"))

    # await — це join: дочекатися результату.
    print(await user, await orders)  # user orders
    print(round(time.perf_counter() - start, 1))  # 0.1


asyncio.run(one_by_one())
asyncio.run(at_once())
```

### Що буде, якщо викликати блокувальну функцію

Це головна помилка в асинхронному коді. Звичайний `time.sleep`, драйвер бази чи `requests` не містять `await`, тому не віддають керування. Цикл подій стоїть, і всі інші запити чекають. Те саме у WebFlux буває за блокувального виклику на потоці Netty. Такі виклики потрібно відправляти в окремий потік через `asyncio.to_thread`.

```python
import asyncio
import time


async def bad():

    # Увесь цикл подій стоїть: інші завдання не працюють 0,1 секунди.
    time.sleep(0.1)


async def good():

    # Функція йде в інший потік, а цикл продовжує працювати.
    await asyncio.to_thread(time.sleep, 0.1)


async def measure(work):
    start = time.perf_counter()
    await asyncio.gather(work(), work())
    return round(time.perf_counter() - start, 1)


print(asyncio.run(measure(bad)))  # 0.2
print(asyncio.run(measure(good)))  # 0.1
```

### Як правильно запускати групу завдань

`TaskGroup` з'явився в 3.11, це аналог `StructuredTaskScope` у Java. Правило просте: завдання не переживають блок `async with`. Якщо одне впало, інші скасовуються, а помилки приходять разом у групі винятків. Для їх обробки є особлива форма `except*`. З `gather` цього немає: у разі помилки інші завдання продовжать працювати самі по собі.

```python
import asyncio


async def ok():
    await asyncio.sleep(0.2)
    print("never printed")


async def fail():
    await asyncio.sleep(0.1)
    raise ValueError("boom")


async def main():
    try:
        async with asyncio.TaskGroup() as tasks:

            # 1. Запускаємо два завдання.
            tasks.create_task(ok())
            tasks.create_task(fail())

    # 2. fail упала, тому ok скасовано.
    # 3. except* ловить потрібний тип із групи помилок.
    except* ValueError as group:
        print(group.exceptions)  # (ValueError('boom'),)


asyncio.run(main())
```

### Як задати таймаут і скасувати завдання

Скасування працює так: `task.cancel()` кидає `CancelledError` усередині корутини в точці `await`. Це схоже на `InterruptedException`. Ловити й ковтати її не можна, інакше завдання не зупинити, тому очищення роблять у `finally`. `CancelledError` не успадковує `Exception`, тому звичайний `except Exception` її не зловить.

```java
var slow = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
    }
    return "done";
});

try {
    slow.orTimeout(50, TimeUnit.MILLISECONDS).join();
} catch (CompletionException e) {
    System.out.println(e.getCause() instanceof TimeoutException);  // true
}
```

```python
import asyncio


async def slow():
    try:
        await asyncio.sleep(1)
    finally:
        print("cleanup")  # 2. cleanup


async def main():
    try:

        # 1. Через 0,05 секунди завдання скасують.
        async with asyncio.timeout(0.05):
            await slow()

    # 3. Ззовні скасування перетворюється на TimeoutError.
    except TimeoutError:
        print("timed out")  # 3. timed out


asyncio.run(main())
```

### Чому завдання може зникнути

Цикл подій зберігає на завдання лише слабкі посилання. Якщо ви запустили завдання й не зберегли результат `create_task`, збирач сміття може його видалити просто під час роботи. Крім того, помилку в такому завданні ніхто не побачить. Тому посилання зберігають або завдання запускають у `TaskGroup`.

```python
import asyncio

background = set()


def fire_and_forget(coroutine):
    task = asyncio.create_task(coroutine)

    # Тримаємо посилання, поки завдання працює, потім прибираємо самі.
    background.add(task)
    task.add_done_callback(background.discard)


async def notify():
    print("sent")  # sent


async def main():
    fire_and_forget(notify())
    await asyncio.sleep(0)


asyncio.run(main())
```

### Як обмежити кількість одночасних завдань

Сто тисяч завдань створити легко, але стільки запитів база не витримає. Як і з віртуальними потоками в Java, обмеження роблять семафором.

```java
var limit = new Semaphore(10);

try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 100; i++) {
        pool.submit(() -> {
            limit.acquire();
            try {
                return 1;
            } finally {
                limit.release();
            }
        });
    }
}
```

```python
import asyncio


async def fetch(limit, n):

    # Не більше 10 завдань одночасно всередині блоку.
    async with limit:
        await asyncio.sleep(0)
        return n


async def main():
    limit = asyncio.Semaphore(10)
    results = await asyncio.gather(*(fetch(limit, n) for n in range(100)))
    print(len(results))  # 100


asyncio.run(main())
```

### Чи потрібні блокування в asyncio

Між двома `await` код виконується безперервно, бо потік один. Тому `counter += 1` тут безпечний. Гонка з'являється, лише якщо між читанням і записом є `await`. Для цього випадку є `asyncio.Lock`, але звичайний `threading.Lock` тут використовувати не можна: він зупинить увесь цикл подій.

```python
import asyncio

balance = 100


async def withdraw(lock, amount):
    global balance
    async with lock:

        # 1. Прочитали баланс.
        current = balance

        # 2. Без блокування сюди встигне зайти друге завдання з тим самим балансом.
        await asyncio.sleep(0)

        # 3. Записали новий баланс.
        balance = current - amount


async def main():
    lock = asyncio.Lock()
    await asyncio.gather(withdraw(lock, 10), withdraw(lock, 10))
    print(balance)  # 80


asyncio.run(main())
```

### Що таке `async with` і `async for`

Це ті самі `with` і `for`, тільки їхні внутрішні кроки самі містять `await`: наприклад, відкрити з'єднання чи отримати наступну порцію з мережі. Асинхронний генератор, як і звичайний, пишеться через `yield`, тільки всередині `async def`. Такий генератор схожий на `Flux` у WebFlux.

```python
import asyncio


class Connection:
    async def __aenter__(self):
        await asyncio.sleep(0)
        print("open")  # 1. open
        return self

    async def __aexit__(self, exc_type, exc, tb):
        await asyncio.sleep(0)
        print("close")  # 3. close


async def rows():
    for row in [1, 2]:
        await asyncio.sleep(0)
        yield row


async def main():
    async with Connection():
        async for row in rows():
            print(row)  # 2. 1 2


asyncio.run(main())
```

### Чим замінити `ThreadLocal`

Звичайний `threading.local` тут не підійде: потік один, і всі завдання побачили б одне значення. Для цього є `ContextVar`. Кожне завдання отримує свою копію контексту. Це аналог `ScopedValue` у Java та MDC у логуванні: ідентифікатор запиту не треба передавати через усі виклики.

```java
var requestId = new ThreadLocal<String>();
requestId.set("a");
System.out.println(requestId.get());
```

```python
import asyncio
from contextvars import ContextVar

request_id = ContextVar("request_id", default="-")


def log(message):
    print(request_id.get(), message)  # a work b work


async def handle(rid):
    request_id.set(rid)
    await asyncio.sleep(0)
    log("work")


async def main():
    await asyncio.gather(handle("a"), handle("b"))

    # У самій main значення не змінювалося.
    print(request_id.get())  # -


asyncio.run(main())
```

### Як зв'язати потоки та asyncio

| Звідки | Куди | Як |
|---|---|---|
| Звичайний код | корутина | `asyncio.run(main())` |
| Корутина | блокувальна функція | `await asyncio.to_thread(func)` |
| Інший потік | корутина | `asyncio.run_coroutine_threadsafe(coro, loop)` |
| Інший потік | звичайна функція в циклі | `loop.call_soon_threadsafe(func)` |

### Чим asyncio відрізняється від віртуальних потоків

Мета та сама: дешеве очікування. Різниця в тому, що віртуальні потоки ховають це всередині JVM, а звичайний блокувальний код продовжує працювати. В asyncio асинхронність видно в коді: функцію з `async` можна викликати лише з іншої такої функції. Це називають «розфарбовуванням функцій».

| | Віртуальні потоки | asyncio |
|---|---|---|
| Ядра процесора | усі | одне |
| Де перемикаються | за будь-якого блокування | лише на `await` |
| Блокувальний код | працює | зупиняє все |
| Гонки в коді | як у звичайних потоків | лише між `await` |
| Скасування | `interrupt` | `CancelledError` |
| Бібліотеки | звичайні | потрібні асинхронні |

## Залежності та тести

### Чим замінити Maven і Gradle

У Java залежності лежать у спільному кеші `~/.m2`, а потрібні версії передаються кожному проєкту через classpath. У Python встановлена бібліотека видна всій копії інтерпретатора, тому в кожного проєкту своя копія в каталозі `.venv`. Такий каталог називають віртуальним оточенням. Опис проєкту лежить у файлі `pyproject.toml`, а точні версії в lock-файлі. Зараз усім цим зазвичай керує інструмент uv.

| Java | Python |
|---|---|
| `pom.xml`, `build.gradle` | `pyproject.toml` |
| `gradle.lockfile` | `uv.lock` |
| Maven Central | PyPI |
| `mvn dependency:tree` | `uv tree` |
| `gradle dependencies add` | `uv add httpx` |
| `gradle build` | `uv build` |
| `gradle test` | `uv run pytest` |
| Checkstyle, Spotless | `ruff` |
| `javac` як перевірка типів | `mypy` |
| `.jar` | `.whl` |

Імпорт у Python шукає модулі за списком каталогів `sys.path`, як classpath у Java. Циклічний імпорт тут небезпечний, на відміну від Java: імпорт виконує файл, і якщо `a.py` імпортує `b.py`, а той знову `a.py`, другий побачить наполовину виконаний модуль і впаде з `ImportError`. Лікується це винесенням спільного коду в третій модуль.

### Чим замінити JUnit

Стандарт на практиці — pytest. Тест — це звичайна функція з іменем на `test_`, а замість набору `assertEquals` використовується звичайний `assert`. У разі падіння pytest сам показує, що з чим порівнювалося. Замість `@BeforeEach` є фікстура: функція, чиє ім'я тест указує як параметр. Усе, що стоїть після `yield` у фікстурі, виконується після тесту, як `@AfterEach`.

```java
class CartTest {
    private List<String> cart;

    @BeforeEach
    void setUp() {
        cart = new ArrayList<>();
    }

    @Test
    void addsItem() {
        cart.add("tea");
        assertEquals(1, cart.size());
    }

    @ParameterizedTest
    @CsvSource({"2, 4", "3, 9"})
    void squares(int input, int expected) {
        assertEquals(expected, input * input);
    }

    @Test
    void rejectsBadNumber() {
        assertThrows(NumberFormatException.class, () -> Integer.parseInt("x"));
    }
}
```

```python
import pytest


# Замінює @BeforeEach: pytest викличе її перед кожним тестом.
@pytest.fixture
def cart():
    return []


# Ім'я параметра збігається з іменем фікстури.
def test_adds_item(cart):
    cart.append("tea")
    assert len(cart) == 1


@pytest.mark.parametrize("value, expected", [(2, 4), (3, 9)])
def test_squares(value, expected):
    assert value * value == expected


def test_rejects_bad_number():
    with pytest.raises(ValueError):
        int("x")
```

### Чим замінити Mockito

Заглушки є у стандартній бібліотеці, у модулі `unittest.mock`. `Mock` приймає будь-який виклик і запам'ятовує його. Функція `patch` замінює ім'я у вказаному модулі на час тесту. Важливе правило: підміняти потрібно там, де ім'я використовується, а не там, де оголошене.

```java
var repository = mock(UserRepository.class);
when(repository.find(1)).thenReturn("Ann");

assertEquals("Ann", repository.find(1));
verify(repository).find(1);
```

```python
from unittest.mock import Mock

repository = Mock()
repository.find.return_value = "Ann"

print(repository.find(1))  # Ann
repository.find.assert_called_once_with(1)

# Змусити метод кинути виняток: thenThrow.
repository.save.side_effect = ConnectionError("down")
```

Хоча підміна працює, код зручніше тестувати так само, як у Java: через залежності, передані в конструктор. Так написано сервіс наприкінці статті.

## Пам'ять і продуктивність

### Як Python керує пам'яттю

У JVM об'єкти живуть, поки їх не знайде збирач сміття. У CPython кожен об'єкт має лічильник посилань, і об'єкт видаляється одразу, щойно лічильник став нулем. Циклічні посилання так не виявити, тому додатково є збирач циклів із модуля `gc`.

| | JVM | CPython |
|---|---|---|
| Коли видаляється об'єкт | коли дійде збирач | одразу, коли немає посилань |
| Переміщення об'єктів | так, купа стискається | ні |
| Налаштування розміру | `-Xmx` | немає, обмежує контейнер |
| Розмір `int` | 4 байти | близько 28 байтів |

### Що робити, якщо повільно

Спочатку виміряти. `cProfile` — це простий профайлер, а для працюючого сервісу є py-spy, який підключається ззовні. Зазвичай найшвидше допомагає вибір правильної структури даних і вбудованих функцій, написаних на C: `sum`, `sorted`, `set`. Важкі обчислення роблять у бібліотеках на C на кшталт NumPy, які відпускають GIL на час роботи.

## Архітектура сервісу на Python

### Що замінює Tomcat і Servlet API

Схема та сама: є сервер, який приймає з'єднання, і стандартний інтерфейс, через який він викликає ваш застосунок. Інтерфейсу два: WSGI для синхронного коду та ASGI для асинхронного.

| Java | Python | Що це |
|---|---|---|
| Servlet API | WSGI | синхронний інтерфейс сервера й застосунку |
| WebFlux `HttpHandler` | ASGI | асинхронний інтерфейс |
| Tomcat, Jetty | gunicorn | сервер для WSGI |
| Netty | uvicorn | сервер для ASGI |
| Spring MVC | Django, Flask | синхронні фреймворки |
| WebFlux | FastAPI | асинхронний фреймворк |

### Чим модель роботи відрізняється від Java

Java-сервіс — це один процес із пулом потоків і спільною пам'яттю. Через GIL сервіс на Python — це кілька процесів, їх називають воркерами. Звідси випливає ось що:

- **Масштабування.** У Kubernetes зазвичай тримають один воркер на под і додають репліки.
- **Кеш у пам'яті** у кожного воркера свій. Спільні дані зберігають у Redis або базі.
- **Пул з'єднань** теж у кожного свій: 4 воркери по 10 з'єднань — це 40 з'єднань із базою.
- **Планувальник** запуститься в кожному воркері, тому його виносять в окремий процес.
- **Довгі обчислення** в обробнику запиту неприпустимі. Їх відправляють у чергу завдань, наприклад Celery.

### Як обійтися без Spring

Контейнер залежностей майже ніколи не потрібен. Залежності передають через конструктор, а замість контексту Spring є одна функція під час запуску, яка створює об'єкти й передає їх одне одному. Таке місце називають composition root. Шари при цьому ті самі, тільки проходять не пакетами з класами, а модулями.

| Spring | Модуль у Python | Що знає |
|---|---|---|
| `@Entity` | `domain.py` | лише стандартну бібліотеку |
| інтерфейси репозиторіїв | `ports.py` | домен |
| `@Service` | `service.py` | домен і порти |
| `@Repository` | `storage.py` | домен і базу |
| `@RestController` | `http_api.py` | сервіс і HTTP |
| `@Configuration`, `main` | `__main__.py` | усе |

### Які патерни з Java тут не потрібні

Багато патернів у Java обходять обмеження мови, яких у Python немає. Наприклад, стратегія в Java — це інтерфейс і клас, а в Python просто функція.

```java
interface Discount {
    double apply(double price);
}

class Seasonal implements Discount {
    public double apply(double price) {
        return price * 0.9;
    }
}

double checkout(double price, Discount discount) {
    return discount.apply(price);
}

System.out.println(checkout(100, new Seasonal()));  // 90.0
```

```python
def seasonal(price):
    return price * 0.9


def checkout(price, discount):
    return discount(price)


print(checkout(100, seasonal))  # 90.0
```

| Java | Python |
|---|---|
| Singleton | об'єкт на рівні модуля |
| Factory | функція або сам клас |
| Builder | іменовані аргументи та `replace` |
| Strategy, Command | передати функцію |
| Observer | список функцій |
| Visitor | `match`, `singledispatch` |
| Decorator | `@decorator` |
| Iterator | генератор |
| Proxy | `__getattr__` |
| Клас-утиліта | функції в модулі |
| Гетери й сетери | поле, потім `@property` |

Код, написаний із Java-звичками, видно одразу. Ось що видає такий код:

```python
# Погано
for i in range(len(items)):
    print(items[i])

# Добре
for item in items:
    print(item)

# Погано
if len(items) == 0:
    pass

# Добре
if not items:
    pass

# Погано
if value == None or flag == True:
    pass

# Добре
if value is None or flag:
    pass

# Погано
user.get_name()

# Добре
user.name
```

## Мікросервіс на стандартній бібліотеці

### Що ми будуємо

Зберемо все разом і напишемо сервіс замовлень. Сторонніх бібліотек у ньому немає, щоб було видно саму мову. У справжньому проєкті HTTP-сервер і маршрути ви віддасте FastAPI, а SQLite замінить PostgreSQL, але заміняться лише файли на краях. Неоплачені замовлення старші за годину сервіс скасовує у фоні.

| Запит | Що робить | Відповідь |
|---|---|---|
| `GET /health` | перевірка, що сервіс живий | 200 |
| `POST /orders` | створити замовлення | 201, 422 |
| `GET /orders?status=paid` | список замовлень | 200 |
| `GET /orders/{id}` | показати замовлення | 200, 404 |
| `POST /orders/{id}/pay` | оплатити | 200, 409 |
| `POST /orders/{id}/cancel` | скасувати | 200, 409 |

Запит проходить через файли згори донизу, і кожен файл знає лише про нижнього сусіда. Логіка в центрі не залежить від зовнішнього світу. Вона спілкується з ним через інтерфейси, їх називають **портами**, а їхні реалізації для конкретної технології **адаптерами**. У Spring це сервіс, який працює з інтерфейсом репозиторію й не знає, що за ним JDBC.

```text
http_server.py    читає байти, будує об'єкт Request
      │
      ▼
http_api.py       вибирає маршрут, розбирає JSON
      │
      ▼
service.py        створює, оплачує, скасовує замовлення
      │        │
      ▼        ▼
domain.py    storage.py
правила      запити до SQLite
```

```text
orders-service/
├── pyproject.toml
├── Dockerfile
├── src/orders/
│   ├── __init__.py       порожній файл, ознака пакета
│   ├── __main__.py       запуск і збирання об'єктів
│   ├── config.py         налаштування з оточення
│   ├── domain.py         замовлення та його правила
│   ├── ports.py          що сервісу потрібно ззовні
│   ├── service.py        бізнес-логіка
│   ├── storage.py        адаптер SQLite
│   ├── http_server.py    HTTP-сервер на asyncio
│   ├── http_api.py       маршрути й помилки
│   └── logs.py           логи в JSON
└── tests/
    ├── test_service.py
    └── test_api.py
```

### Як описати проєкт

Файл `pyproject.toml` — це аналог `pom.xml`.

```toml title="pyproject.toml"
[project]
name = "orders-service"
version = "0.1.0"
requires-python = ">=3.12"

# Сторонніх бібліотек для роботи немає.
dependencies = []

# Створює команду orders-service, яка викликає функцію main.
[project.scripts]
orders-service = "orders.__main__:main"

# Як testImplementation у Gradle: в образ не потраплять.
[dependency-groups]
dev = ["mypy>=1.13", "pytest>=8", "ruff>=0.8"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/orders"]

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.mypy]
strict = true
files = ["src", "tests"]

[tool.ruff]
line-length = 88

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "ASYNC", "G"]
```

### Як описати замовлення

Почнемо з центру. Цей файл замінює клас `Order` із бізнес-методами. Він не знає ні про HTTP, ні про базу.

```python title="src/orders/domain.py"
from dataclasses import dataclass, replace
from datetime import datetime
from decimal import Decimal
from enum import StrEnum
from typing import Self


class OrderStatus(StrEnum):
    NEW = "new"
    PAID = "paid"
    CANCELLED = "cancelled"


# Спільний батько: веб-шар зловить усі помилки одним except.
class DomainError(Exception):
    pass


class OrderNotFound(DomainError):
    pass


class InvalidOrder(DomainError):
    pass


class IllegalTransition(DomainError):
    pass


# Незмінний запис: pay і cancel повертають нове замовлення.
@dataclass(frozen=True, slots=True)
class Order:
    id: str
    customer: str
    amount: Decimal
    status: OrderStatus
    created_at: datetime

    # Перевірка під час створення. NaN і Infinity — теж допустимі Decimal.
    def __post_init__(self) -> None:
        if not self.customer.strip():
            raise InvalidOrder("customer must not be empty")
        if not self.amount.is_finite() or self.amount <= 0:
            raise InvalidOrder("amount must be a positive number")

    def pay(self) -> Self:
        if self.status is not OrderStatus.NEW:
            raise IllegalTransition(f"cannot pay an order that is {self.status}")
        return replace(self, status=OrderStatus.PAID)

    def cancel(self) -> Self:
        if self.status is not OrderStatus.NEW:
            raise IllegalTransition(f"cannot cancel an order that is {self.status}")
        return replace(self, status=OrderStatus.CANCELLED)
```

### Як відокремити логіку від бази

Це аналог інтерфейсу `OrderRepository`. Реалізації не потрібно писати `implements`, тому в тестах можна використовувати просту підробку, яка ні від чого не успадковується.

```python title="src/orders/ports.py"
from collections.abc import Sequence
from typing import Protocol

from .domain import Order, OrderStatus


# Методи async: поки база відповідає, цикл зайнятий іншими запитами.
class OrderRepository(Protocol):
    async def add(self, order: Order) -> None: ...

    async def get(self, order_id: str) -> Order | None: ...

    async def find(
        self, status: OrderStatus | None, limit: int
    ) -> Sequence[Order]: ...

    # Зберігає новий статус, лише якщо в базі все ще expected.
    async def update_status(self, order: Order, expected: OrderStatus) -> bool: ...
```

### Як написати бізнес-логіку

Це аналог класу з `@Service`. У Java для годинника завели б `Clock`, а для ідентифікаторів інтерфейс із реалізацією. Тут досить передати функцію, а в тесті легко підставити іншу.

```python title="src/orders/service.py"
from collections.abc import Callable, Sequence
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from uuid import uuid4

from .domain import IllegalTransition, Order, OrderNotFound, OrderStatus
from .ports import OrderRepository


def _utc_now() -> datetime:
    return datetime.now(UTC)


def _new_id() -> str:
    return uuid4().hex


class OrderService:

    # Параметри після * передаються лише за іменем.
    def __init__(
        self,
        repository: OrderRepository,
        *,
        clock: Callable[[], datetime] = _utc_now,
        new_id: Callable[[], str] = _new_id,
    ) -> None:
        self._repository = repository
        self._clock = clock
        self._new_id = new_id

    async def create(self, customer: str, amount: Decimal) -> Order:
        order = Order(
            self._new_id(), customer, amount, OrderStatus.NEW, self._clock()
        )
        await self._repository.add(order)
        return order

    async def get(self, order_id: str) -> Order:
        order = await self._repository.get(order_id)
        if order is None:
            raise OrderNotFound(f"no order {order_id}")
        return order

    async def find(
        self, status: OrderStatus | None, limit: int = 100
    ) -> Sequence[Order]:
        return await self._repository.find(status, limit)

    # Order.pay — звичайна функція, у якої перший параметр self.
    async def pay(self, order_id: str) -> Order:
        return await self._change(order_id, Order.pay)

    async def cancel(self, order_id: str) -> Order:
        return await self._change(order_id, Order.cancel)

    async def expire_unpaid(self, older_than: timedelta) -> int:
        cutoff = self._clock() - older_than
        expired = 0
        for order in await self._repository.find(OrderStatus.NEW, limit=1000):
            if order.created_at < cutoff:

                # bool — це int, тому True додає 1.
                expired += await self._repository.update_status(
                    order.cancel(), OrderStatus.NEW
                )
        return expired

    async def _change(
        self, order_id: str, transition: Callable[[Order], Order]
    ) -> Order:

        # 1. Прочитали замовлення.
        order = await self.get(order_id)

        # 2. Перевірили правила й отримали нове замовлення.
        changed = transition(order)

        # 3. Умовний запис: поки йшов await, інший запит міг змінити замовлення.
        if not await self._repository.update_status(changed, expected=order.status):
            raise IllegalTransition(f"order {order_id} was changed concurrently")
        return changed
```

Крок 3 методу `_change` називають оптимістичним блокуванням: запис заздалегідь не блокується, а під час збереження перевіряється, що він не змінився. У JPA те саме завдання розв'язує `@Version`. Ось що без нього сталося б за двох одночасних оплат:

```text
Запит A                         Запит B
1. читає замовлення: new
                                 2. читає замовлення: new
3. UPDATE ... WHERE status='new'
   оновлено 1 рядок, успіх
                                 4. UPDATE ... WHERE status='new'
                                    оновлено 0 рядків, помилка 409
```

### Як звернутися до бази з асинхронного коду

Це аналог класу з `@Repository`. Драйвер `sqlite3` блокувальний, тому кожен виклик іде в окремий потік. Для PostgreSQL ви візьмете асинхронний драйвер, і `to_thread` зникне, а порт не зміниться.

```python title="src/orders/storage.py"
import asyncio
import sqlite3
from collections.abc import Callable, Sequence
from contextlib import closing
from datetime import datetime
from decimal import Decimal
from pathlib import Path

from .domain import Order, OrderStatus

_SCHEMA = """
    CREATE TABLE IF NOT EXISTS orders (
        id         TEXT PRIMARY KEY,
        customer   TEXT NOT NULL,
        amount     TEXT NOT NULL,
        status     TEXT NOT NULL,
        created_at TEXT NOT NULL
    );
    CREATE INDEX IF NOT EXISTS orders_status ON orders (status);
"""


def _to_order(row: sqlite3.Row) -> Order:
    return Order(
        id=row["id"],
        customer=row["customer"],
        amount=Decimal(row["amount"]),
        status=OrderStatus(row["status"]),
        created_at=datetime.fromisoformat(row["created_at"]),
    )


class SqliteOrderRepository:
    def __init__(self, path: Path) -> None:
        self._path = path

    async def migrate(self) -> None:
        await self._run(lambda c: c.executescript(_SCHEMA))

    async def add(self, order: Order) -> None:
        values = (
            order.id,
            order.customer,
            str(order.amount),
            order.status,
            order.created_at.isoformat(),
        )
        query = "INSERT INTO orders VALUES (?, ?, ?, ?, ?)"
        await self._run(lambda c: c.execute(query, values))

    async def get(self, order_id: str) -> Order | None:
        query = "SELECT * FROM orders WHERE id = ?"
        row = await self._run(lambda c: c.execute(query, (order_id,)).fetchone())
        return _to_order(row) if row else None

    async def find(self, status: OrderStatus | None, limit: int) -> Sequence[Order]:
        where, args = ("WHERE status = ?", [status]) if status else ("", [])
        query = f"SELECT * FROM orders {where} ORDER BY created_at LIMIT ?"
        rows = await self._run(lambda c: c.execute(query, (*args, limit)).fetchall())
        return [_to_order(row) for row in rows]

    async def update_status(self, order: Order, expected: OrderStatus) -> bool:
        query = "UPDATE orders SET status = ? WHERE id = ? AND status = ?"
        values = (order.status, order.id, expected)
        return await self._run(lambda c: c.execute(query, values).rowcount) == 1

    # Як TransactionTemplate: відкрити, виконати, зафіксувати чи відкотити.
    async def _run[T](self, work: Callable[[sqlite3.Connection], T]) -> T:
        def in_thread() -> T:
            connection = sqlite3.connect(self._path, timeout=5.0)
            connection.row_factory = sqlite3.Row

            # with connection робить commit або rollback, а closing закриває.
            with closing(connection), connection:
                return work(connection)

        return await asyncio.to_thread(in_thread)
```

### Як додати ідентифікатор запиту в логи

Це роль Logback і MDC. Кожен запит обробляється у своєму завданні, тому в нього своє значення `ContextVar`. Код, який пише в лог, про ідентифікатор нічого не знає.

```python title="src/orders/logs.py"
import json
import logging
from contextvars import ContextVar

request_id: ContextVar[str] = ContextVar("request_id", default="-")


# Один запис лога — один рядок JSON. Такі зручно розбирати збирачам логів.
class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        entry = {
            "time": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
            "level": record.levelname,
            "logger": record.name,
            "request_id": request_id.get(),
            "message": record.getMessage(),
        }
        if record.exc_info:
            entry["exception"] = self.formatException(record.exc_info)
        return json.dumps(entry, ensure_ascii=False)


def configure_logging(level: str) -> None:
    handler = logging.StreamHandler()
    handler.setFormatter(JsonFormatter())
    logging.basicConfig(level=level, handlers=[handler], force=True)
```

### Як написати HTTP-сервер на asyncio

У справжньому проєкті цю роботу робить uvicorn. Тут вона написана, щоб показати `asyncio` на реальному завданні. HTTP-запит — це звичайний текст: рядок запиту, заголовки, порожній рядок і тіло.

```http languages="HTTP, JSON"
POST /orders HTTP/1.1
Host: localhost:8080
Content-Length: 38

{"customer": "ann", "amount": "19.99"}
```

```python title="src/orders/http_server.py"
import asyncio
import json
import logging
from collections.abc import Awaitable, Callable
from contextlib import suppress
from dataclasses import dataclass
from http import HTTPStatus
from typing import Any
from urllib.parse import parse_qsl, urlsplit

log = logging.getLogger(__name__)

MAX_BODY = 1024 * 1024


@dataclass(frozen=True, slots=True)
class Request:
    method: str
    path: str
    query: dict[str, str]
    headers: dict[str, str]
    body: bytes


@dataclass(frozen=True, slots=True)
class Response:
    status: HTTPStatus
    payload: Any = None

    def encode(self) -> bytes:
        body = (
            b"" if self.payload is None else json.dumps(self.payload).encode("utf-8")
        )
        head = (
            f"HTTP/1.1 {self.status.value} {self.status.phrase}\r\n"
            "Content-Type: application/json\r\n"
            f"Content-Length: {len(body)}\r\n"
            "Connection: close\r\n\r\n"
        )

        # Протокол вимагає для заголовків кодування latin-1.
        return head.encode("latin-1") + body


# Псевдонім типу: будь-яка асинхронна функція від Request до Response.
type Handler = Callable[[Request], Awaitable[Response]]


async def read_request(reader: asyncio.StreamReader) -> Request:

    # 1. Читаємо байти до порожнього рядка: це заголовки.
    head = await reader.readuntil(b"\r\n\r\n")
    request_line, *header_lines = head.decode("latin-1").split("\r\n")
    method, target, _version = request_line.split(" ")

    # 2. Розбираємо заголовки у словник.
    headers = {}
    for line in header_lines:
        name, separator, value = line.partition(":")
        if separator:
            headers[name.strip().lower()] = value.strip()

    # 3. З Content-Length дізнаємося довжину тіла й читаємо його.
    length = int(headers.get("content-length", "0"))
    if length > MAX_BODY:
        raise ValueError("body is too large")
    body = await reader.readexactly(length)

    url = urlsplit(target)
    return Request(method, url.path, dict(parse_qsl(url.query)), headers, body)


class HttpServer:
    def __init__(
        self,
        handler: Handler,
        host: str,
        port: int,
        *,
        max_concurrent: int = 100,
        read_timeout: float = 5.0,
        handler_timeout: float = 30.0,
    ) -> None:
        self._handler = handler
        self._host = host
        self._port = port
        self._slots = asyncio.Semaphore(max_concurrent)
        self._read_timeout = read_timeout
        self._handler_timeout = handler_timeout

        # Цикл зберігає на завдання лише слабкі посилання, тому тримаємо свої.
        self._connections: set[asyncio.Task[Any]] = set()
        self._server: asyncio.Server | None = None

    # На кожне з'єднання asyncio створює завдання з _on_connect.
    async def start(self) -> int:
        self._server = await asyncio.start_server(
            self._on_connect, self._host, self._port
        )
        return int(self._server.sockets[0].getsockname()[1])

    async def stop(self, grace: float) -> None:
        assert self._server is not None

        # 1. Перестаємо приймати нові з'єднання.
        self._server.close()

        # 2. Даємо поточним час закінчити, решту скасовуємо.
        if self._connections:
            _, unfinished = await asyncio.wait(self._connections, timeout=grace)
            for task in unfinished:
                task.cancel()
            await asyncio.gather(*unfinished, return_exceptions=True)
        await self._server.wait_closed()

    async def _on_connect(
        self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
    ) -> None:
        task = asyncio.current_task()
        assert task is not None
        self._connections.add(task)
        task.add_done_callback(self._connections.discard)
        try:
            response = await self._respond(reader)
            if response is not None:
                writer.write(response.encode())

                # Якщо клієнт читає повільно, завдання тут почекає.
                await writer.drain()
        except ConnectionError:
            log.info("client disconnected")
        finally:
            writer.close()
            with suppress(ConnectionError):
                await writer.wait_closed()

    async def _respond(self, reader: asyncio.StreamReader) -> Response | None:

        # Таймаут захищає від клієнта, який підключився й нічого не надсилає.
        try:
            async with asyncio.timeout(self._read_timeout):
                request = await read_request(reader)
        except asyncio.IncompleteReadError:
            return None
        except (ValueError, asyncio.LimitOverrunError):
            return Response(HTTPStatus.BAD_REQUEST, {"error": "malformed request"})
        except TimeoutError:
            return Response(
                HTTPStatus.REQUEST_TIMEOUT, {"error": "request not read in time"}
            )

        # Семафор захищає базу від перевантаження, а таймаут від завислого запиту.
        async with self._slots:
            try:
                async with asyncio.timeout(self._handler_timeout):
                    return await self._handler(request)
            except TimeoutError:
                log.warning("timed out: %s %s", request.method, request.path)
                return Response(HTTPStatus.GATEWAY_TIMEOUT, {"error": "timed out"})

            # CancelledError сюди не потрапить: скасування має пройти наскрізь.
            except Exception:
                log.exception("failed: %s %s", request.method, request.path)
                return Response(
                    HTTPStatus.INTERNAL_SERVER_ERROR, {"error": "internal error"}
                )
```

### Як зробити маршрути й обробку помилок

Це роль `@RestController` і `@ControllerAdvice`. Маршрути зручно вибирати через `match`: зразок `"GET", ["orders", order_id]` підходить для `GET /orders/42` і одразу кладе `42` у змінну `order_id`.

```python title="src/orders/http_api.py"
import json
import logging
from decimal import Decimal, InvalidOperation
from http import HTTPStatus
from uuid import uuid4

from .domain import (
    DomainError,
    IllegalTransition,
    InvalidOrder,
    Order,
    OrderNotFound,
    OrderStatus,
)
from .http_server import Request, Response
from .logs import request_id
from .service import OrderService

log = logging.getLogger(__name__)

_ERROR_STATUS: dict[type[DomainError], HTTPStatus] = {
    OrderNotFound: HTTPStatus.NOT_FOUND,
    InvalidOrder: HTTPStatus.UNPROCESSABLE_ENTITY,
    IllegalTransition: HTTPStatus.CONFLICT,
}


def _to_json(order: Order) -> dict[str, str]:
    return {
        "id": order.id,
        "customer": order.customer,
        "amount": str(order.amount),
        "status": order.status,
        "created_at": order.created_at.isoformat(),
    }


def _parse_new_order(body: bytes) -> tuple[str, Decimal]:
    try:
        payload = json.loads(body)
    except ValueError as e:
        raise InvalidOrder("body is not valid JSON") from e

    # Зразок перевіряє і наявність полів, і їхні типи.
    # Сума — рядок: число в JSON втрачає точність.
    match payload:
        case {"customer": str(customer), "amount": str(amount)}:
            try:
                return customer, Decimal(amount)
            except InvalidOperation as e:
                raise InvalidOrder(f"bad amount: {amount!r}") from e
        case _:
            raise InvalidOrder("expected strings: customer, amount")


class OrdersApi:
    def __init__(self, service: OrderService) -> None:
        self._service = service

    # Об'єкт можна викликати як функцію, тому він підходить під Handler.
    async def __call__(self, request: Request) -> Response:
        request_id.set(request.headers.get("x-request-id") or uuid4().hex)
        try:
            response = await self._route(request)

        # Як @ControllerAdvice: помилка бізнес-логіки стає кодом відповіді.
        except DomainError as e:
            response = Response(_ERROR_STATUS[type(e)], {"error": str(e)})
        log.info("%s %s -> %d", request.method, request.path, response.status)
        return response

    async def _route(self, request: Request) -> Response:
        match request.method, request.path.strip("/").split("/"):
            case "GET", ["health"]:
                return Response(HTTPStatus.OK, {"status": "ok"})
            case "POST", ["orders"]:
                customer, amount = _parse_new_order(request.body)
                order = await self._service.create(customer, amount)
                return Response(HTTPStatus.CREATED, _to_json(order))
            case "GET", ["orders"]:
                return await self._list(request)
            case "GET", ["orders", order_id]:
                return Response(
                    HTTPStatus.OK, _to_json(await self._service.get(order_id))
                )
            case "POST", ["orders", order_id, "pay"]:
                return Response(
                    HTTPStatus.OK, _to_json(await self._service.pay(order_id))
                )
            case "POST", ["orders", order_id, "cancel"]:
                return Response(
                    HTTPStatus.OK, _to_json(await self._service.cancel(order_id))
                )
            case _:
                return Response(HTTPStatus.NOT_FOUND, {"error": "no such route"})

    async def _list(self, request: Request) -> Response:
        raw = request.query.get("status")
        try:
            status = OrderStatus(raw) if raw else None
        except ValueError as e:
            raise InvalidOrder(f"unknown status: {raw!r}") from e
        orders = await self._service.find(status)
        return Response(HTTPStatus.OK, [_to_json(order) for order in orders])
```

### Як читати налаштування

Це роль `@ConfigurationProperties`: незмінний об'єкт, один раз прочитаний зі змінних оточення. У тесті замість оточення можна передати звичайний словник.

```python title="src/orders/config.py"
import os
from collections.abc import Mapping
from dataclasses import dataclass
from datetime import timedelta
from pathlib import Path


@dataclass(frozen=True)
class Settings:
    host: str = "0.0.0.0"
    port: int = 8080
    database: Path = Path("orders.db")
    log_level: str = "INFO"
    unpaid_ttl: timedelta = timedelta(hours=1)
    shutdown_grace: float = 10.0

    @classmethod
    def from_env(cls, env: Mapping[str, str] = os.environ) -> "Settings":
        return cls(
            host=env.get("ORDERS_HOST", cls.host),
            port=int(env.get("ORDERS_PORT", cls.port)),
            database=Path(env.get("ORDERS_DATABASE", cls.database)),
            log_level=env.get("ORDERS_LOG_LEVEL", cls.log_level),
        )
```

### Де збираються всі об'єкти

Цей файл і є весь контейнер залежностей. Його можна прочитати згори донизу й побачити, що з чим пов'язано. Цифри показують, як сервіс завершується: Docker надсилає сигнал `SIGTERM`, а сервіс допрацьовує поточні запити.

```python title="src/orders/__main__.py"
import asyncio
import logging
import signal
from contextlib import suppress
from datetime import timedelta

from .config import Settings
from .http_api import OrdersApi
from .http_server import HttpServer
from .logs import configure_logging
from .service import OrderService
from .storage import SqliteOrderRepository

log = logging.getLogger("orders")


# Фонове завдання, як @Scheduled(fixedDelay = ...).
async def expire_loop(
    service: OrderService, stop: asyncio.Event, ttl: timedelta, every: float = 60.0
) -> None:
    while not stop.is_set():
        try:
            if expired := await service.expire_unpaid(ttl):
                log.info("expired %d unpaid orders", expired)

        # Один збій не повинен зупинити завдання назавжди.
        except Exception:
            log.exception("expiration job failed")

        # Чекаємо хвилину або сигналу зупинки.
        with suppress(TimeoutError):
            async with asyncio.timeout(every):
                await stop.wait()


async def run(settings: Settings) -> None:
    repository = SqliteOrderRepository(settings.database)
    await repository.migrate()
    service = OrderService(repository)
    server = HttpServer(OrdersApi(service), settings.host, settings.port)

    # 1. Docker надсилає SIGTERM, і обробник виставляє подію stop.
    stop = asyncio.Event()
    loop = asyncio.get_running_loop()
    for sig in (signal.SIGINT, signal.SIGTERM):
        signal.signal(sig, lambda *_: loop.call_soon_threadsafe(stop.set))

    port = await server.start()
    log.info("listening on %s:%d", settings.host, port)

    # TaskGroup гарантує, що фонове завдання не переживе run.
    async with asyncio.TaskGroup() as tasks:
        tasks.create_task(expire_loop(service, stop, settings.unpaid_ttl))

        # 2. Тут run прокидається.
        await stop.wait()
        log.info("shutting down")

        # 3. Сервер допрацьовує поточні запити й закривається.
        await server.stop(settings.shutdown_grace)
    log.info("stopped")


def main() -> None:
    settings = Settings.from_env()
    configure_logging(settings.log_level)
    asyncio.run(run(settings))


if __name__ == "__main__":
    main()
```

### Як протестувати сервіс

Юніт-тести працюють із підробкою замість бази. Клас `InMemoryOrders` зберігає замовлення у словнику. Він ніде не успадковує `OrderRepository`, але mypy перевірить, що він підходить.

```python title="tests/test_service.py"
import asyncio
from collections.abc import Sequence
from datetime import UTC, datetime, timedelta
from decimal import Decimal

import pytest

from orders.domain import (
    IllegalTransition,
    InvalidOrder,
    Order,
    OrderNotFound,
    OrderStatus,
)
from orders.service import OrderService

NOW = datetime(2026, 11, 5, 12, 0, tzinfo=UTC)


class InMemoryOrders:
    def __init__(self) -> None:
        self.orders: dict[str, Order] = {}

    async def add(self, order: Order) -> None:
        self.orders[order.id] = order

    async def get(self, order_id: str) -> Order | None:
        return self.orders.get(order_id)

    async def find(self, status: OrderStatus | None, limit: int) -> Sequence[Order]:
        found = [
            o for o in self.orders.values() if status is None or o.status is status
        ]
        return found[:limit]

    async def update_status(self, order: Order, expected: OrderStatus) -> bool:
        if self.orders[order.id].status is not expected:
            return False
        self.orders[order.id] = order
        return True


# Годинник заморожено, тому тести не залежать від поточного часу.
@pytest.fixture
def service() -> OrderService:
    return OrderService(InMemoryOrders(), clock=lambda: NOW, new_id=lambda: "id-1")


def test_new_order_can_be_paid(service: OrderService) -> None:
    async def scenario() -> None:
        order = await service.create("ann", Decimal("10.50"))
        assert (await service.pay(order.id)).status is OrderStatus.PAID
        assert (await service.get(order.id)).status is OrderStatus.PAID

    asyncio.run(scenario())


def test_paid_order_cannot_be_cancelled(service: OrderService) -> None:
    async def scenario() -> None:
        order = await service.create("ann", Decimal("1"))
        await service.pay(order.id)
        with pytest.raises(IllegalTransition):
            await service.cancel(order.id)

    asyncio.run(scenario())


@pytest.mark.parametrize(
    "customer, amount",
    [("", "1"), ("ann", "0"), ("ann", "-1"), ("ann", "NaN"), ("ann", "Infinity")],
)
def test_invalid_orders_are_rejected(
    service: OrderService, customer: str, amount: str
) -> None:
    with pytest.raises(InvalidOrder):
        asyncio.run(service.create(customer, Decimal(amount)))


def test_missing_order(service: OrderService) -> None:
    with pytest.raises(OrderNotFound):
        asyncio.run(service.get("nope"))


def test_old_unpaid_orders_expire() -> None:
    now = [NOW]
    service = OrderService(
        InMemoryOrders(), clock=lambda: now[0], new_id=lambda: "id-1"
    )

    async def scenario() -> None:
        order = await service.create("ann", Decimal("1"))
        assert await service.expire_unpaid(timedelta(hours=1)) == 0
        now[0] += timedelta(hours=2)
        assert await service.expire_unpaid(timedelta(hours=1)) == 1
        assert (await service.get(order.id)).status is OrderStatus.CANCELLED

    asyncio.run(scenario())
```

Інтеграційний тест піднімає справжній сервер на вільному порту (порт 0 означає «вибери сам») зі справжньою базою в тимчасовому каталозі.

```python title="tests/test_api.py"
import asyncio
import json
from pathlib import Path
from typing import Any

from orders.http_api import OrdersApi
from orders.http_server import HttpServer
from orders.service import OrderService
from orders.storage import SqliteOrderRepository


async def call(
    port: int, method: str, path: str, body: Any = None
) -> tuple[int, Any]:
    payload = b"" if body is None else json.dumps(body).encode()
    head = (
        f"{method} {path} HTTP/1.1\r\n"
        f"Host: test\r\nContent-Length: {len(payload)}\r\n\r\n"
    )
    reader, writer = await asyncio.open_connection("127.0.0.1", port)
    writer.write(head.encode() + payload)
    await writer.drain()
    response = await reader.read()
    writer.close()
    await writer.wait_closed()
    status_line, _, content = response.partition(b"\r\n\r\n")
    return int(status_line.split()[1]), json.loads(content) if content else None


def test_order_lifecycle_over_http(tmp_path: Path) -> None:
    async def scenario() -> None:
        repository = SqliteOrderRepository(tmp_path / "test.db")
        await repository.migrate()
        server = HttpServer(OrdersApi(OrderService(repository)), "127.0.0.1", 0)
        port = await server.start()
        try:
            assert (await call(port, "GET", "/health"))[0] == 200

            order = {"customer": "ann", "amount": "19.99"}
            status, created = await call(port, "POST", "/orders", order)
            assert status == 201
            assert created["amount"] == "19.99"
            assert created["status"] == "new"
            pay = f"/orders/{created['id']}/pay"

            status, paid = await call(port, "POST", pay)
            assert (status, paid["status"]) == (200, "paid")
            assert (await call(port, "POST", pay))[0] == 409

            _, listed = await call(port, "GET", "/orders?status=paid")
            assert [o["id"] for o in listed] == [created["id"]]
            assert (await call(port, "GET", "/orders?status=lost"))[0] == 422

            assert (await call(port, "GET", "/orders/nope"))[0] == 404
            assert (await call(port, "POST", "/orders", {"amount": 5}))[0] == 422
            assert (await call(port, "DELETE", "/orders"))[0] == 404
        finally:
            await server.stop(grace=1.0)

    asyncio.run(scenario())


def test_malformed_request_gets_400(tmp_path: Path) -> None:
    async def scenario() -> None:
        repository = SqliteOrderRepository(tmp_path / "test.db")
        await repository.migrate()
        server = HttpServer(OrdersApi(OrderService(repository)), "127.0.0.1", 0)
        port = await server.start()
        try:
            reader, writer = await asyncio.open_connection("127.0.0.1", port)
            writer.write(b"nonsense\r\n\r\n")
            await writer.drain()
            assert b"400 Bad Request" in await reader.read()
            writer.close()
            await writer.wait_closed()
        finally:
            await server.stop(grace=1.0)

    asyncio.run(scenario())
```

### Як запакувати сервіс у Docker

Збірка двоетапна, як із Gradle і JRE. Пояснення до кожного кроку в коментарях.

```dockerfile title="Dockerfile"
# Етап 1: uv ставить лише робочі залежності за lock-файлом.
FROM python:3.15-slim AS build
RUN pip install --no-cache-dir uv
WORKDIR /app
COPY pyproject.toml uv.lock ./
COPY src ./src
RUN uv sync --frozen --no-dev --no-editable

# Етап 2: готове оточення копіюється в чистий образ.
FROM python:3.15-slim
RUN useradd --create-home app && mkdir /data && chown app /data
COPY --from=build /app/.venv /app/.venv
USER app
ENV ORDERS_DATABASE=/data/orders.db PATH=/app/.venv/bin:$PATH
EXPOSE 8080
VOLUME /data
HEALTHCHECK CMD python -c "import urllib.request as r; \
    r.urlopen('http://127.0.0.1:8080/health', timeout=2)"

# Форма масиву: сервіс стане PID 1, і SIGTERM прийде просто йому.
CMD ["orders-service"]
```

### Як запустити сервіс

```shellscript
# Створити оточення й uv.lock
uv sync

# Тести
uv run pytest

# Типи в суворому режимі
uv run mypy

# Лінтер
uv run ruff check

# Запуск сервісу
uv run python -m orders

# Або в контейнері
docker build -t orders-service .
docker run --rm -p 8080:8080 orders-service
```

Створимо замовлення, оплатимо його, спробуємо оплатити ще раз і надішлемо неправильні дані.

```shellscript languages="Shell, JSON"
$ curl -s -X POST localhost:8080/orders \
    -d '{"customer": "ann", "amount": "19.99"}' | python -m json.tool
{
    "id": "2165d2a6a1ee4cbb966203d6099d49c9",
    "customer": "ann",
    "amount": "19.99",
    "status": "new",
    "created_at": "2026-11-05T18:44:05.717023+00:00"
}

$ ID=2165d2a6a1ee4cbb966203d6099d49c9
$ curl -s -X POST localhost:8080/orders/$ID/pay | python -m json.tool
{
    "id": "2165d2a6a1ee4cbb966203d6099d49c9",
    "customer": "ann",
    "amount": "19.99",
    "status": "paid",
    "created_at": "2026-11-05T18:44:05.717023+00:00"
}

$ curl -s -X POST localhost:8080/orders/$ID/pay
{"error": "cannot pay an order that is paid"}

$ curl -s -X POST localhost:8080/orders \
    -d '{"customer": "a", "amount": "NaN"}'
{"error": "amount must be a positive number"}
```

### Що з цього замінять фреймворки

Цей код написано руками лише для того, щоб побачити механізми. У справжньому проєкті більшу частину візьмуть на себе бібліотеки. Написаний тут `service.py` при цьому не зміниться.

| Тут написано | Що візьмуть у проєкті | Аналог у Java |
|---|---|---|
| `http_server.py` | uvicorn, gunicorn | Tomcat, Netty |
| `http_api.py` | FastAPI, Flask, Django | Spring MVC |
| розбір JSON | Pydantic | Jackson, Bean Validation |
| `storage.py` | SQLAlchemy, asyncpg | Hibernate, JDBC |
| `CREATE TABLE` | Alembic | Flyway, Liquibase |
| `Settings` | pydantic-settings | `@ConfigurationProperties` |
| `expire_loop` | Celery, APScheduler | `@Scheduled`, Quartz |
| `retry` | tenacity | Resilience4j |
| `logs.py` | structlog | Logback, MDC |
| немає | prometheus-client, OpenTelemetry | Micrometer |
| немає | httpx | `WebClient` |
| немає | aiokafka | Spring Kafka |

## Що читати далі

Першоджерела: [модель даних](https://docs.python.org/3/reference/datamodel.html), [документація asyncio](https://docs.python.org/3/library/asyncio.html), [розділ про збірку без GIL](https://docs.python.org/3/howto/free-threading-python.html), [документація mypy](https://mypy.readthedocs.io/) і [що нового в кожній версії](https://docs.python.org/3/whatsnew/index.html). Для порівняння з Java на цьому сайті є статті про [основи Java](/uk/java-core-questions/), [багатопотоковість](/uk/java-kotlin-multithreading/), [Project Loom](/uk/project-loom-manual/) і [Spring](/uk/java-spring-concepts/).
