---
title: "Python для Java-разработчиков"
url: "https://romankryvolapov.com/ru/python-for-java-developers/"
description: "Python для тех, кто знает Java: синтаксис и основные методы рядом с Java, строки, коллекции, генераторы, потоки, asyncio и микросервис на стандартной библиотеке."
language: ru
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

# Хэш SHA-256 первые 8 символов.
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](/ru/java-core-questions/), [многопоточность](/ru/java-kotlin-multithreading/), [Project Loom](/ru/project-loom-manual/) и [Spring](/ru/java-spring-concepts/).
