Roman Kryvolapov Engineering Blog

Python за Java разработчици

Тази статия е за онези, които вече знаят 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 няма декларация. Променливата се появява при първото присвояване, а типът е не на нея, а на стойността, която се намира в нея. Това се нарича динамична типизация. По-подробно за това какво е променлива се разказва по-долу.

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.

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. Основната разлика е, че думите в имената на променливи и функции се разделят с долна черта.

КаквоJavaPython
ПроменливаuserNameuser_name
Метод, функцияfindUser()find_user()
КласOrderServiceOrderService
КонстантаMAX_SIZEMAX_SIZE
Пакетcom.shop.ordersorders
ФайлOrderService.javaorder_service.py
Скрито полеprivate int size_size
Отстъп4 интервала4 интервала

Кои са базовите типове

Примитиви в Python няма, всичко е обект, дори числото. Затова няма и двойка int и Integer. Различни цели типове също няма, един int съхранява число с произволен размер.

JavaPythonПример
byte, short, int, long, BigIntegerint42
float, doublefloat3.14
booleanboolTrue, False
char, Stringstr"a", "Ann"
nullNoneNone
byte[]bytesb"abc"
Objectobjectobject()
BigDecimaldecimal.DecimalDecimal("9.99")

Обърнете внимание: True, False и None се пишат с главна буква. Отделен тип за символ няма, символът е низ с дължина 1.

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

Как да преобразуваме един тип в друг

Преобразуване в скоби няма. Вместо него класът на нужния тип се извиква като функция.

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 ще изведе грешка.

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 / винаги дава дробно число, а целочисленото деление се записва като //. Оператори ++ и -- няма.

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.

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 се смятат за лъжа.

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, а един от своите операнди. На това се основава краткият запис на стойност по подразбиране.

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() функцията просто се извиква. Ламбдата може да съдържа само един израз.

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 се подава функция, която по един обект връща онова, по което да се сортира.

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 няма. Една долна черта в началото на името означава „това е вътрешно, отвън не пипайте“, но забрана няма, това е конвенция.

JavaPythonКакво означава
public int sizesizeпублично
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, по време на работа от параметъра на типа не остава нищо. По-подробно в раздела за подсказките за типове.

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

Имената на стандартните изключения са други, но съответствието е просто:

JavaPython
ThrowableBaseException
ExceptionException
IllegalArgumentException, NumberFormatExceptionValueError
ClassCastExceptionTypeError
NullPointerExceptionAttributeError или TypeError
IndexOutOfBoundsExceptionIndexError
няма ключ в MapKeyError
IOExceptionOSError
FileNotFoundExceptionFileNotFoundError
ArithmeticExceptionZeroDivisionError
UnsupportedOperationExceptionNotImplementedError
StackOverflowErrorRecursionError
TimeoutExceptionTimeoutError

С какво да заменим 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 няма: файлът сам е модул, а името му е равно на името на файла. Каталог с модули се нарича пакет. В един файл може да се сложат колкото си искате класове и функции, а отделен файл за всеки клас тук не е прието. Може да се импортира както самият модул, така и отделно име от него.

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
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

Бърза справка по синтаксиса

JavaPython
int a = 1;a = 1
final int MAX = 1;MAX = 1
nullNone
true, falseTrue, False
a.equals(b)a == b
a == b за референцииa is b
&&, ||, !and, or, not
x > 0 ? a : ba if x > 0 else b
7 / 27 // 2
Math.pow(2, 3)2 ** 3
i++i += 1
else ifelif
switchmatch
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 * 2lambda x: x * 2
String::lengthlen
new User("Ann")User("Ann")
thisself
class B extends Aclass B(A):
super.run()super().run()
static int f()@staticmethod
record Point(int x)@dataclass(frozen=True)
interfaceProtocol
abstract classclass A(ABC):
List<String>list[str]
throwraise
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 закръгля надолу.

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.

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 не са нужни.

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.

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".

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, връщат нов низ.

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
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. Всяка част може да се пропусне. Индексите е удобно да се представят като номера под буквите. По същия начин срезовете работят със списъци.

низ: H e l l o
индекс: 0 1 2 3 4
от края: -5 -4 -3 -2 -1
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 се извиква от разделителя.

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.

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
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 и преди сравнение такъв текст се нормализира.

Java, Python
import unicodedata # Python
"😀".length() # Java: 2
len("😀") # Python: 1
"😀".codePointCount(0, 2) # Java: 1
len("😀") # Python: 1
# é като един символ и като e плюс знак за ударение.
"é".equals("é") # Java: false
"é" == "é" # Python: False
Normalizer.normalize("é", Normalizer.Form.NFC).equals("é") # Java: true
unicodedata.normalize("NFC", "é") == "é" # Python: True

Как да работим с регулярни изрази

Синтаксисът на изразите е почти същият, а методите са в модула re. Шаблонът е прието да се пише със суров низ. Ако няма съвпадение, методите връщат None.

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.

