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
# Перші 8 символів хешу SHA-256.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]Що таке замикання
Замикання — це функція, яка запам'ятала змінні з місця, де її створено. 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. Атрибута append у Logged немає, тому викликано __getattr__. # 2. Він бере атрибут у справжнього об'єкта. def __getattr__(self, name): print(f"access {name}") # access append return getattr(self._target, name)
items = Logged([])
# 3. Отриманий метод викликається з аргументом 1.items.append(1)Що ще трапиться в чужому коді
Ці речі потрібні рідко, але в коді фреймворків вони є.
| Що | Що робить | Аналог у Java |
|---|---|---|
__new__ | створює об'єкт, а __init__ його заповнює | немає |
| Дескриптор | об'єкт із методами __get__ і __set__ у полі класу. Так влаштовані property і методи | немає |
| Метаклас | клас, який створює класи. Так Django збирає моделі за їхніми полями | обробка анотацій і генерація байткоду |
__init_subclass__ | викликається під час оголошення нащадка, зручний для реєстрації | ServiceLoader |
__del__ | фіналізатор, покладатися на нього не можна, замість нього with | finalize |
Підказки типів
Чи перевіряє Python типи
Ні. Анотації зберігаються, але під час запуску ні на що не впливають. Перевіряє їх окрема програма, найчастіше mypy або pyright, яку запускають так само, як лінтер у CI. Виходить компілятор Java, тільки необов'язковий. У сервісах анотації пишуть усюди, а перевірку вмикають у строгому режимі.
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]Як зупинити потік
Методу interrupt у Python немає, ззовні потік зупинити не можна. Потік сам перевіряє прапорець, і для цього є 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 |
gradle dependencies add | uv add httpx |
gradle build | uv build |
gradle test | uv run pytest |
| Checkstyle, Spotless | ruff |
javac як перевірка типів | mypy |
.jar | .whl |
Імпорт у Python шукає модулі за списком каталогів sys.path, як classpath у Java. Циклічний імпорт тут небезпечний, на відміну від Java: імпорт виконує файл, і якщо a.py імпортує b.py, а той знову a.py, другий побачить наполовину виконаний модуль і впаде з ImportError. Лікується це винесенням спільного коду в третій модуль.
Чим замінити JUnit
Стандарт на практиці — pytest. Тест — це звичайна функція з іменем на test_, а замість набору assertEquals використовується звичайний assert. У разі падіння pytest сам показує, що з чим порівнювалося. Замість @BeforeEach є фікстура: функція, чиє ім'я тест указує як параметр. Усе, що стоїть після yield у фікстурі, виконується після тесту, як @AfterEach.
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 | |
|---|---|---|
| Коли видаляється об'єкт | коли дійде збирач | одразу, коли немає посилань |
| Переміщення об'єктів | так, купа стискається | ні |
| Налаштування розміру | -Xmx | немає, обмежує контейнер |
Розмір int | 4 байти | близько 28 байтів |
Що робити, якщо повільно
Спочатку виміряти. cProfile — це простий профайлер, а для працюючого сервісу є py-spy, який підключається ззовні. Зазвичай найшвидше допомагає вибір правильної структури даних і вбудованих функцій, написаних на C: sum, sorted, set. Важкі обчислення роблять у бібліотеках на C на кшталт NumPy, які відпускають GIL на час роботи.
Архітектура сервісу на Python
Що замінює Tomcat і Servlet API
Схема та сама: є сервер, який приймає з'єднання, і стандартний інтерфейс, через який він викликає ваш застосунок. Інтерфейсу два: WSGI для синхронного коду та ASGI для асинхронного.
| Java | Python | Що це |
|---|---|---|
| Servlet API | WSGI | синхронний інтерфейс сервера й застосунку |
WebFlux HttpHandler | ASGI | асинхронний інтерфейс |
| Tomcat, Jetty | gunicorn | сервер для WSGI |
| Netty | uvicorn | сервер для ASGI |
| Spring MVC | Django, Flask | синхронні фреймворки |
| WebFlux | FastAPI | асинхронний фреймворк |
Чим модель роботи відрізняється від Java
Java-сервіс — це один процес із пулом потоків і спільною пам'яттю. Через GIL сервіс на Python — це кілька процесів, їх називають воркерами. Звідси випливає ось що:
- Масштабування. У Kubernetes зазвичай тримають один воркер на под і додають репліки.
- Кеш у пам'яті у кожного воркера свій. Спільні дані зберігають у Redis або базі.
- Пул з'єднань теж у кожного свій: 4 воркери по 10 з'єднань — це 40 з'єднань із базою.
- Планувальник запуститься в кожному воркері, тому його виносять в окремий процес.
- Довгі обчислення в обробнику запиту неприпустимі. Їх відправляють у чергу завдань, наприклад Celery.
Як обійтися без Spring
Контейнер залежностей майже ніколи не потрібен. Залежності передають через конструктор, а замість контексту Spring є одна функція під час запуску, яка створює об'єкти й передає їх одне одному. Таке місце називають composition root. Шари при цьому ті самі, тільки проходять не пакетами з класами, а модулями.
| Spring | Модуль у Python | Що знає |
|---|---|---|
@Entity | domain.py | лише стандартну бібліотеку |
| інтерфейси репозиторіїв | ports.py | домен |
@Service | service.py | домен і порти |
@Repository | storage.py | домен і базу |
@RestController | http_api.py | сервіс і HTTP |
@Configuration, main | __main__.py | усе |
Які патерни з Java тут не потрібні
Багато патернів у Java обходять обмеження мови, яких у Python немає. Наприклад, стратегія в Java — це інтерфейс і клас, а в Python просто функція.
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.