---
title: "Python за Java разработчици"
url: "https://romankryvolapov.com/bg/python-for-java-developers/"
description: "Python за онези, които знаят Java: синтаксис и основни методи редом с Java, низове, колекции, генератори, нишки, asyncio и микроуслуга със стандартната библиотека."
language: bg
published: 2026-11-06
updated: 2026-11-13
---
Тази статия е за онези, които вече знаят 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]
```

### Какво е затваряне

Затварянето (closure) е функция, която е запомнила променливите от мястото, където е създадена. 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. Logged няма атрибут append, затова е извикан __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]
```

### Как да спрем нишка

В Python няма метод `interrupt`, отвън нишка не може да се спре. Нишката сама проверява флаг, а за това има `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` |
| ред в блока `dependencies { }` | `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 |
|---|---|---|
| Кога се изтрива обектът | когато стигне събирачът | веднага, когато няма референции |
| Преместване на обекти | да, heap се уплътнява | не |
| Настройка на размера | `-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 връзки към базата данни.
- **Планировчикът на задачи** (scheduler) ще се стартира във всеки воркър, затова се изнася в отделен процес.
- **Дългите изчисления** в обработчика на заявка са недопустими. Те се изпращат в опашка със задачи, например 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](/bg/java-core-questions/), [многонишковостта](/bg/java-kotlin-multithreading/), [Project Loom](/bg/project-loom-manual/) и [Spring](/bg/java-spring-concepts/).