JavaPythonЗапис
ArrayListlist[1, 2]
List.oftuple(1, 2)
LinkedHashMapdict{"a": 1}
HashSetset{1, 2}
Set.offrozensetfrozenset({1, 2})
ArrayDeque, LinkedListcollections.dequedeque([1, 2])
PriorityQueueheapqфункции върху списък
int[]list или array.array[0] * 3
TreeMapняма, обикновено sorted(d)
ConcurrentHashMapdict и Lock

Как да работим със списък

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. Използва се за фиксиран набор от стойности, като координати или ред от база данни. А освен това може да служи за ключ на речник, а списъкът не може.

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.

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 има обикновени оператори. Те връщат ново множество и не променят изходните.

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. Те работят с обикновен списък, а най-отгоре винаги стои минималният елемент.

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.

Операцияlistdict, setdeque
Четене по индекс или ключO(1)O(1)O(n)
x in cO(n)O(1)O(n)
Добавяне в краяO(1)O(1)O(1)
Добавяне или премахване в началотоO(n)–O(1)
sortedO(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 е записана с един израз, без отделни извиквания.

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).

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. Частите на пътя се съединяват с оператора /.

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")

Къде да търсим останалото

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
JavaPython
System.exit(1)sys.exit(1)
System.err.println(x)print(x, file=sys.stderr)
new Scanner(System.in)input()
HttpClienturllib.request, на практика httpx
URLEncoder.encodeurllib.parse.quote
Propertiesconfigparser, tomllib
@Cacheablefunctools.lru_cache
Thread, ExecutorServicethreading, concurrent.futures
CompletableFutureasyncio
JUnitunittest, на практика pytest

С какво Python е устроен различно от Java

Как се изпълнява кодът

Основната реализация на езика се нарича CPython. Както javac и JVM, тя компилира изходния код в байткод и го изпълнява във виртуална машина, само че двете стъпки се правят с една команда. Байткодът се кешира в каталога __pycache__, като файловете .class. Основната разлика от JVM е, че байткодът обикновено се интерпретира. JIT компилаторът, който в JVM превръща горещия код в машинен, в CPython тепърва се появява и засега е експериментален. Оттук и разликата в скоростта.

JavaPython
Компилацияjavac предварителнопри стартиране
Грешката в типа се виждапри компилацияпри изпълнение на реда
JITC2, 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 копира само референцията. Удобно е променливата да се представи като етикет, залепен към обекта.

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, Nonelist
str, bytesdict
tuple, frozensetset, 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__финализатор, на него не може да се разчита, вместо него се използва withfinalize

Подсказки за типовете

Проверява ли 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

Как се записват типовете

JavaPython
int, Stringint, str
List<String>list[str]
Map<String, Integer>dict[str, int]
Set<Long>set[int]
int[]list[int]
двойка стойностиtuple[str, int]
неизменяем списъкtuple[str, ...]
@Nullable Stringstr | None
Objectobject
изобщо няма проверкаAny
Collection<? extends T>Iterable[T], Sequence[T]
Function<Integer, String>Callable[[int], str]
Class<T>type[T]
voidNone
sealed interface с вариантиA | B
ограничен набор от низовеLiteral["asc", "desc"]
връща thisSelf
никога не връщаNoReturn
typealias: няма в Javatype UserId = int

В стария код типовете изглеждат другояче. Ако срещнете такова нещо, знайте, че е същото.

Стар записНов записОт коя версия
List[str], Dict[str, int]list[str], dict[str, int]3.9
Optional[str]str | None3.10
Union[int, str]int | str3.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.

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.

ЗадачаJavaPython
Много чакане на мрежа или база даннинишки, виртуални нишки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: заключването се освобождава при излизане от блока, дори при грешка.

Нишка 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

JavaPython
Threadthreading.Thread
synchronized, ReentrantLockthreading.RLock
Semaphorethreading.Semaphore
CountDownLatchthreading.Event, Barrier
CyclicBarrierthreading.Barrier
Conditionthreading.Condition
BlockingQueuequeue.Queue
ConcurrentHashMapdict и Lock
AtomicIntegerint и Lock
ThreadLocalthreading.local
ExecutorServiceThreadPoolExecutor
ForkJoinPoolProcessPoolExecutor
Futureconcurrent.futures.Future
CompletableFutureasyncio.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
ОтмянаinterruptCancelledError
Библиотекиобикновенитрябват асинхронни

Зависимости и тестове

С какво се заменят Maven и Gradle

В Java зависимостите се намират в общ кеш ~/.m2, а нужните версии се предават на всеки проект през classpath. В Python инсталираната библиотека се вижда от цялото копие на интерпретатора, затова всеки проект има собствено копие в директорията .venv. Такава директория се нарича виртуална среда. Описанието на проекта е във файла pyproject.toml, а точните версии – в lock файла. Сега всичко това обикновено се управлява от инструмента uv.

