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 файлът просто се изпълнява отгоре надолу.
public class Main { public static void main(String[] args) { System.out.println("Hello"); }}# Целият файл: нито клас, нито main, нито точка и запетая.print("Hello") # HelloАко файлът може както да се стартира, така и да се импортира от друг файл, кодът за стартиране се скрива в проверка. Това е най-близкото до main в Java.
def main(): print("Hello") # Hello
# __name__ – името на текущия модул. Ако файлът е стартиран директно,# там е низът "__main__". Ако е импортиран – името на файла,# и main() няма да се извика.if __name__ == "__main__": main()Как се записват блоковете и коментарите
Вместо фигурни скоби Python използва отстъпи. Това не е оформление, а синтаксис: грешен отстъп няма да позволи на програмата да стартира.
int price = 150;
// Едноредов коментарif (price > 100) { System.out.println("expensive"); // expensive} else { System.out.println("cheap");}
/* Многоредов коментар в Python няма */price = 150
# Едноредов коментар# Скоби около условието не са нужни, в края на реда има двоеточие,# а тялото на блока е изместено с 4 интервала.if price > 100: print("expensive") # expensiveelse: print("cheap")Дългият израз се пренася вътре в скоби. Вместо Javadoc се пише низ с документация веднага под декларацията, нарича се docstring.
/** * Изчислява сумата с данък. */int total(int price, int tax) { return price + tax;}
System.out.println(total(100, 20)); // 120def total(price, tax): """Изчислява сумата с данък."""
# Вътре в скобите пренасянето на ред е разрешено. return (price + tax)
print(total(100, 20)) # 120
# Docstring е достъпен и по време на работата на програмата.print(total.__doc__) # Изчислява сумата с данък.Заместителят на празния блок в Java е {}. В Python празен блок не може да се запише, затова има думата pass, която не прави нищо.
void todo() {}
class Empty {}def todo(): pass
class Empty: passКак се декларира променлива
В Python няма декларация. Променливата се появява при първото присвояване, а типът е не на нея, а на стойността, която се намира в нея. Това се нарича динамична типизация. По-подробно за това какво е променлива се разказва по-долу.
int age = 30; # Javaage = 30 # Python
var name = "Ann"; # Javaname = "Ann" # Python
# Думата final я няма. ГЛАВНИ_БУКВИ означава „не променяйте“, но това е конвенция.final int MAX_SIZE = 100; # JavaMAX_SIZE = 100 # Python
# В Java това е грешка при компилация, в Python сега в age има низ.age = "thirty" # Python
# Присвояване на няколко стойности наведнъж.int a = 1, b = 2; # Javaa, b = 1, 2 # Python
# Размяна: в Java е нужна временна променлива.int tmp = a; a = b; b = tmp; # Javaa, b = b, a # Python
a + " " + b # Java: 2 1f"{a} {b}" # Python: 2 1Ако все пак искате да посочите тип, за това има подсказка за типа. Python не я проверява, но я проверяват IDE и отделният инструмент mypy.
int count = 0; # Javacount: int = 0 # Python
String title = "Report"; # Javatitle: str = "Report" # Python
# Python не възразява: подсказката е само бележка за инструментите.count = "many" # PythonКак е прието да се именува
Правилата са описани в документа PEP 8, това е аналог на Java Code Conventions. Основната разлика е, че думите в имената на променливи и функции се разделят с долна черта.
| Какво | Java | Python |
|---|---|---|
| Променлива | userName | user_name |
| Метод, функция | findUser() | find_user() |
| Клас | OrderService | OrderService |
| Константа | MAX_SIZE | MAX_SIZE |
| Пакет | com.shop.orders | orders |
| Файл | OrderService.java | order_service.py |
| Скрито поле | private int size | _size |
| Отстъп | 4 интервала | 4 интервала |
Кои са базовите типове
Примитиви в Python няма, всичко е обект, дори числото. Затова няма и двойка int и Integer. Различни цели типове също няма, един int съхранява число с произволен размер.
| Java | Python | Пример |
|---|---|---|
byte, short, int, long, BigInteger | int | 42 |
float, double | float | 3.14 |
boolean | bool | True, False |
char, String | str | "a", "Ann" |
null | None | None |
byte[] | bytes | b"abc" |
Object | object | object() |
BigDecimal | decimal.Decimal | Decimal("9.99") |
Обърнете внимание: True, False и None се пишат с главна буква. Отделен тип за символ няма, символът е низ с дължина 1.
int number = 42; # Javanumber = 42 # Python
Object text = "Ann"; # Javatext = "Ann" # Python
# Да разберем класа на стойността. При числото в Python също има такъв.text.getClass().getSimpleName() # Java: Stringtype(text).__name__ # Python: strtype(number).__name__ # Python: int
# Да проверим типа.text instanceof String # Java: trueisinstance(text, str) # Python: True
# Числото има методове, скоби са нужни само при литерала.Integer.bitCount(255) # Java: 8(255).bit_count() # Python: 8Как да преобразуваме един тип в друг
Преобразуване в скоби няма. Вместо него класът на нужния тип се извиква като функция.
(int) 3.7 # Java: 3int(3.7) # Python: 3
(double) 3 # Java: 3.0float(3) # Python: 3.0
Integer.parseInt("42") # Java: 42int("42") # Python: 42
Double.parseDouble("3.5") # Java: 3.5float("3.5") # Python: 3.5
String.valueOf(42) # Java: 42str(42) # Python: 42
# Символ и неговият код.(int) 'A' # Java: 65ord("A") # Python: 65
(char) 65 # Java: Achr(65) # Python: A
# Капан: в Python непразният низ винаги е истина, дори "false".Boolean.parseBoolean("false") # Java: falsebool("false") # Python: TrueНизът и числото сами не се слепват. Там, където Java мълчаливо ще извика String.valueOf, Python ще изведе грешка.
int age = 30; # Javaage = 30 # Python
"Age: " + age # Java: Age: 30
# В Python "Age: " + age ще даде TypeError. За f-низовете се разказва по-долу."Age: " + str(age) # Python: Age: 30f"Age: {age}" # Python: Age: 30Кои са аритметичните оператори
Най-честата грешка тук е делението. В Python / винаги дава дробно число, а целочисленото деление се записва като //. Оператори ++ и -- няма.
7 / 2 # Java: 37 // 2 # Python: 3
7.0 / 2 # Java: 3.57 / 2 # Python: 3.5
7 % 2 # Java: 17 % 2 # Python: 1
# В Python за степен има оператор, а резултатът остава цял.Math.pow(2, 10) # Java: 1024.02 ** 10 # Python: 1024
int counter = 0; # Javacounter = 0 # Python
# counter++ в Python няма да работи.counter++; # Javacounter += 1 # PythonКак да сравняваме стойности
Тук в Python всичко е наобратно. == сравнява съдържанието, като equals в Java. А is проверява дали това е един и същ обект, като == за референции в Java.
var first = new ArrayList<>(List.of(1, 2)); # Javafirst = [1, 2] # Python
var second = new ArrayList<>(List.of(1, 2)); # Javasecond = [1, 2] # Python
# Равно ли е съдържанието.first.equals(second) # Java: truefirst == second # Python: True
# Един и същ ли е обектът.first == second # Java: falsefirst is second # Python: False
# Празна стойност. None се проверява само чрез is.String name = null; # Javaname = None # Python
name == null # Java: truename is None # Python: True
name != null # Java: falsename is not None # Python: False
# Сравненията могат да се пишат във верига, както в математиката.int age = 30; # Javaage = 30 # Python
age >= 18 && age < 65 # Java: true18 <= age < 65 # Python: TrueКак да запишем логическите условия
Вместо &&, || и ! се използват думи. Освен това за условие може да служи всяка стойност, а не само boolean: празните колекции, празният низ, нулата и None се смятат за лъжа.
boolean active = true; # Javaactive = True # Python
boolean admin = false; # Javaadmin = False # Python
active && !admin # Java: trueactive and not admin # Python: True
active || admin # Java: trueactive or admin # Python: Trueactive = Trueadmin = Falseprint(active and not admin) # Trueprint(active or admin) # True
# Обичайният начин да проверим, че колекцията е празна.items = []if not items: print("no items") # no items
# Едно условие замества проверката и за None, и за празнота.name = Noneif name: print(name)and и or връщат не True или False, а един от своите операнди. На това се основава краткият запис на стойност по подразбиране.
String name = ""; # Javaname = "" # Python
# or ще върне първата истинна стойност, тук това е "guest".name == null || name.isEmpty() ? "guest" : name # Java: guestname or "guest" # Python: guest
# Внимавайте: 0 също е лъжа и истинската стойност ще бъде загубена.0 or 100 # Python: 100Как да запишем условие и тернарен оператор
else if се съкращава до elif. Тернарният оператор се чете като обикновена фраза: първо стойността, после условието.
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); // passscore = 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 не е нужен.
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)); // RedirectSystem.out.println(httpName(500)); // Unknowndef 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)) # Redirectprint(http_name(500)) # UnknownСъпоставянето по тип работи така, както pattern matching в Java 21, само че образецът умее и да разбива стойността на части. Примери с dataclass и речници ще има по-нататък.
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 numberSystem.out.println(describe(List.of(1, 2))); // list of 2def 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 numberprint(describe([1, 2])) # list of 2
# А така образецът разбива стойността: списък точно от два елемента.match ["move", 10]: case ["move", distance]: print(distance) # 10Как да напишем цикъл for
В Python има само един вид for, това е аналог на for (x : items) от Java. Там, където в Java е нужен брояч, се използва range: функция, която извежда числата по ред.
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}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)) се смята за лош стил.
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}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 няма, а етикети за излизане от вложени цикли също няма.
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); // 3n = 0while n < 10: n += 1 if n % 2 == 0: continue if n > 5: break print(n) # 1 3 5
# do-while се записва като вечен цикъл с проверка накрая.tries = 0while True: tries += 1 if tries >= 3: breakprint(tries) # 3Цикълът може да има клон else, който в Java го няма. Той се изпълнява, ако цикълът е стигнал до края и не е излязъл чрез break. Обикновено това замества флага found.
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}ids = [1, 2, 3]
for id_ in ids: if id_ == 7: breakelse:
# Тук стигаме само ако break не е сработил. print("not found") # not foundОператорът := присвоява стойност направо вътре в израза. В Java присвояването и без това е израз, а в Python за това е нужен отделен оператор.
var names = List.of("Ann", "Bob");int count;
if ((count = names.size()) > 1) { System.out.println(count); // 2}names = ["Ann", "Bob"]
# Изброихме и веднага проверихме. Обикновеното = тук би дало SyntaxError.if (count := len(names)) > 1: print(count) # 2Как да декларираме функция
В Java методът винаги е в клас. В Python функцията може да е направо във файла, затова класове със статични методи тук не са нужни. Типът на резултата и на параметрите може да се посочи с подсказки, но това не е задължително.
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)); // 5greet("Ann");def add(a, b): return a + b
# Същото с подсказки за типове. None тук означава void.def greet(name: str) -> None: print(f"Hi, {name}") # Hi, Ann
print(add(2, 3)) # 5greet("Ann")
# Функция без return всъщност връща None.print(greet("Ann")) # NoneС какво да заменим претоварването на методи
Претоварване в Python няма: вторият def със същото име просто ще замени първия. Вместо него се използват стойности на параметрите по подразбиране. При извикване аргументите могат да се назовават по име, което замества и шаблона Builder.
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 falseSystem.out.println(connect("a", 443, true)); // a:443 truedef 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 събира именуваните аргументи в речник.
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# В 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).
def send(message, *, retry=False): return f"{message} {retry}"
print(send("hi", retry=True)) # hi True
# TypeError: така не може да се подава.# send("hi", True)Как да върнем няколко стойности
В Java за това е нужен отделен клас или record. В Python функцията просто връща няколко стойности със запетая и те се пакетират в кортеж.
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 3def 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() функцията просто се извиква. Ламбдата може да съдържа само един израз.
Function<Integer, Integer> twice = x -> x * 2; # Javatwice = lambda x: x * 2 # Python
twice.apply(4) # Java: 8twice(4) # Python: 8
# Референцията към метод е просто името на функцията без скоби.Function<String, Integer> size = String::length; # Javasize = len # Python
size.apply("Ann") # Java: 3size("Ann") # Python: 3
# Няколко параметъра.BinaryOperator<Integer> plus = (a, b) -> a + b; # Javaplus = lambda a, b: a + b # Python
plus.apply(2, 3) # Java: 5plus(2, 3) # Python: 5Най-честото приложение на ламбдата е ключ за сортиране. Вместо Comparator, който сравнява два обекта, в Python се подава функция, която по един обект връща онова, по което да се сортира.
var names = new ArrayList<>(List.of("Bob", "Al", "Cindy")); # Javanames = ["Bob", "Al", "Cindy"] # Python
names.sort(Comparator.comparingInt(String::length)); # Javanames.sort(key=len) # Python
names # Java: [Al, Bob, Cindy]names # Python: ['Al', 'Bob', 'Cindy']
names.sort(Comparator.comparing(s -> s.charAt(s.length() - 1))); # Javanames.sort(key=lambda s: s[-1]) # Python
names # Java: [Bob, Al, Cindy]names # Python: ['Bob', 'Al', 'Cindy']Как да декларираме клас
Конструкторът се нарича __init__. Полетата не се декларират предварително: те се появяват, когато им се присвои стойност в конструктора. Думата new я няма, обектът се създава чрез извикване на класа. Първият параметър на всеки метод е self. Това е същото като this в 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, Annclass 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. Как работи, се разказва по-долу.
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); // USDSystem.out.println(Money.isValid(-1)); // falseSystem.out.println(Money.ofDollars(2).cents); // 200class 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) # USDprint(Money.is_valid(-1)) # Falseprint(Money.of_dollars(2).cents) # 200Как да направим поле скрито и какво има вместо гетери
Модификатори за достъп в Python няма. Една долна черта в началото на името означава „това е вътрешно, отвън не пипайте“, но забрана няма, това е конвенция.
| Java | Python | Какво означава |
|---|---|---|
public int size | size | публично |
protected int size | _size | вътрешно, по конвенция |
private int size | _size | същото, отделно ниво няма |
Гетери и сетери предварително не се пишат, полето просто се прави публично. Ако по-късно е нужна проверка, полето се заменя с @property: отвън кодът остава същият, account.balance, но се извиква метод.
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()); // 100class 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 = 100print(account.balance) # 100С какво да заменим toString, equals и hashCode
Това също са dunder методи. Има и разлика от Java: низовите представяния са две. __str__ е за потребителя, използва го print. __repr__ е за разработчика, показват го дебъгерът и контейнерите. Ако е декларирано само __repr__, то работи и вместо __str__, затова обикновено пишат него.
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))); // trueclass 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__.
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()); // 1System.out.println(point.withX(5)); // Point[x=5, y=2]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: всеки обект получава собствен списък.@dataclassclass Cart: items: list = field(default_factory=list)
print(Cart()) # Cart(items=[])Как да наследим клас
Родителят се посочва в скоби. Извикването на родителския конструктор не е задължително и не става само, затова super().__init__() лесно се забравя. Всички методи могат да се предефинират, думата @Override я няма. Родителски класове може да има няколко, за това по-долу.
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: woofSystem.out.println(pet instanceof Animal); // trueclass 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: woofprint(isinstance(pet, Animal)) # TrueС какво да заменим абстрактния клас и интерфейса
Абстрактният клас се декларира чрез ABC. Не може да се създаде негов екземпляр, а наследникът е длъжен да реализира абстрактните методи. Разликата от 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.0from 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.
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: downfrom 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: неговите елементи са самите низове.
enum Status { NEW("new"), PAID("paid");
final String code;
Status(String code) { this.code = code; }}
System.out.println(Status.PAID.name()); // PAIDSystem.out.println(Status.PAID.code); // paidSystem.out.println(Status.valueOf("NEW").code); // newSystem.out.println(Status.values().length); // 2from enum import Enum, StrEnum
class Status(Enum): NEW = "new" PAID = "paid"
print(Status.PAID.name) # PAIDprint(Status.PAID.value) # paid
# Търсене по име и по стойност.print(Status["NEW"].value) # newprint(Status("paid").name) # PAIDprint(len(Status)) # 2print(Status.PAID is Status("paid")) # True
# Елементът на StrEnum е равен на низ, това е удобно за JSON и база данни.class Color(StrEnum): RED = "red"
print(Color.RED == "red") # TrueС какво да заменим анонимните и вложените класове
Анонимни класове няма и обикновено не са нужни, защото може да се подаде функция. Вложен клас има, но прилича на static class в Java: достъп до обекта на външния клас той няма.
Runnable task = new Runnable() { @Override public void run() { System.out.println("run"); // run }};task.run();def run_task(task): task()
# Вместо анонимен клас – функция, декларирана там, където е нужна.def task(): print("run") # run
run_task(task)Как да напишем генерик
Подсказката за типа на колекцията се пише в квадратни скоби. Собствените генерични класове от Python 3.12 се декларират както в Java, само че скобите са други. Както в Java, по време на работа от параметъра на типа не остава нищо. По-подробно в раздела за подсказките за типове.
List<String> names = new ArrayList<>(); # Javanames: list[str] = [] # Python
Map<String, Integer> ages = new HashMap<>(); # Javaages: dict[str, int] = {} # Python
# Стойност или null.String nickname = null; # Javanickname: str | None = None # Pythonclass 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()); // xSystem.out.println(maxOf(1, 2)); // 2class 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()) # xprint(max_of(1, 2)) # 2Как да обработваме изключения
catch се нарича except, а throw се нарича raise. Проверявани изключения няма: думата throws не съществува и да се прихваща нещо не е задължително.
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");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 не е имало грешка.
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}class OrderError(Exception): pass
try: try: int("abc") except ValueError as e: raise OrderError("bad id") from eexcept OrderError as e: print(e) # bad id
# __cause__ е същото като getCause(). print(isinstance(e.__cause__, ValueError)) # True
try: value = int("42")except ValueError: print("bad")else:
# Само ако не е имало грешка. print(value) # 42Имената на стандартните изключения са други, но съответствието е просто:
| Java | Python |
|---|---|
Throwable | BaseException |
Exception | Exception |
IllegalArgumentException, NumberFormatException | ValueError |
ClassCastException | TypeError |
NullPointerException | AttributeError или TypeError |
IndexOutOfBoundsException | IndexError |
няма ключ в Map | KeyError |
IOException | OSError |
FileNotFoundException | FileNotFoundError |
ArithmeticException | ZeroDivisionError |
UnsupportedOperationException | NotImplementedError |
StackOverflowError | RecursionError |
TimeoutException | TimeoutError |
С какво да заменим try-with-resources
Вместо него има оператор with. Обектът за него се нарича контекстен мениджър. Вместо метода close от AutoCloseable той има два метода: __enter__ се извиква при влизане, а __exit__ при излизане, дори при грешка. Редът на изпълнение е показан с цифри.
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}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 се среща при работа с файлове:
Path path = Files.createTempFile("demo", ".txt");
try (var writer = Files.newBufferedWriter(path)) { writer.write("hello");}
System.out.println(Files.readString(path)); // helloimport tempfilefrom 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 няма: файлът сам е модул, а името му е равно на името на файла. Каталог с модули се нарича пакет. В един файл може да се сложат колкото си искате класове и функции, а отделен файл за всеки клас тук не е прието. Може да се импортира както самият модул, така и отделно име от него.
# Импорт на едно име.import java.time.LocalDate; # Javafrom datetime import date # Python
# Статичен импорт.import static java.lang.Math.sqrt; # Javafrom math import sqrt # Python
# Импорт на всичко накуп. В Python така не се прави.import java.util.*; # Javafrom math import * # Python
# Само в Python: целият модул, а после обръщение през неговото име.import math # Pythonmath.floor(2.5) # Python: 2
# Само в Python: собствено кратко име.import datetime as dt # Pythondt.date(2026, 1, 2).year # Python: 2026orders/ пакет, като package orders├── __init__.py признак за пакет, обикновено празен├── domain.py модул orders.domain└── api/ ├── __init__.py └── routes.py модул orders.api.routes# От друг файл на проекта.from orders.domain import Order
# От съседен файл на същия пакет: точката означава „този пакет“.from .domain import Order
# Едно ниво нагоре.from ..domain import OrderБърза справка по синтаксиса
| Java | Python |
|---|---|
int a = 1; | a = 1 |
final int MAX = 1; | MAX = 1 |
null | None |
true, false | True, False |
a.equals(b) | a == b |
a == b за референции | a is b |
&&, ||, ! | and, or, not |
x > 0 ? a : b | a if x > 0 else b |
7 / 2 | 7 // 2 |
Math.pow(2, 3) | 2 ** 3 |
i++ | i += 1 |
else if | elif |
switch | match |
for (int i = 0; i < n; i++) | for i in range(n): |
for (T x : items) | for x in items: |
do { } while (x); | while True: и break |
void f(int x) | def f(x): |
int... xs | *xs |
x -> x * 2 | lambda x: x * 2 |
String::length | len |
new User("Ann") | User("Ann") |
this | self |
class B extends A | class B(A): |
super.run() | super().run() |
static int f() | @staticmethod |
record Point(int x) | @dataclass(frozen=True) |
interface | Protocol |
abstract class | class A(ABC): |
List<String> | list[str] |
throw | raise |
catch (E e) | except E as e: |
try (var r = ...) | with ... as r: |
toString() | __repr__, __str__ |
equals(), hashCode() | __eq__, __hash__ |
System.out.println(x) | print(x) |
Числа: основни операции
С какво целите числа се различават от Java
int в Python не прелива, той расте, докато стига паметта. Делението с отрицателно число също работи различно: Java отрязва дробната част, а Python закръгля надолу.
Integer.MAX_VALUE + 1 # Java: -21474836482147483647 + 1 # Python: 2147483648
BigInteger.TWO.pow(70) # Java: 11805916207174113034242 ** 70 # Python: 1180591620717411303424
# Деление с отрицателно число: резултатите са различни.-7 / 2 # Java: -3-7 // 2 # Python: -4
-7 % 3 # Java: -1-7 % 3 # Python: 2
# Долни черти в числата, двоичен и шестнадесетичен запис – както в Java.1_000_000 # Java: 10000001_000_000 # Python: 1000000
0xFF # Java: 2550xFF # Python: 255
0b101 # Java: 50b101 # Python: 5
# Превръщане в друга бройна система. Python добавя префикс.Integer.toBinaryString(10) # Java: 1010bin(10) # Python: 0b1010
Integer.toHexString(255) # Java: ffhex(255) # Python: 0xff
Integer.parseInt("ff", 16) # Java: 255int("ff", 16) # Python: 255
# Битовите операции са същите, само че >>> няма: числото няма фиксирана дължина.5 & 3 # Java: 15 & 3 # Python: 1
1 << 4 # Java: 161 << 4 # Python: 16Къде да търсим Math
Най-честите функции са вградени в езика и са достъпни без импорт, а останалите са в модула math.
import math # Python
Math.abs(-5) # Java: 5abs(-5) # Python: 5
Math.max(1, 2) # Java: 2max(1, 2) # Python: 2
# min и max в Python приемат и няколко стойности, и списък.Collections.min(List.of(3, 1)) # Java: 1min(3, 1) # Python: 1
# floor и ceil в Python връщат цяло число, а не дробно.Math.floor(2.7) # Java: 2.0math.floor(2.7) # Python: 2
Math.ceil(2.1) # Java: 3.0math.ceil(2.1) # Python: 3
Math.sqrt(16) # Java: 4.0math.sqrt(16) # Python: 4.0
Math.PI # Java: 3.141592653589793math.pi # Python: 3.141592653589793
# Капан: round в Python закръгля половината към четното.Math.round(2.5) # Java: 3round(2.5) # Python: 2
Math.round(3.5) # Java: 4round(3.5) # Python: 4
# Закръгляне до два знака.Math.round(3.14159 * 100) / 100.0 # Java: 3.14round(3.14159, 2) # Python: 3.14
# Частно и остатък наведнъж.divmod(7, 2) # Python: (3, 1)
Double.isNaN(0.0 / 0) # Java: truemath.isnan(float("nan")) # Python: True
# В Java делението на дробно число на нула дава Infinity, в Python – ZeroDivisionError.1.0 / 0 # Java: Infinityfloat("inf") # Python: infКак да смятаме пари
float в Python е същият double, със същите грешки при закръгляне. За пари има Decimal, аналог на BigDecimal. Създава се от низ. Обикновените оператори работят, затова add и multiply не са нужни.
from decimal import Decimal, ROUND_HALF_UP # Python
0.1 + 0.2 # Java: 0.300000000000000040.1 + 0.2 # Python: 0.30000000000000004
new BigDecimal("0.1").add(new BigDecimal("0.2")) # Java: 0.3Decimal("0.1") + Decimal("0.2") # Python: 0.3
new BigDecimal("19.99").multiply(new BigDecimal("3")) # Java: 59.97Decimal("19.99") * 3 # Python: 59.97
# Закръгляне до стотинки.new BigDecimal("2.675").setScale(2, RoundingMode.HALF_UP) # Java: 2.68Decimal("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: falseDecimal("1.0") == Decimal("1.00") # Python: TrueКак да получим случайно число
Обърнете внимание: при randint и двете граници са включени. Модулът random не е подходящ за пароли и токени, за тях има secrets, като SecureRandom в Java.
import random # Pythonimport secrets # Python
# Зар: от 1 до 6.int dice = ThreadLocalRandom.current().nextInt(1, 7); # Javadice = random.randint(1, 6) # Python
# Дробно число от 0 до 1.double share = Math.random(); # Javashare = random.random() # Python
# Случаен елемент и разбъркване.var cards = new ArrayList<>(List.of(1, 2, 3)); # Javacards = [1, 2, 3] # Python
Collections.shuffle(cards); # Javarandom.shuffle(cards) # Python
int card = cards.get(new Random().nextInt(cards.size())); # Javacard = random.choice(cards) # Python
# Токен за сигурност.var token = new java.security.SecureRandom().nextLong(); # Javatoken = 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".
"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.
String query = """ SELECT * FROM users """;System.out.print(query);from textwrap import dedent
# Обратната наклонена черта след кавичките премахва първия нов ред.query = dedent("""\ SELECT * FROM users """)print(query)При съседните литерали има капан. Python ги слепва без никакъв плюс. Това е удобно за дълги низове, но ако забравите запетая в списък, грешка няма да има.
message = ( "first part, " "second part")print(message) # first part, second part
# Забравена запетая: в списъка има два елемента, а не три.names = [ "Ann", "Bob" "Cid",]print(names) # ['Ann', 'BobCid']Какви методи има низът
Тук са събрани методите, от които имате нужда всеки ден. Низът е неизменяем, затова всички те, както в Java, връщат нов низ.
String s = "Hello World"; # Javas = "Hello World" # Python
# Дължината е функция, а не метод.s.length() # Java: 11len(s) # Python: 11
s.charAt(0) # Java: Hs[0] # Python: H
# Отрицателният индекс брои от края.s.charAt(s.length() - 1) # Java: ds[-1] # Python: d
s.toUpperCase() # Java: HELLO WORLDs.upper() # Python: HELLO WORLD
s.toLowerCase() # Java: hello worlds.lower() # Python: hello world
s.contains("World") # Java: true"World" in s # Python: True
s.startsWith("He") # Java: trues.startswith("He") # Python: True
s.endsWith("ld") # Java: trues.endswith("ld") # Python: True
s.indexOf("o") # Java: 4s.find("o") # Python: 4
s.lastIndexOf("o") # Java: 7s.rfind("o") # Python: 7
# Не е намерено: -1. Методът s.index() в този случай ще даде ValueError.s.indexOf("z") # Java: -1s.find("z") # Python: -1
s.replace("World", "Ann") # Java: Hello Anns.replace("World", "Ann") # Python: Hello Ann
# Да заменим само първото срещане.s.replaceFirst("l", "L") # Java: HeLlo Worlds.replace("l", "L", 1) # Python: HeLlo World
# Колко пъти се среща. Готов метод в Java няма.s.chars().filter(c -> c == 'l').count() # Java: 3s.count("l") # Python: 3# Интервали по краищата." 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: truenot "" # Python: True
" ".isBlank() # Java: truenot " ".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 -1String s = "Hello"; # Javas = "Hello" # Python
s.substring(1, 3) # Java: els[1:3] # Python: el
# Без начало: от първия символ.s.substring(0, 2) # Java: Hes[:2] # Python: He
# Без край: до последния символ.s.substring(2) # Java: llos[2:] # Python: llo
# Последните три символа.s.substring(s.length() - 3) # Java: llos[-3:] # Python: llo
# Всеки втори символ. В Java това се прави с цикъл.s[::2] # Python: Hlo
# За разлика от substring, излизането извън границите не дава грешка.s[3:100] # Python: loКак да разделим и слепим низ
Тук разликите са най-много. split в Java приема регулярен израз и изхвърля празните низове в края, а split в Python нищо не изхвърля и търси обикновен текст. Без аргументи той разделя по произволни интервали. join в 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: 3list("abc") # Python: ['a', 'b', 'c']Как да подставим стойности в низ
Основният начин е f-низът: буквата f пред кавичката и произволни изрази във фигурни скоби. След двоеточието идва форматът, много подобен на този в String.format.
String name = "Ann"; # Javaname = "Ann" # Python
int n = 42; # Javan = 42 # Python
double x = 3.14159; # Javax = 3.14159 # Python
String.format("%s is %d", name, n) # Java: Ann is 42f"{name} is {n}" # Python: Ann is 42
# В скобите може да се пише произволен израз.String.format("%d", n * 2) # Java: 84f"{n * 2}" # Python: 84
# Знаци след десетичната запетая.String.format(Locale.US, "%.2f", x) # Java: 3.14f"{x:.2f}" # Python: 3.14
# Ширина и водещи нули.String.format("%05d", n) # Java: 00042f"{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,567f"{1234567:,}" # Python: 1,234,567
# Шестнадесетичен запис и проценти.String.format("%x", 255) # Java: fff"{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-низове в извикванията на логъра не се използват.
name = "Ann"
# Като printf. Най-старият начин.print("Hi, %s" % name) # Hi, Ann
# Като MessageFormat. Нужен е, когато шаблонът е зададен предварително, например в настройките.print("Hi, {}".format(name)) # Hi, Annprint("Hi, {who}".format(who=name)) # Hi, AnnКакво е t-низ
f-низът веднага слепва всичко в обикновен текст, затова да се подставят в него данни от потребителя в SQL или HTML е опасно: така се появяват инжекции. t-низът се появи в Python 3.14. Той не слепва нищо, а връща текста и стойностите поотделно, а вие сами решавате как да ги свържете.
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.
var builder = new StringBuilder();for (int i = 0; i < 3; i++) { builder.append(i).append(",");}System.out.println(builder); // 0,1,2,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, затова в списъка низовете се виждат с кавички.
text = "a\tb"
print(str(text)) # a bprint(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 |
import base64 # Python
byte[] data = "héllo".getBytes(StandardCharsets.UTF_8); # Javadata = "héllo".encode("utf-8") # Python
new String(data, StandardCharsets.UTF_8) # Java: héllodata.decode("utf-8") # Python: héllo
# Дължина в байтове: é заема два.data.length # Java: 6len(data) # Python: 6
# Байт: в Java е със знак, а в Python е от 0 до 255.data[1] # Java: -61data[1] # Python: 195
HexFormat.of().formatHex(data) # Java: 68c3a96c6c6fdata.hex() # Python: 68c3a96c6c6f
Base64.getEncoder().encodeToString("hi".getBytes()) # Java: aGk=base64.b64encode(b"hi").decode() # Python: aGk=data = b"abc"
# Отпечатва се с буквата b, за да не се бъркат текстът и байтовете.print(data) # b'abc'
# "abc" + b"abc" ще даде TypeError.
# bytes не може да се променя, bytearray може.buffer = bytearray(b"abc")buffer[0] = 65print(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 и преди сравнение такъв текст се нормализира.
import unicodedata # Python
"😀".length() # Java: 2len("😀") # Python: 1
"😀".codePointCount(0, 2) # Java: 1len("😀") # Python: 1
# é като един символ и като e плюс знак за ударение."é".equals("é") # Java: false"é" == "é" # Python: False
Normalizer.normalize("é", Normalizer.Form.NFC).equals("é") # Java: trueunicodedata.normalize("NFC", "é") == "é" # Python: TrueКак да работим с регулярни изрази
Синтаксисът на изразите е почти същият, а методите са в модула re. Шаблонът е прието да се пише със суров низ. Ако няма съвпадение, методите връщат None.
import re # Python
# Подхожда ли целият низ."123".matches("\\d+") # Java: truebool(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(); # Javafound.map(r -> r.group()).toList() # Java: [1, 2]re.findall(r"\d", "a1b2") # Python: ['1', '2']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}import re
# Предварително компилиран шаблон, като Pattern. Име на групата: ?P<име>.pattern = re.compile(r"(?P<key>\w+)=(?P<value>\d+)")
# search търси на произволно място в низа, като find().if match := pattern.search("size=42"): print(match["key"]) # size print(match["value"]) # 42Колекции: основни методи
Кои колекции на кои съответстват
Основните колекции са вградени в езика и имат собствен синтаксис, затова new и импорти за тях не са нужни. Интерфейс List с различни реализации също няма: има един list и вътре той е динамичен масив, като ArrayList.
| Java | Python | Запис |
|---|---|---|
ArrayList | list | [1, 2] |
List.of | tuple | (1, 2) |
LinkedHashMap | dict | {"a": 1} |
HashSet | set | {1, 2} |
Set.of | frozenset | frozenset({1, 2}) |
ArrayDeque, LinkedList | collections.deque | deque([1, 2]) |
PriorityQueue | heapq | функции върху списък |
int[] | list или array.array | [0] * 3 |
TreeMap | няма, обикновено sorted(d) | |
ConcurrentHashMap | dict и Lock |
Как да работим със списък
var items = new ArrayList<>(List.of(3, 1, 2)); # Javaitems = [3, 1, 2] # Python
items.size() # Java: 3len(items) # Python: 3
items.get(0) # Java: 3items[0] # Python: 3
# Последният елемент.items.getLast() # Java: 2items[-1] # Python: 2
items.set(0, 9); # Javaitems[0] = 9 # Python
items.add(4); # Javaitems.append(4) # Python
# Вмъкване по индекс.items.add(0, 7); # Javaitems.insert(0, 7) # Python
items.addAll(List.of(5, 6)); # Javaitems.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: 7items.pop(0) # Python: 7
# Премахване по стойност.items.remove(Integer.valueOf(9)); # Javaitems.remove(9) # Python
items.contains(1) # Java: true1 in items # Python: True
# Ако стойността я няма, index дава ValueError, а не -1.items.indexOf(2) # Java: 1items.index(2) # Python: 1
items.isEmpty() # Java: falsenot items # Python: False
Collections.max(items) # Java: 6max(items) # Python: 6
items.stream().mapToInt(i -> i).sum() # Java: 18sum(items) # Python: 18
# Обръщане на място.Collections.reverse(items); # Javaitems.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); # Javacopy = items.copy() # Python
# Слепване и повторение.[1] + [2] # Python: [1, 2][0] * 3 # Python: [0, 0, 0]
items.clear(); # Javaitems.clear() # 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] * 2grid[0][0] = 1print(grid) # [[1, 0], [1, 0]]
# Правилно: всеки вътрешен списък се създава отделно.grid = [[0] * 2 for _ in range(2)]grid[0][0] = 1print(grid) # [[1, 0], [0, 0]]Какво е кортеж
tuple е неизменяем списък, най-близкият аналог на List.of. Използва се за фиксиран набор от стойности, като координати или ред от база данни. А освен това може да служи за ключ на речник, а списъкът не може.
var point = List.of(10, 20); # Javapoint = (10, 20) # Python
point.get(0) # Java: 10point[0] # Python: 10
# point.set(0, 1) в Java и point[0] = 1 в Python ще дадат грешка.
# Кортеж от един елемент: запетаята е задължителна.var single = List.of(1); # Javasingle = (1,) # Python
# Без запетая това е просто число в скоби.type((1)).__name__ # Python: inttype((1,)).__name__ # Python: tuple
# Разпределяне по променливи.var x = point.get(0); # Javax, y = point # Python
x # Java: 10y # Python: 20
# Кортежите се сравняват поелементно.(1, 2) < (1, 3) # Python: TrueКак да работим с речник
dict е Map. Той пази реда на добавяне, като LinkedHashMap. Най-важната разлика от Java: ages["Bob"], когато ключът липсва, дава грешка KeyError, а не null. Поведението като при get в Java го дава методът get.
var ages = new LinkedHashMap<String, Integer>(); # Javaages = {} # Python
ages.put("Ann", 30); # Javaages["Ann"] = 30 # Python
ages.put("Bob", 25); # Javaages["Bob"] = 25 # Python
ages.get("Ann") # Java: 30ages["Ann"] # Python: 30
# Ключът го няма.ages.get("Cid") # Java: nullages.get("Cid") # Python: None
ages.getOrDefault("Cid", 0) # Java: 0ages.get("Cid", 0) # Python: 0
ages.containsKey("Ann") # Java: true"Ann" in ages # Python: True
ages.containsValue(25) # Java: true25 in ages.values() # Python: True
ages.size() # Java: 2len(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); # Javaages.setdefault("Cid", 40) # Python: 40
# Премахване и получаване на стойността.ages.remove("Cid") # Java: 40ages.pop("Cid") # Python: 40
# Премахване, а ако ключът го няма, връща се стойност по подразбиране.ages.pop("Cid", None) # Python: None
# Ключове по ред, вместо TreeMap.new TreeMap<>(ages).firstKey() # Java: Annsorted(ages)[0] # Python: AnnОбхождането се записва чрез items, той връща двойки ключ и стойност. Замества entrySet.
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));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 брои колко пъти се среща всеки елемент.
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}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 има обикновени оператори. Те връщат ново множество и не променят изходните.
var seen = new TreeSet<>(List.of(1, 2)); # Javaseen = {1, 2} # Python
seen.add(3); # Javaseen.add(3) # Python
seen.contains(3) # Java: true3 in seen # Python: True
seen.remove(3); # Javaseen.remove(3) # Python
# В Python remove ще даде KeyError, ако стойността я няма. discard ще мълчи.seen.discard(100) # Python
# Празно множество: {} ще създаде речник.var empty = new HashSet<Integer>(); # Javaempty = 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. Те работят с обикновен списък, а най-отгоре винаги стои минималният елемент.
from collections import deque # Pythonimport heapq # Python
var queue = new ArrayDeque<>(List.of(1, 2)); # Javaqueue = deque([1, 2]) # Python
queue.addLast(3); # Javaqueue.append(3) # Python
queue.addFirst(0); # Javaqueue.appendleft(0) # Python
queue.pollFirst() # Java: 0queue.popleft() # Python: 0
queue.pollLast() # Java: 3queue.pop() # Python: 3
# Поглеждане, без да се премахва.queue.peekFirst() # Java: 1queue[0] # Python: 1
# Приоритетна опашка.var tasks = new PriorityQueue<>(List.of(3, 1)); # Javatasks = [1, 3] # Python
tasks.add(2); # Javaheapq.heappush(tasks, 2) # Python
tasks.poll() # Java: 1heapq.heappop(tasks) # Python: 1
# С приоритет: кортежът се сравнява по първия елемент.jobs = [(2, "later"), (1, "urgent")]; heapq.heapify(jobs) # Pythonheapq.heappop(jobs) # Python: (1, 'urgent')
# N най-големи наведнъж.heapq.nlargest(2, [1, 5, 3]) # Python: [5, 3]Как да сортираме
list.sort() променя самия списък, а sorted() връща нов и приема всяка колекция. Правилата за сортиране се задават от функцията key. За сортиране по няколко полета тя просто връща кортеж.
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()); // Annfrom dataclasses import dataclass
@dataclassclass 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 поведението е същото, а списъкът мълчаливо пропуска елементи.
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"] = 2except RuntimeError as e: print(e) # dictionary changed size during iterationКаква е сложността на операциите
Стойностите са същите като при съответните колекции в Java.
| Операция | list | dict, set | deque |
|---|---|---|---|
| Четене по индекс или ключ | O(1) | O(1) | O(n) |
x in c | O(n) | O(1) | O(n) |
| Добавяне в края | O(1) | O(1) | O(1) |
| Добавяне или премахване в началото | O(n) | – | O(1) |
sorted | O(n log n) | O(n log n) | O(n log n) |
Stream API: какво има вместо него
Какво е списъчен израз
Вместо верига от filter и map в Python има особен запис, който се нарича списъчен израз (list comprehension). Чете се като обикновена фраза: какво да поставим, откъде да вземем, при какво условие. Този запис замества по-голямата част от Stream API.
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 е записана с един израз, без отделни извиквания.
import functools # Python
var items = List.of(3, 1, 2, 3); # Javaitems = [3, 1, 2, 3] # Python
# filteritems.stream().filter(i -> i > 1).toList() # Java: [3, 2, 3][i for i in items if i > 1] # Python: [3, 2, 3]
# mapitems.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, maxitems.stream().mapToInt(i -> i).sum() # Java: 9sum(items) # Python: 9
items.stream().filter(i -> i > 1).count() # Java: 3sum(1 for i in items if i > 1) # Python: 3
items.stream().max(Integer::compare).get() # Java: 3max(items) # Python: 3
# Средна стойност.items.stream().mapToInt(i -> i).average().getAsDouble() # Java: 2.25sum(items) / len(items) # Python: 2.25
# anyMatch, allMatch, noneMatchitems.stream().anyMatch(i -> i > 2) # Java: trueany(i > 2 for i in items) # Python: True
items.stream().allMatch(i -> i > 2) # Java: falseall(i > 2 for i in items) # Python: False
items.stream().noneMatch(i -> i > 5) # Java: truenot any(i > 5 for i in items) # Python: True
# findFirstitems.stream().filter(i -> i < 3).findFirst().orElse(null) # Java: 1next((i for i in items if i < 3), None) # Python: 1
# sorted, limit, skipitems.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}
# flatMapStream.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]
# joiningvar words = items.stream().map(String::valueOf); # Javawords.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: 18functools.reduce(lambda a, b: a * b, items) # Python: 18Обли скоби вместо квадратни в изрази като sum(1 for i in items if i > 1) създават генератор. Той не изгражда списък в паметта, а произвежда стойностите една по една, като мързелив стрийм. По-подробно за това се разказва по-долу.
Как да съберем речник и да групираме стойности
Има изрази и за речник, и за множество. А при groupingBy прост аналог няма: групирането обикновено се пише с цикъл.
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()); // 2from collections import Counter, defaultdictfrom dataclasses import dataclass
@dataclassclass User: name: str city: str age: int
users = [ User("Ann", "Kyiv", 30), User("Bob", "Lviv", 25), User("Cid", "Kyiv", 40),]
# toMapages_by_name = {u.name: u.age for u in users}print(ages_by_name["Ann"]) # 30
# groupingByby_city = defaultdict(list)for user in users: by_city[user.city].append(user)print(len(by_city["Kyiv"])) # 2
# groupingBy + countingcount_by_city = Counter(u.city for u in users)print(count_by_city["Kyiv"]) # 2
# partitioningByadults = [u for u in users if u.age >= 30]print(len(adults)) # 2С какво да заменим Optional
Optional в Python не се използва. Липсата на стойност е None, а типът се записва като User | None. Веригите се заменят с обикновени проверки.
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); // guestfrom dataclasses import dataclass
@dataclassclass 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).
from datetime import UTC, date, datetime, timedelta # Pythonfrom zoneinfo import ZoneInfo # Python
import java.time.format.DateTimeFormatter; # Javaimport java.time.*; # Java
var day = LocalDate.of(2026, 11, 6); # Javaday = date(2026, 11, 6) # Python
# Събиране и изваждане: вместо plusDays – обикновени оператори.day.plusDays(30) # Java: 2026-12-06day + timedelta(days=30) # Python: 2026-12-06
day.getDayOfWeek().getValue() # Java: 5day.isoweekday() # Python: 5
# Разлика между дати.Duration.ofHours(36).toDays() # Java: 1timedelta(hours=36).days # Python: 1
# В низ и от низ.day.toString() # Java: 2026-11-06day.isoformat() # Python: 2026-11-06
LocalDate.parse("2026-01-02").getYear() # Java: 2026date.fromisoformat("2026-01-02").year # Python: 2026
day.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) # Java: 06.11.2026day.strftime("%d.%m.%Y") # Python: 06.11.2026
# Текущият момент с часова зона.var now = ZonedDateTime.now(ZoneOffset.UTC); # Javanow = datetime.now(UTC) # Python
var kyiv = ZoneId.of("Europe/Kyiv"); # Javavar at = ZonedDateTime.of(2026, 1, 2, 12, 0, 0, 0, kyiv); # Javaat = datetime(2026, 1, 2, 12, tzinfo=ZoneInfo("Europe/Kyiv")) # Python
at.toEpochSecond() # Java: 1767348000int(at.timestamp()) # Python: 1767348000Как да работим с файлове
Path и Files в Python са обединени в един клас pathlib.Path. Частите на пътя се съединяват с оператора /.
import tempfile # Pythonfrom pathlib import Path # Python
Path dir = Files.createTempDirectory("demo"); # Javadir = Path(tempfile.mkdtemp()) # Python
Path file = dir.resolve("logs").resolve("a.txt"); # Javafile = dir / "logs" / "a.txt" # Python
file.getFileName() # Java: a.txtfile.name # Python: a.txt
file.getParent().getFileName() # Java: logsfile.parent.name # Python: logs
Files.exists(file) # Java: falsefile.exists() # Python: False
# Създаване на каталози заедно с родителските.Files.createDirectories(file.getParent()); # Javafile.parent.mkdir(parents=True, exist_ok=True) # Python
Files.writeString(file, "one\ntwo\n"); # Javafile.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: 1sum(1 for f in dir.rglob("*.txt")) # Python: 1
Files.delete(file); # Javafile.unlink() # PythonГолемият файл се чете ред по ред. Обектът файл в Python може да се обхожда в цикъл, а в паметта тогава попада по един ред наведнъж.
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}import tempfilefrom 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.
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);import jsonfrom dataclasses import asdict, dataclass
@dataclassclass 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.
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);}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 / 0except ZeroDivisionError:
# exception записва съобщението и целия стек, като log.error("...", e). log.exception("payment failed")Къде да търсим останалото
import hashlib # Pythonimport os # Pythonimport sys # Pythonimport time # Pythonimport uuid # Python
# Променлива на средата или стойност по подразбиране.String port = System.getenv().getOrDefault("NO_SUCH_VAR", "80"); # Javaport = os.environ.get("NO_SUCH_VAR", "80") # Python
# Аргументи на командния ред. sys.argv[0] е името на файла.String[] args = new String[0]; # Javaargs = sys.argv[1:] # Python
# Измерване на време.long start = System.nanoTime(); # Javastart = time.perf_counter() # Python
Thread.sleep(1); # Javatime.sleep(0.001) # Python
# Уникален идентификатор.UUID.randomUUID().toString().length() # Java: 36len(str(uuid.uuid4())) # Python: 36
# SHA-256 хеш, първите 8 символа.var sha = java.security.MessageDigest.getInstance("SHA-256"); # JavaHexFormat.of().formatHex(sha.digest(new byte[0])).substring(0, 8) # Java: e3b0c442hashlib.sha256(b"").hexdigest()[:8] # Python: e3b0c442| Java | Python |
|---|---|
System.exit(1) | sys.exit(1) |
System.err.println(x) | print(x, file=sys.stderr) |
new Scanner(System.in) | input() |
HttpClient | urllib.request, на практика httpx |
URLEncoder.encode | urllib.parse.quote |
Properties | configparser, tomllib |
@Cacheable | functools.lru_cache |
Thread, ExecutorService | threading, concurrent.futures |
CompletableFuture | asyncio |
JUnit | unittest, на практика pytest |
С какво Python е устроен различно от Java
Как се изпълнява кодът
Основната реализация на езика се нарича CPython. Както javac и JVM, тя компилира изходния код в байткод и го изпълнява във виртуална машина, само че двете стъпки се правят с една команда. Байткодът се кешира в каталога __pycache__, като файловете .class. Основната разлика от JVM е, че байткодът обикновено се интерпретира. JIT компилаторът, който в JVM превръща горещия код в машинен, в CPython тепърва се появява и засега е експериментален. Оттук и разликата в скоростта.
| Java | Python | |
|---|---|---|
| Компилация | javac предварително | при стартиране |
| Грешката в типа се вижда | при компилация | при изпълнение на реда |
| JIT | C2, Graal | експериментален |
| Старт на програмата | по-бавен | по-бърз |
| Събиране на боклука | трасиращо | брояч на референции и събирач на цикли |
| Паралелни нишки | да | обикновено не, заради GIL |
Декларацията на клас или функция в Python е обикновена команда, която се изпълнява, когато до нея дойде редът. В Java класовете се зареждат от JVM, а тук файлът просто върви отгоре надолу. Цифрите показват реда.
print("start") # 1. start
# Тук функцията само се създава. Тялото не се изпълнява.def greet(): print("hi") # 4. hi
# А тялото на класа се изпълнява веднага.class Config: print("class body") # 2. class body
print("end") # 3. endgreet()Това обяснява много от следващото. Импортът на файл също просто го изпълнява отгоре надолу, затова страничният код на най-горното ниво на модула ще сработи при първия импорт. Затова е нужна и проверката 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]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]a = [1, 2]b = ab.append(3)print(a) # [1, 2, 3]
# Етикетът b е преместен върху друг обект. Обектът a не се е променил.b = [9]print(a) # [1, 2, 3]
# id() е адресът на обекта, като System.identityHashCode.c = aprint(id(c) == id(a)) # TrueКак се предават аргументи на функция
Както обектите в 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]def change(items):
# 1. Променяме общия обект: това се вижда отвън. items.append(1)
# 2. Преместваме локалния етикет: отвън не се вижда. items = [] items.append(99)
data = []change(data)print(data) # [1]Кои типове са изменяеми и кои не
| Неизменяеми | Изменяеми |
|---|---|
int, float, bool, None | list |
str, bytes | dict |
tuple, frozenset | set, bytearray |
@dataclass(frozen=True) | обикновени обекти |
Оттук идва важен капан с +=. При изменяем обект той променя самия обект, а при неизменяем създава нов и премества етикета.
# Списък: един обект за двамата, промяната се вижда през двата етикета.a = [1]b = aa += [2]print(b) # [1, 2]
# Низ: a вече сочи към нов низ, а b – към стария.a = "x"b = aa += "y"print(b) # xКак да направим копие на обект
list(x) или x.copy() е плитко копие, като new ArrayList<>(x): копира се самият списък, а вложените обекти остават общи. Модулът copy умее да копира цялото дърво от обекти. За това в Java би се наложило да се пише код или да се сериализира обектът.
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.
Integer small1 = 127, small2 = 127;System.out.println(small1 == small2); // true
Integer big1 = 1000, big2 = 1000;System.out.println(big1 == big2); // falseSystem.out.println(big1.equals(big2)); // truesmall1 = int("256")small2 = int("256")print(small1 is small2) # True
big1 = int("100000")big2 = int("100000")print(big1 is big2) # Falseprint(big1 == big2) # TrueКъде се вижда променливата
В Java променливата живее вътре в своя блок във фигурни скоби. В Python блокове на видимост няма: if и for не ги създават. Граници поставят само функциите, класовете и модулите. Променливата се търси по ред на четири места, този ред се нарича LEGB: Local (собствената функция), Enclosing (външната функция), Global (файлът), Built-in (вградените имена).
for (int i = 0; i < 3; i++) { int square = i * i;}
// Грешка при компилация: i и square тук не съществуват.// System.out.println(i);for i in range(3): square = i * i
# И двете променливи са живи: цикълът не създава собствена област.print(i, square) # 2 4
# Изключение: променлива вътре в списъчен израз отвън не се вижда.squares = [n * n for n in range(3)]Четенето на външна променлива работи само. А присвояването вътре във функция създава локална променлива. За да запишете във външна, трябва да го кажете с думата global или nonlocal.
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__.
print(bool(0), bool(0.0), bool(""), bool(None)) # False False False Falseprint(bool([]), bool({}), bool(set()), bool(())) # False False False Falseprint(bool(1), bool("0"), bool([0])) # True True True
# bool е наследник на int, затова True е равно на 1. Така се броят съвпаденията.print(True + True) # 2print(sum(n > 1 for n in [1, 2, 3])) # 2Какво означава „да искаме прошка, а не разрешение“
В Java е прието първо да се провери, а после да се действа. В Python по-често действат веднага и обработват грешката. Такъв стил се нарича EAFP (easier to ask forgiveness than permission). Той освен това премахва и състезателното условие между проверката и действието. Изключенията тук не се смятат за скъпа рядкост, използват се съвсем обичайно.
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); // 80config = {"port": "80"}
try: port = int(config["port"])except (KeyError, ValueError): port = 0print(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 |
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()); // 1System.out.println(cart.get(0)); // teaclass 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)) # 1print(cart[0]) # teaprint("tea" in cart) # Trueprint(list(cart)) # ['tea']
# Празната количка е лъжа, защото len е равно на 0.print(bool(Cart())) # FalseОператорите се претоварват по същия начин. Сравненията е удобно да се получат от един метод: __lt__ замества Comparable, а декораторът total_ordering ще допълни <=, > и >=.
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); // truefrom dataclasses import dataclassfrom 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.
# 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 прихваща самата променлива, затова тя може да се променя, а ще се вижда последната ѝ стойност.
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()); // 0def 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. Ето как е устроено стъпка по стъпка.
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
@loggeddef add(a, b): print("add") # 4. add return a + b
# Сега името add сочи към wrapper.print(add(2, 3)) # 6. 5Записът @logged е само кратка форма на присвояване. Двата варианта един до друг:
@loggeddef 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) и вече резултатът му работи като декоратор.
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()) # okprint(len(calls)) # 3
# Без wraps тук щеше да бъде wrapper.print(flaky.__name__) # flakyКакви готови декоратори има
Кеш на резултатите, като @Cacheable в Spring, има в стандартната библиотека. Изборът по типа на първия аргумент частично замества претоварването.
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)) # 12586269025print(fib.cache_info().hits) # 48
# Избира реализация по типа на първия аргумент.@singledispatchdef show(value): return "other"
@show.registerdef _(value: int): return "number"
@show.registerdef _(value: str): return "text"
print(show(1)) # numberprint(show("a")) # textprint(show(1.5)) # otherКласове: какво още трябва да знаете
С какво полето на класа се различава от полето на обекта
При четене на user.role 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.
class Point: __slots__ = ("x", "y")
def __init__(self, x, y): self.x = x self.y = y
point = Point(1, 2)try: point.z = 3except AttributeError as e: print(type(e).__name__) # AttributeErrorКак работи множественото наследяване
В Java наследяването на класове е единично, а общото поведение се изнася в default методите на интерфейсите. В Python родителите могат да са няколко. Редът, в който се търси методът, се нарича MRO (Method Resolution Order). Той е линеен и може да се разгледа. Важен момент: super() извиква не родителя, а следващия клас в този ред.
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 методите на интерфейса.
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}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.
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, Annclass User: name = "Ann"
def greet(self): return f"Hi, {self.name}"
user = User()print(getattr(user, "name")) # Annprint(getattr(user, "age", 0)) # 0print(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 с интерфейс.
class Logged: def __init__(self, target): self._target = target
# 1. Logged няма атрибут append, затова е извикан __getattr__. # 2. Той взема атрибута от истинския обект. def __getattr__(self, name): print(f"access {name}") # access append return getattr(self._target, name)
items = Logged([])
# 3. Полученият метод се извиква с аргумент 1.items.append(1)Какво още ще срещнете в чужд код
Тези неща трябват рядко, но в кода на фреймуърците ги има.
| Какво | Какво прави | Аналог в Java |
|---|---|---|
__new__ | създава обекта, а __init__ го попълва | няма |
| Дескриптор | обект с методи __get__ и __set__ в поле на класа. Така са устроени property и методите | няма |
| Метаклас | клас, който създава класове. Така Django сглобява моделите по техните полета | обработка на анотации и генериране на байткод |
__init_subclass__ | извиква се при деклариране на наследник, удобен за регистрация | ServiceLoader |
__del__ | финализатор, на него не може да се разчита, вместо него се използва with | finalize |
Подсказки за типовете
Проверява ли Python типовете
Не. Анотациите се запазват, но при изпълнение не влияят на нищо. Проверява ги отделна програма, най-често mypy или pyright, която се стартира по същия начин като линтера в CI. Получава се нещо като компилатора на Java, само че незадължителен. В услугите анотации се пишат навсякъде, а проверката се включва в строг режим.
def double(value: int) -> int: return value * 2
# Python ще изпълни това без грешки. Грешката ще покаже само mypy.print(double("ab")) # abab
# Анотациите могат да се прочетат. На това са построени Pydantic и FastAPI.print(double.__annotations__["value"].__name__) # intКак се записват типовете
| Java | Python |
|---|---|
int, String | int, str |
List<String> | list[str] |
Map<String, Integer> | dict[str, int] |
Set<Long> | set[int] |
int[] | list[int] |
| двойка стойности | tuple[str, int] |
| неизменяем списък | tuple[str, ...] |
@Nullable String | str | None |
Object | object |
| изобщо няма проверка | Any |
Collection<? extends T> | Iterable[T], Sequence[T] |
Function<Integer, String> | Callable[[int], str] |
Class<T> | type[T] |
void | None |
sealed interface с варианти | A | B |
| ограничен набор от низове | Literal["asc", "desc"] |
| връща this | Self |
| никога не връща | NoReturn |
typealias: няма в Java | type UserId = int |
В стария код типовете изглеждат другояче. Ако срещнете такова нещо, знайте, че е същото.
| Стар запис | Нов запис | От коя версия |
|---|---|---|
List[str], Dict[str, int] | list[str], dict[str, int] | 3.9 |
Optional[str] | str | None | 3.10 |
Union[int, str] | int | str | 3.10 |
T = TypeVar("T"), Generic[T] | class Box[T]: | 3.12 |
Параметърът Sequence[T] приема и списък, и кортеж, но не може да се променя. Това е същата идея като List<? extends T>: за да приема подтипове, параметърът трябва да е само за четене. Затова за параметрите на функциите са по-добри широките типове като Iterable и Sequence, а за резултата – конкретният list.
from collections.abc import Callable, Iterable
def total(items: Iterable[int]) -> int: return sum(items)
# Подхожда всяка колекция, която може да се обхожда.print(total([1, 2])) # 3print(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.
var iterator = List.of("a", "b").iterator();
while (iterator.hasNext()) { System.out.print(iterator.next()); // ab}# 1. for извиква iter(items).iterator = iter(["a", "b"])
# 2. На всяка стъпка той извиква next(iterator).print(next(iterator)) # aprint(next(iterator)) # b
# 3. StopIteration завършва цикъла for, никой не я вижда.print(next(iterator, "end")) # endКакво е генератор
Да се напише итератор в Java означава клас с полета, които пазят състоянието, и два метода. В Python за това има генератор: функция, в която вместо return стои yield. Тя връща стойност и замръзва на този ред, запазвайки всички променливи, а при следващото поискване продължава от същото място. Цифрите показват реда на изпълнение.
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.
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}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: стойността се появява, само когато я поискат. Затова може да се опише безкрайна последователност и затова големият файл минава през верига от функции, без да се зарежда в паметта.
var firstFive = Stream.iterate(0, n -> n + 2) .limit(5) .toList();System.out.println(firstFive); // [0, 2, 4, 6, 8]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]var lines = Stream.of("1", "x", "2");
int total = lines .filter(line -> line.matches("\\d+")) .mapToInt(Integer::parseInt) .sum();System.out.println(total); // 3lines = ["1", "x", "2"]
# Кръгли скоби: това е генераторен израз. Всяка стъпка е мързелива, както в стрийма.digits = (line for line in lines if line.isdigit())numbers = (int(line) for line in digits)
# Работата започва едва тук.print(sum(numbers)) # 3Разликата между квадратните и кръглите скоби е главното, което трябва да запомните тук. Първият вариант веднага строи целия списък в паметта, а вторият не строи нищо.
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) # Trueprint(sys.getsizeof(as_gen) < 1_000) # TrueГенераторът, както и стриймът, може да се мине само веднъж. Второто минаване не дава грешка, а просто е празно. Същото важи за резултатите от map, filter и zip.
var stream = Stream.of(1, 2);stream.toList();
// Втори път: IllegalStateException.// stream.toList();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)) # 6print(sum(doubled)) # 0Какво още могат генераторите
yield from предава стойностите на друг генератор. Методът send изпраща стойност вътре, в точката на спиране на yield. На този механизъм по-рано се градеше асинхронността, а днес се среща рядко.
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.0print(avg.send(20)) # 15.0Готовите мързеливи операции са в модула itertools.
from itertools import batched, chain, groupby, islice, takewhile # Python
Stream.of(1, 2, 3).limit(2).toList() # Java: [1, 2]list(islice([1, 2, 3], 2)) # Python: [1, 2]
Stream.concat(Stream.of(1), Stream.of(2)).toList() # Java: [1, 2]list(chain([1], [2])) # Python: [1, 2]
Stream.of(1, 2, 3).takeWhile(i -> i < 3).toList() # Java: [1, 2]list(takewhile(lambda i: i < 3, [1, 2, 3])) # Python: [1, 2]
# Разбива на партиди по 2.list(batched([1, 2, 3], 2)) # Python: [(1, 2), (3,)]
# Групира само последователни стойности, затова първо се сортира.[k for k, _ in groupby(sorted("abab"))] # Python: ['a', 'b']Нишки и процеси
Какво е GIL
GIL (Global Interpreter Lock) е едно общо заключване за целия процес. Код на Python може да изпълнява само нишката, която го държи, затова в даден момент работи само една нишка, колкото и ядра да има. Нишките обаче са истински, системни, както в Java. По време на чакане на мрежа, диск или sleep нишката освобождава заключването и работят другите.
Изводът е прост: нишките ускоряват чакането, но не ускоряват изчисленията. За изчисления са нужни процеси, всеки от които има собствен GIL.
| Задача | Java | Python |
|---|---|---|
| Много чакане на мрежа или база данни | нишки, виртуални нишки | asyncio или нишки |
| Изчисления на всички ядра | нишки | процеси |
| Блокираща библиотека | нишки | нишки |
Как да стартираме нишка
Класът Thread е почти същият. Вместо да реализира Runnable, в него се подават функция и нейните аргументи.
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()); // workerSystem.out.println(thread.isAlive()); // falseimport 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) # workerprint(thread.is_alive()) # FalseЕто какво значи „нишките ускоряват чакането“. Четири нишки спят по 0,1 секунда, а общото време също е около 0,1 секунда. Ако заменим sleep с изчисление в цикъл, те ще се изпълняват строго една след друга.
import threadingimport 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 Нишка B1. прочита counter = 5 2. прочита counter = 5 3. записва counter = 64. записва counter = 6Две прибавяния, а броячът нарасна с 1.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()); // 40000import 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.
import threading
# Обикновен Lock: вторият acquire в същата нишка би увиснал завинаги.lock = threading.Lock()lock.acquire()print(lock.acquire(blocking=False)) # Falselock.release()
# RLock може да се взема повторно.rlock = threading.RLock()with rlock: with rlock: print("inside") # insideС какво се заменя ExecutorService
Пулът от нишки работи по същия начин. submit връща Future, а result() е get(). Вместо try с shutdown се използва with: при излизане от блока пулът изчаква всички задачи и се затваря.
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 }}from concurrent.futures import ThreadPoolExecutor, as_completed
def square(n): return n * n
with ThreadPoolExecutor(max_workers=4) as pool: future = pool.submit(square, 3) print(future.result()) # 9
# map: резултатите са в реда на аргументите. print(list(pool.map(square, [1, 2]))) # [1, 4]
# as_completed: в реда на готовност, като CompletionService. futures = [pool.submit(square, n) for n in [1, 2]] print(sorted(f.result() for f in as_completed(futures))) # [1, 4]Изключението от задача се държи както в Java: пази се във Future и се хвърля при извикване на result(). Ако никой не поиска резултата, грешката се губи безшумно.
Кое на кое съответства в java.util.concurrent
| Java | Python |
|---|---|
Thread | threading.Thread |
synchronized, ReentrantLock | threading.RLock |
Semaphore | threading.Semaphore |
CountDownLatch | threading.Event, Barrier |
CyclicBarrier | threading.Barrier |
Condition | threading.Condition |
BlockingQueue | queue.Queue |
ConcurrentHashMap | dict и Lock |
AtomicInteger | int и Lock |
ThreadLocal | threading.local |
ExecutorService | ThreadPoolExecutor |
ForkJoinPool | ProcessPoolExecutor |
Future | concurrent.futures.Future |
CompletableFuture | asyncio.Task |
volatile | не е нужен |
Как да направим производител и потребител
queue.Queue е BlockingQueue: put чака, ако опашката е пълна, а get чака, ако е празна. Цифрите показват реда на работа. За завършване в опашката е прието да се слага специална стойност, в Java я наричат poison pill.
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]import queueimport 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: той се събужда веднага щом флагът бъде вдигнат.
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()); // falseimport 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__", защото новият процес импортира файла отново.
var result = IntStream.of(1, 2, 3, 4) .parallel() .map(n -> n * n) .sum();System.out.println(result); // 30from 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]))) # 30GIL постепенно си отива. От версия 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.
Сравнете с виртуалните нишки. Кодът прави едно и също: две операции на чакане вървят едновременно.
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}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. Цифрите показват реалния ред на изхода.
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 един след друг се изпълняват последователно. За да вървят операциите едновременно, са нужни задачи.
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 ordersimport asyncioimport 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.
import asyncioimport 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.2print(asyncio.run(measure(good))) # 0.1Как правилно да стартираме група задачи
TaskGroup се появи в 3.11, това е аналог на StructuredTaskScope в Java. Правилото е просто: задачите не надживяват блока async with. Ако една се провали, останалите се отменят, а грешките пристигат заедно в група изключения. За обработката им има специална форма except*. При gather такова нещо няма: при грешка останалите задачи продължават да работят сами по себе си.
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 не я улавя.
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}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.
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, ограничението се прави със семафор.
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(); } }); }}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 тук не може да се използва: той ще спре целия цикъл на събитията.
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.
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 при логването: идентификаторът на заявката не се налага да се предава през всички извиквания.
var requestId = new ThreadLocal<String>();requestId.set("a");System.out.println(requestId.get());import asynciofrom contextvars import ContextVar
request_id = ContextVar("request_id", default="-")
def log(message): print(request_id.get(), message) # a work b work
async def handle(rid): request_id.set(rid) await asyncio.sleep(0) log("work")
async def main(): await asyncio.gather(handle("a"), handle("b"))
# В самия main стойността не се е променяла. print(request_id.get()) # -
asyncio.run(main())Как да свържем нишките и asyncio
| Откъде | Накъде | Как |
|---|---|---|
| Обикновен код | корутина | asyncio.run(main()) |
| Корутина | блокираща функция | await asyncio.to_thread(func) |
| Друга нишка | корутина | asyncio.run_coroutine_threadsafe(coro, loop) |
| Друга нишка | обикновена функция в цикъла | loop.call_soon_threadsafe(func) |
С какво asyncio се различава от виртуалните нишки
Целта е една и съща: евтино чакане. Разликата е, че виртуалните нишки го скриват вътре в JVM, а обикновеният блокиращ код продължава да работи. В asyncio асинхронността се вижда в кода: функция с async може да се извиква само от друга такава функция. Това се нарича „оцветяване на функциите“.
| Виртуални нишки | asyncio | |
|---|---|---|
| Ядра на процесора | всички | едно |
| Къде се превключва | при всяко блокиране | само при await |
| Блокиращ код | работи | спира всичко |
| Състезания в кода | както при обикновените нишки | само между await |
| Отмяна | interrupt | CancelledError |
| Библиотеки | обикновени | трябват асинхронни |
Зависимости и тестове
С какво се заменят Maven и Gradle
В Java зависимостите се намират в общ кеш ~/.m2, а нужните версии се предават на всеки проект през classpath. В Python инсталираната библиотека се вижда от цялото копие на интерпретатора, затова всеки проект има собствено копие в директорията .venv. Такава директория се нарича виртуална среда. Описанието на проекта е във файла pyproject.toml, а точните версии – в lock файла. Сега всичко това обикновено се управлява от инструмента uv.
| Java | Python |
|---|---|
pom.xml, build.gradle | pyproject.toml |
gradle.lockfile | uv.lock |
| Maven Central | PyPI |
mvn dependency:tree | uv tree |
ред в блока dependencies { } | uv add httpx |
gradle build | uv build |
gradle test | uv run pytest |
| Checkstyle, Spotless | ruff |
javac като проверка на типовете | mypy |
.jar | .whl |
Импортът в Python търси модули по списък от директории sys.path, като classpath в Java. Цикличните импорти тук са опасни, за разлика от Java: импортът изпълнява файла и ако a.py импортира b.py, а той отново a.py, вторият ще види наполовина изпълнен модул и ще падне с ImportError. Решава се с изнасяне на общия код в трети модул.
С какво се заменя JUnit
На практика стандартът е pytest. Тестът е обикновена функция с име, започващо с test_, а вместо набор от assertEquals се използва обикновен assert. При провал pytest сам показва какво с какво е сравнявано. Вместо @BeforeEach има фикстура: функция, чието име тестът посочва като параметър. Всичко след yield във фикстурата се изпълнява след теста, като @AfterEach.
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")); }}import pytest
# Заменя @BeforeEach: pytest я извиква преди всеки тест.@pytest.fixturedef 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 заменя името в посочения модул за времето на теста. Важно правило: подменя се там, където името се използва, а не там, където е декларирано.
var repository = mock(UserRepository.class);when(repository.find(1)).thenReturn("Ann");
assertEquals("Ann", repository.find(1));verify(repository).find(1);from unittest.mock import Mock
repository = Mock()repository.find.return_value = "Ann"
print(repository.find(1)) # Annrepository.find.assert_called_once_with(1)
# Да накараме метода да хвърли изключение: thenThrow.repository.save.side_effect = ConnectionError("down")Макар че подмяната работи, кодът е по-удобно да се тества както в Java: през зависимости, подадени в конструктора. Така е написана услугата в края на статията.
Памет и производителност
Как Python управлява паметта
В JVM обектите живеят, докато не ги намери събирачът на боклука. В CPython всеки обект има брояч на референциите и обектът се изтрива веднага щом броячът стане нула. Циклични референции така не се откриват, затова допълнително има събирач на цикли от модула gc.
| JVM | CPython | |
|---|---|---|
| Кога се изтрива обектът | когато стигне събирачът | веднага, когато няма референции |
| Преместване на обекти | да, heap се уплътнява | не |
| Настройка на размера | -Xmx | няма, ограничава го контейнерът |
Размер на int | 4 байта | около 28 байта |
Какво да правим, ако е бавно
Първо се измерва. cProfile е прост профайлър, а за работеща услуга има py-spy, който се закача отвън. Обикновено най-бързо помага изборът на правилна структура от данни и на вградени функции, написани на C: sum, sorted, set. Тежките изчисления се правят в библиотеки на C като NumPy, които освобождават GIL по време на работа.
Архитектура на услуга на Python
Какво заменя Tomcat и Servlet API
Схемата е същата: има сървър, който приема връзките, и стандартен интерфейс, през който той извиква вашето приложение. Интерфейсите са два: WSGI за синхронен код и ASGI за асинхронен.
| Java | Python | Какво е |
|---|---|---|
| Servlet API | WSGI | синхронен интерфейс между сървъра и приложението |
WebFlux HttpHandler | ASGI | асинхронен интерфейс |
| Tomcat, Jetty | gunicorn | сървър за WSGI |
| Netty | uvicorn | сървър за ASGI |
| Spring MVC | Django, Flask | синхронни фреймуърци |
| WebFlux | FastAPI | асинхронен фреймуърк |
С какво моделът на работа се различава от Java
Java услугата е един процес с пул от нишки и обща памет. Заради GIL услугата на Python се състои от няколко процеса, които се наричат воркъри. От това следва следното:
- Мащабиране. В Kubernetes обикновено се държи по един воркър на под и се добавят реплики.
- Кешът в паметта е собствен за всеки воркър. Общите данни се пазят в Redis или в базата данни.
- Пулът от връзки също е собствен за всеки: 4 воркъра по 10 връзки са 40 връзки към базата данни.
- Планировчикът на задачи (scheduler) ще се стартира във всеки воркър, затова се изнася в отделен процес.
- Дългите изчисления в обработчика на заявка са недопустими. Те се изпращат в опашка със задачи, например Celery.
Как да минем без Spring
Контейнер за зависимости почти никога не е нужен. Зависимостите се предават през конструктора, а вместо контекст на Spring има една функция при стартиране, която създава обектите и ги предава един на друг. Такова място се нарича composition root. Слоевете са същите, само че минават не по пакети с класове, а по модули.
| Spring | Модул в Python | Какво знае |
|---|---|---|
@Entity | domain.py | само стандартната библиотека |
| интерфейси на хранилищата | ports.py | домейна |
@Service | service.py | домейна и портовете |
@Repository | storage.py | домейна и базата данни |
@RestController | http_api.py | услугата и HTTP |
@Configuration, main | __main__.py | всичко |
Кои шаблони от Java тук не са нужни
Много шаблони в Java заобикалят ограничения на езика, каквито в Python няма. Например стратегията в Java е интерфейс и клас, а в Python – просто функция.
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.0def seasonal(price): return price * 0.9
def checkout(price, discount): return discount(price)
print(checkout(100, seasonal)) # 90.0| Java | Python |
|---|---|
| Singleton | обект на ниво модул |
| Factory | функция или самият клас |
| Builder | именувани аргументи и replace |
| Strategy, Command | предава се функция |
| Observer | списък от функции |
| Visitor | match, singledispatch |
| Decorator | @decorator |
| Iterator | генератор |
| Proxy | __getattr__ |
| Клас с помощни методи | функции в модул |
| Гетъри и сетъри | поле, после @property |
Код, написан с навиците на Java, си личи веднага. Ето какво го издава:
# Лошо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правила заявки към SQLiteorders-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.
[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 = truefiles = ["src", "tests"]
[tool.ruff]line-length = 88
[tool.ruff.lint]select = ["E", "F", "I", "UP", "B", "ASYNC", "G"]Как да опишем поръчката
Започваме от центъра. Този файл заменя класа Order с бизнес методи. Той не знае нищо нито за HTTP, нито за базата данни.
from dataclasses import dataclass, replacefrom datetime import datetimefrom decimal import Decimalfrom enum import StrEnumfrom 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, затова в тестовете може да се използва проста имитация, която не наследява нищо.
from collections.abc import Sequencefrom 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, а за идентификаторите – интерфейс с реализация. Тук е достатъчно да се подаде функция, а в теста лесно може да се подаде друга.
from collections.abc import Callable, Sequencefrom datetime import UTC, datetime, timedeltafrom decimal import Decimalfrom uuid import uuid4
from .domain import IllegalTransition, Order, OrderNotFound, OrderStatusfrom .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 Заявка B1. чете поръчката: new 2. чете поръчката: new3. UPDATE ... WHERE status='new' обновен 1 ред, успех 4. UPDATE ... WHERE status='new' обновени 0 реда, грешка 409Как да се обърнем към базата данни от асинхронен код
Това е аналог на клас с @Repository. Драйверът sqlite3 е блокиращ, затова всяко извикване отива в отделна нишка. За PostgreSQL ще вземете асинхронен драйвер и to_thread ще изчезне, а портът няма да се промени.
import asyncioimport sqlite3from collections.abc import Callable, Sequencefrom contextlib import closingfrom datetime import datetimefrom decimal import Decimalfrom 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. Кодът, който пише в лога, нищо не знае за идентификатора.
import jsonimport loggingfrom 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 заявката е обикновен текст: ред на заявката, хедъри, празен ред и тяло.
POST /orders HTTP/1.1Host: localhost:8080Content-Length: 38
{"customer": "ann", "amount": "19.99"}import asyncioimport jsonimport loggingfrom collections.abc import Awaitable, Callablefrom contextlib import suppressfrom dataclasses import dataclassfrom http import HTTPStatusfrom typing import Anyfrom 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.
import jsonimport loggingfrom decimal import Decimal, InvalidOperationfrom http import HTTPStatusfrom uuid import uuid4
from .domain import ( DomainError, IllegalTransition, InvalidOrder, Order, OrderNotFound, OrderStatus,)from .http_server import Request, Responsefrom .logs import request_idfrom .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: неизменяем обект, прочетен веднъж от променливите на средата (срокът на неплатената поръчка и времето за спиране на услугата се вземат от стойностите по подразбиране). В теста вместо средата може да се подаде обикновен речник.
import osfrom collections.abc import Mappingfrom dataclasses import dataclassfrom datetime import timedeltafrom 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, а услугата довършва текущите заявки.
import asyncioimport loggingimport signalfrom contextlib import suppressfrom datetime import timedelta
from .config import Settingsfrom .http_api import OrdersApifrom .http_server import HttpServerfrom .logs import configure_loggingfrom .service import OrderServicefrom .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 ще провери, че му пасва.
import asynciofrom collections.abc import Sequencefrom datetime import UTC, datetime, timedeltafrom 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.fixturedef 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 означава „избери сам“) с истинска база данни във временна директория.
import asyncioimport jsonfrom pathlib import Pathfrom typing import Any
from orders.http_api import OrdersApifrom orders.http_server import HttpServerfrom orders.service import OrderServicefrom 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. Обяснения към всяка стъпка има в коментарите.
# Етап 1: uv инсталира само работните зависимости по lock файла.FROM python:3.15-slim AS buildRUN pip install --no-cache-dir uvWORKDIR /appCOPY pyproject.toml uv.lock ./COPY src ./srcRUN uv sync --frozen --no-dev --no-editable
# Етап 2: готовата среда се копира в чист образ.FROM python:3.15-slimRUN useradd --create-home app && mkdir /data && chown app /dataCOPY --from=build /app/.venv /app/.venvUSER appENV ORDERS_DATABASE=/data/orders.db PATH=/app/.venv/bin:$PATHEXPOSE 8080VOLUME /dataHEALTHCHECK 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"]Как да стартираме услугата
# Създава среда и uv.lockuv 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Ще създадем поръчка, ще я платим, ще опитаме да платим още веднъж и ще изпратим неверни данни.
$ curl -s -X POST localhost:8080/orders \ -d '{"customer": "ann", "amount": "19.99"}' | python -m json.tool{ "id": "2165d2a6a1ee4cbb966203d6099d49c9", "customer": "ann", "amount": "19.99", "status": "new", "created_at": "2026-11-05T18:44:05.717023+00:00"}
$ ID=2165d2a6a1ee4cbb966203d6099d49c9$ curl -s -X POST localhost:8080/orders/$ID/pay | python -m json.tool{ "id": "2165d2a6a1ee4cbb966203d6099d49c9", "customer": "ann", "amount": "19.99", "status": "paid", "created_at": "2026-11-05T18:44:05.717023+00:00"}
$ curl -s -X POST localhost:8080/orders/$ID/pay{"error": "cannot pay an order that is paid"}
$ curl -s -X POST localhost:8080/orders \ -d '{"customer": "a", "amount": "NaN"}'{"error": "amount must be a positive number"}Какво от това ще заменят фреймуърците
Този код е написан на ръка само за да се видят механизмите. В истински проект по-голямата част ще поемат библиотеките. Написаният тук service.py при това няма да се промени.
| Тук е написано | Какво ще се вземе в проекта | Аналог в Java |
|---|---|---|
http_server.py | uvicorn, gunicorn | Tomcat, Netty |
http_api.py | FastAPI, Flask, Django | Spring MVC |
| разчитане на JSON | Pydantic | Jackson, Bean Validation |
storage.py | SQLAlchemy, asyncpg | Hibernate, JDBC |
CREATE TABLE | Alembic | Flyway, Liquibase |
Settings | pydantic-settings | @ConfigurationProperties |
expire_loop | Celery, APScheduler | @Scheduled, Quartz |
retry | tenacity | Resilience4j |
logs.py | structlog | Logback, MDC |
| няма | prometheus-client, OpenTelemetry | Micrometer |
| няма | httpx | WebClient |
| няма | aiokafka | Spring Kafka |
Какво да четем по-нататък
Първоизточници: моделът на данните, документацията на asyncio, разделът за билд без GIL, документацията на mypy и какво е ново във всяка версия. За сравнение с Java на този сайт има статии за основите на Java, многонишковостта, Project Loom и Spring.