JavaPython
pom.xml, build.gradlepyproject.toml
gradle.lockfileuv.lock
Maven CentralPyPI
mvn dependency:treeuv tree
ред в блока dependencies { }uv add httpx
gradle builduv build
gradle testuv run pytest
Checkstyle, Spotlessruff
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.

JVMCPython
Кога се изтрива обектъткогато стигне събирачътведнага, когато няма референции
Преместване на обектида, heap се уплътняване
Настройка на размера-Xmxняма, ограничава го контейнерът
Размер на int4 байтаоколо 28 байта

Какво да правим, ако е бавно

Първо се измерва. cProfile е прост профайлър, а за работеща услуга има py-spy, който се закача отвън. Обикновено най-бързо помага изборът на правилна структура от данни и на вградени функции, написани на C: sum, sorted, set. Тежките изчисления се правят в библиотеки на C като NumPy, които освобождават GIL по време на работа.

Архитектура на услуга на Python

Какво заменя Tomcat и Servlet API

Схемата е същата: има сървър, който приема връзките, и стандартен интерфейс, през който той извиква вашето приложение. Интерфейсите са два: WSGI за синхронен код и ASGI за асинхронен.

JavaPythonКакво е
Servlet APIWSGIсинхронен интерфейс между сървъра и приложението
WebFlux HttpHandlerASGIасинхронен интерфейс
Tomcat, Jettygunicornсървър за WSGI
Nettyuvicornсървър за ASGI
Spring MVCDjango, Flaskсинхронни фреймуърци
WebFluxFastAPIасинхронен фреймуърк

С какво моделът на работа се различава от Java

Java услугата е един процес с пул от нишки и обща памет. Заради GIL услугата на Python се състои от няколко процеса, които се наричат воркъри. От това следва следното:

  • Мащабиране. В Kubernetes обикновено се държи по един воркър на под и се добавят реплики.
  • Кешът в паметта е собствен за всеки воркър. Общите данни се пазят в Redis или в базата данни.
  • Пулът от връзки също е собствен за всеки: 4 воркъра по 10 връзки са 40 връзки към базата данни.
  • Планировчикът на задачи (scheduler) ще се стартира във всеки воркър, затова се изнася в отделен процес.
  • Дългите изчисления в обработчика на заявка са недопустими. Те се изпращат в опашка със задачи, например Celery.

Как да минем без Spring

Контейнер за зависимости почти никога не е нужен. Зависимостите се предават през конструктора, а вместо контекст на Spring има една функция при стартиране, която създава обектите и ги предава един на друг. Такова място се нарича composition root. Слоевете са същите, само че минават не по пакети с класове, а по модули.

SpringМодул в PythonКакво знае
@Entitydomain.pyсамо стандартната библиотека
интерфейси на хранилищатаports.pyдомейна
@Serviceservice.pyдомейна и портовете
@Repositorystorage.pyдомейна и базата данни
@RestControllerhttp_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
JavaPython
Singletonобект на ниво модул
Factoryфункция или самият клас
Builderименувани аргументи и replace
Strategy, Commandпредава се функция
Observerсписък от функции
Visitormatch, 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.

http_server.py чете байтове, строи обект Request
│
▼
http_api.py избира маршрут, разчита JSON
│
▼
service.py създава, плаща, отменя поръчка
│ │
▼ ▼
domain.py storage.py
правила заявки към SQLite
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 · 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 · 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 · 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 · 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. Ето какво би се случило без него при две едновременни плащания:

Заявка 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 · 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 · 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, JSON
POST /orders HTTP/1.1
Host: localhost:8080
Content-Length: 38
{"customer": "ann", "amount": "19.99"}
Python · 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 · 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 · 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 · 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 · 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 · 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
# Етап 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"]

Как да стартираме услугата

Shell
# Създава среда и 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

Ще създадем поръчка, ще я платим, ще опитаме да платим още веднъж и ще изпратим неверни данни.

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.pyuvicorn, gunicornTomcat, Netty
http_api.pyFastAPI, Flask, DjangoSpring MVC
разчитане на JSONPydanticJackson, Bean Validation
storage.pySQLAlchemy, asyncpgHibernate, JDBC
CREATE TABLEAlembicFlyway, Liquibase
Settingspydantic-settings@ConfigurationProperties
expire_loopCelery, APScheduler@Scheduled, Quartz
retrytenacityResilience4j
logs.pystructlogLogback, MDC
нямаprometheus-client, OpenTelemetryMicrometer
нямаhttpxWebClient
нямаaiokafkaSpring Kafka

Какво да четем по-нататък

Първоизточници: моделът на данните, документацията на asyncio, разделът за билд без GIL, документацията на mypy и какво е ново във всяка версия. За сравнение с Java на този сайт има статии за основите на Java, многонишковостта, Project Loom и Spring.