Python для Java-разработчиков
Эта статья для тех, кто уже знает Java и хочет писать на Python. Почти везде сначала идёт код на Java, а сразу под ним то же самое на Python, поэтому объяснения в основном написаны прямо в комментариях к коду.
Порядок такой: сначала синтаксис, затем основные типы и их методы, затем то, что устроено не так, как в Java: переменные, декораторы, генераторы, потоки и asyncio. В конце из всего этого собран микросервис. Статья только про язык и стандартную библиотеку, FastAPI, Django и Flask будут отдельными статьями.
Коротко о самом Python. Его создал Гвидо ван Россум, первая версия вышла в 1991 году, то есть он старше Java. Версия 3.0 в 2008 году сломала совместимость со второй, и сейчас живёт только третья ветка. Примеры запускались на Python 3.14 и 3.15 и на Java 26, кроме тех, где нужны сторонние библиотеки Java. Там, где возможность появилась недавно, версия указана.
Плюсы: кода намного меньше, чем на Java, быстрый старт программы и огромная экосистема, особенно для данных и машинного обучения.
Минусы: вычисления заметно медленнее, чем на JVM, потоки обычно не ускоряют вычисления, а ошибки типов без отдельного инструмента видны только при запуске.
Синтаксис: Java и Python рядом
Как выглядит программа
В Java любой код живёт внутри класса, а запуск начинается с метода main. В Python файл просто выполняется сверху вниз.
public class Main { public static void main(String[] args) { System.out.println("Hello"); }}# Весь файл: ни класса, ни main, ни точки с запятой.print("Hello") # HelloЕсли файл можно и запустить, и импортировать из другого файла, код запуска прячут в проверку. Это ближе всего к main в Java.
def main(): print("Hello") # Hello
# __name__ — имя текущего модуля. Если файл запустили напрямую,# там строка "__main__". Если его импортировали — имя файла,# и main() не вызовется.if __name__ == "__main__": main()Как записываются блоки и комментарии
Вместо фигурных скобок в Python отступы. Это не оформление, а синтаксис: неверный отступ не даст программе запуститься.
int price = 150;
// Однострочный комментарийif (price > 100) { System.out.println("expensive"); // expensive} else { System.out.println("cheap");}
/* Многострочного комментария в Python нет */price = 150
# Однострочный комментарий# Скобки вокруг условия не нужны, в конце строки двоеточие,# а тело блока сдвинуто на 4 пробела.if price > 100: print("expensive") # expensiveelse: print("cheap")Длинное выражение переносят внутри скобок. Вместо Javadoc пишут строку документации сразу под объявлением, она называется docstring.
/** * Считает сумму с налогом. */int total(int price, int tax) { return price + tax;}
System.out.println(total(100, 20)); // 120def total(price, tax): """Считает сумму с налогом."""
# Внутри скобок перенос строки разрешён. return (price + tax)
print(total(100, 20)) # 120
# Docstring доступна и во время работы программы.print(total.__doc__) # Считает сумму с налогом.Заглушка для пустого блока в Java это {}. В Python пустой блок записать нельзя, поэтому есть слово pass, которое ничего не делает.
void todo() {}
class Empty {}def todo(): pass
class Empty: passКак объявить переменную
В Python нет объявления. Переменная появляется при первом присваивании, и тип есть не у неё, а у значения, которое в ней лежит. Это называют динамической типизацией. Подробнее о том, что такое переменная, рассказано ниже.
int age = 30; # Javaage = 30 # Python
var name = "Ann"; # Javaname = "Ann" # Python
# Слова final нет. ВЕРХНИЙ_РЕГИСТР значит «не меняйте», но это соглашение.final int MAX_SIZE = 100; # JavaMAX_SIZE = 100 # Python
# В Java это ошибка компиляции, в Python теперь в age лежит строка.age = "thirty" # Python
# Присвоить сразу несколько значений.int a = 1, b = 2; # Javaa, b = 1, 2 # Python
# Поменять местами: в Java нужна временная переменная.int tmp = a; a = b; b = tmp; # Javaa, b = b, a # Python
a + " " + b # Java: 2 1f"{a} {b}" # Python: 2 1Если тип всё-таки хочется указать, для этого есть подсказка типа. Python её не проверяет, но её проверяют IDE и отдельный инструмент mypy.
int count = 0; # Javacount: int = 0 # Python
String title = "Report"; # Javatitle: str = "Report" # Python
# Python не возражает: подсказка — только пометка для инструментов.count = "many" # PythonКак принято называть
Правила описаны в документе PEP 8, это аналог Java Code Conventions. Главное отличие — слова в именах переменных и функций разделяются подчёркиванием.
| Что | Java | Python |
|---|---|---|
| Переменная | userName | user_name |
| Метод, функция | findUser() | find_user() |
| Класс | OrderService | OrderService |
| Константа | MAX_SIZE | MAX_SIZE |
| Пакет | com.shop.orders | orders |
| Файл | OrderService.java | order_service.py |
| Закрытое поле | private int size | _size |
| Отступ | 4 пробела | 4 пробела |
Какие есть базовые типы
Примитивов в Python нет, всё является объектом, даже число. Поэтому нет и пары int и Integer. Разных целых типов тоже нет, один int хранит число любого размера.
| Java | Python | Пример |
|---|---|---|
byte, short, int, long, BigInteger | int | 42 |
float, double | float | 3.14 |
boolean | bool | True, False |
char, String | str | "a", "Ann" |
null | None | None |
byte[] | bytes | b"abc" |
Object | object | object() |
BigDecimal | decimal.Decimal | Decimal("9.99") |
Обратите внимание: True, False и None пишутся с большой буквы. Отдельного типа для символа нет, символ — это строка длины 1.
int number = 42; # Javanumber = 42 # Python
Object text = "Ann"; # Javatext = "Ann" # Python
# Узнать класс значения. У числа в Python он тоже есть.text.getClass().getSimpleName() # Java: Stringtype(text).__name__ # Python: strtype(number).__name__ # Python: int
# Проверить тип.text instanceof String # Java: trueisinstance(text, str) # Python: True
# У числа есть методы, скобки нужны только у литерала.Integer.bitCount(255) # Java: 8(255).bit_count() # Python: 8Как привести один тип к другому
Приведения в скобках нет. Вместо него вызывают класс нужного типа как функцию.
(int) 3.7 # Java: 3int(3.7) # Python: 3
(double) 3 # Java: 3.0float(3) # Python: 3.0
Integer.parseInt("42") # Java: 42int("42") # Python: 42
Double.parseDouble("3.5") # Java: 3.5float("3.5") # Python: 3.5
String.valueOf(42) # Java: 42str(42) # Python: 42
# Символ и его код.(int) 'A' # Java: 65ord("A") # Python: 65
(char) 65 # Java: Achr(65) # Python: A
# Ловушка: в Python непустая строка всегда истинна, даже "false".Boolean.parseBoolean("false") # Java: falsebool("false") # Python: TrueСтрока и число сами не склеиваются. Там, где Java молча вызовет String.valueOf, Python выдаст ошибку.
int age = 30; # Javaage = 30 # Python
"Age: " + age # Java: Age: 30
# В Python "Age: " + age даст TypeError. Об f-строках рассказано ниже."Age: " + str(age) # Python: Age: 30f"Age: {age}" # Python: Age: 30Какие есть арифметические операторы
Самая частая ошибка здесь — деление. В Python / всегда даёт дробное число, а целочисленное деление записывается как //. Операторов ++ и -- нет.
7 / 2 # Java: 37 // 2 # Python: 3
7.0 / 2 # Java: 3.57 / 2 # Python: 3.5
7 % 2 # Java: 17 % 2 # Python: 1
# В Python для степени есть оператор, а результат остаётся целым.Math.pow(2, 10) # Java: 1024.02 ** 10 # Python: 1024
int counter = 0; # Javacounter = 0 # Python
# counter++ в Python не сработает.counter++; # Javacounter += 1 # PythonКак сравнивать значения
Здесь в Python всё наоборот. == сравнивает содержимое, как equals в Java. А is проверяет, что это один и тот же объект, как == для ссылок в Java.
var first = new ArrayList<>(List.of(1, 2)); # Javafirst = [1, 2] # Python
var second = new ArrayList<>(List.of(1, 2)); # Javasecond = [1, 2] # Python
# Равно ли содержимое.first.equals(second) # Java: truefirst == second # Python: True
# Один ли это объект.first == second # Java: falsefirst is second # Python: False
# Пустое значение. None проверяют только через is.String name = null; # Javaname = None # Python
name == null # Java: truename is None # Python: True
name != null # Java: falsename is not None # Python: False
# Сравнения можно писать цепочкой, как в математике.int age = 30; # Javaage = 30 # Python
age >= 18 && age < 65 # Java: true18 <= age < 65 # Python: TrueКак записать логические условия
Вместо &&, || и ! используются слова. Кроме того, условием может быть любое значение, а не только boolean: пустые коллекции, пустая строка, ноль и None считаются ложью.
boolean active = true; # Javaactive = True # Python
boolean admin = false; # Javaadmin = False # Python
active && !admin # Java: trueactive and not admin # Python: True
active || admin # Java: trueactive or admin # Python: Trueactive = Trueadmin = Falseprint(active and not admin) # Trueprint(active or admin) # True
# Обычный способ проверить, что коллекция пуста.items = []if not items: print("no items") # no items
# Одно условие заменяет проверку и на None, и на пустоту.name = Noneif name: print(name)and и or возвращают не True или False, а один из своих операндов. На этом держится короткая запись значения по умолчанию.
String name = ""; # Javaname = "" # Python
# or вернёт первое истинное значение, здесь это "guest".name == null || name.isEmpty() ? "guest" : name # Java: guestname or "guest" # Python: guest
# Осторожно: 0 тоже ложь, и настоящее значение будет потеряно.0 or 100 # Python: 100Как записать условие и тернарный оператор
else if сокращается до elif. Тернарный оператор читается как обычная фраза: сначала значение, потом условие.
int score = 75;String grade;
if (score >= 90) { grade = "A";} else if (score >= 60) { grade = "B";} else { grade = "C";}System.out.println(grade); // B
String result = score >= 60 ? "pass" : "fail";System.out.println(result); // passscore = 75
if score >= 90: grade = "A"elif score >= 60: grade = "B"else: grade = "C"print(grade) # B
# значение_если_да if условие else значение_если_нетresult = "pass" if score >= 60 else "fail"print(result) # passЧем заменить switch
Вместо него есть match, он появился в Python 3.10. Как и новый switch в Java, он сравнивает не только с константами, но и с образцами. Проваливания из ветки в ветку нет, поэтому break не нужен.
String httpName(int code) { return switch (code) { case 200 -> "OK"; case 301, 302 -> "Redirect"; case 404 -> "Not Found"; default -> "Unknown"; };}
System.out.println(httpName(302)); // RedirectSystem.out.println(httpName(500)); // Unknowndef http_name(code): match code: case 200: return "OK"
# Вертикальная черта значит «или». case 301 | 302: return "Redirect" case 404: return "Not Found"
# Подчёркивание подходит подо всё, это default. case _: return "Unknown"
print(http_name(302)) # Redirectprint(http_name(500)) # UnknownСопоставление по типу работает так же, как pattern matching в Java 21, только образец умеет заодно разбирать значение на части. Примеры с dataclass и словарями будут дальше.
String describe(Object value) { return switch (value) { case Integer n when n > 100 -> "big number"; case Integer n -> "number " + n; case String s -> "text " + s; case List<?> l -> "list of " + l.size(); default -> "other"; };}
System.out.println(describe(500)); // big numberSystem.out.println(describe(List.of(1, 2))); // list of 2def describe(value): match value:
# int(n) проверяет тип и кладёт значение в n, if — это when. case int(n) if n > 100: return "big number" case int(n): return f"number {n}" case str(s): return f"text {s}" case list(items): return f"list of {len(items)}" case _: return "other"
print(describe(500)) # big numberprint(describe([1, 2])) # list of 2
# А так образец разбирает значение: список ровно из двух элементов.match ["move", 10]: case ["move", distance]: print(distance) # 10Как написать цикл for
В Python есть только один вид for, это аналог for (x : items) из Java. Там, где в Java нужен счётчик, используется range: функция, которая выдаёт числа по порядку.
var names = List.of("Ann", "Bob");
for (String name : names) { System.out.print(name + " "); // Ann Bob}
for (int i = 0; i < 3; i++) { System.out.print(i + " "); // 0 1 2}
for (int i = 10; i > 0; i -= 3) { System.out.print(i + " "); // 10 7 4 1}names = ["Ann", "Bob"]
for name in names: print(name) # Ann Bob
# range(3) выдаёт 0, 1, 2. Правая граница не входит.for i in range(3): print(i) # 0 1 2
# range(от, до, шаг)for i in range(10, 0, -3): print(i) # 10 7 4 1Индекс нужен реже, чем кажется. Для частых случаев есть готовые функции, а for i in range(len(names)) считается плохим стилем.
var names = List.of("Ann", "Bob");var ages = List.of(30, 25);
// Значение и индексfor (int i = 0; i < names.size(); i++) { System.out.print(i + ":" + names.get(i) + " "); // 0:Ann 1:Bob}
// Два списка параллельноfor (int i = 0; i < names.size(); i++) { System.out.print(names.get(i) + "=" + ages.get(i) + " "); // Ann=30 Bob=25}
// В обратном порядкеfor (int i = names.size() - 1; i >= 0; i--) { System.out.print(names.get(i) + " "); // Bob Ann}names = ["Ann", "Bob"]ages = [30, 25]
# enumerate выдаёт пары (индекс, значение).for i, name in enumerate(names): print(f"{i}:{name}") # 0:Ann 1:Bob
# zip идёт по двум спискам сразу и выдаёт пары.for name, age in zip(names, ages): print(f"{name}={age}") # Ann=30 Bob=25
for name in reversed(names): print(name) # Bob AnnКак написать while и выйти из цикла
while, break и continue работают так же, как в Java. Цикла do-while нет, и меток для выхода из вложенных циклов тоже нет.
int n = 0;while (n < 10) { n++; if (n % 2 == 0) { continue; } if (n > 5) { break; } System.out.print(n + " "); // 1 3 5}
int tries = 0;do { tries++;} while (tries < 3);System.out.println(tries); // 3n = 0while n < 10: n += 1 if n % 2 == 0: continue if n > 5: break print(n) # 1 3 5
# do-while записывают как вечный цикл с проверкой в конце.tries = 0while True: tries += 1 if tries >= 3: breakprint(tries) # 3У цикла может быть ветка else, которой нет в Java. Она выполняется, если цикл дошёл до конца и не вышел через break. Обычно это заменяет флаг found.
var ids = List.of(1, 2, 3);boolean found = false;
for (int id : ids) { if (id == 7) { found = true; break; }}if (!found) { System.out.println("not found"); // not found}ids = [1, 2, 3]
for id_ in ids: if id_ == 7: breakelse:
# Сюда попадаем, только если break не сработал. print("not found") # not foundОператор := присваивает значение прямо внутри выражения. В Java присваивание и так является выражением, а в Python для этого нужен отдельный оператор.
var names = List.of("Ann", "Bob");int count;
if ((count = names.size()) > 1) { System.out.println(count); // 2}names = ["Ann", "Bob"]
# Посчитали и тут же проверили. Обычное = здесь дало бы SyntaxError.if (count := len(names)) > 1: print(count) # 2Как объявить функцию
В Java метод всегда лежит в классе. В Python функция может лежать прямо в файле, поэтому классы со статическими методами здесь не нужны. Тип результата и параметров можно указать подсказками, но это необязательно.
int add(int a, int b) { return a + b;}
void greet(String name) { System.out.println("Hi, " + name); // Hi, Ann}
System.out.println(add(2, 3)); // 5greet("Ann");def add(a, b): return a + b
# То же с подсказками типов. None здесь означает void.def greet(name: str) -> None: print(f"Hi, {name}") # Hi, Ann
print(add(2, 3)) # 5greet("Ann")
# Функция без return на самом деле возвращает None.print(greet("Ann")) # NoneЧем заменить перегрузку методов
Перегрузки в Python нет: второй def с тем же именем просто заменит первый. Вместо неё используют значения параметров по умолчанию. При вызове аргументы можно называть по имени, что заменяет и паттерн Builder.
String connect(String host, int port, boolean secure) { return host + ":" + port + " " + secure;}
String connect(String host, int port) { return connect(host, port, false);}
String connect(String host) { return connect(host, 80);}
System.out.println(connect("a")); // a:80 falseSystem.out.println(connect("a", 443, true)); // a:443 truedef connect(host, port=80, secure=False): return f"{host}:{port} {secure}"
print(connect("a")) # a:80 False
# По порядку, как в Java.print(connect("a", 443, True)) # a:443 True
# По имени: порт пропущен, и понятно, что значит True.print(connect("a", secure=True)) # a:80 TrueКак передать произвольное число аргументов
int... numbers из Java записывается как *numbers. У Python есть и то, чего нет в Java: **options собирает именованные аргументы в словарь.
int sum(int... numbers) { return Arrays.stream(numbers).sum();}
System.out.println(sum(1, 2, 3)); // 6
int[] values = {1, 2, 3};System.out.println(sum(values)); // 6# В numbers приходит кортеж (1, 2, 3). Кортеж — это неизменяемый список.def total(*numbers): return sum(numbers)
print(total(1, 2, 3)) # 6
# Звёздочка при вызове, наоборот, раскладывает список на аргументы.values = [1, 2, 3]print(total(*values)) # 6
# В options приходит словарь {"port": 80, "secure": True}.def connect(host, **options): print(host, options) # a {'port': 80, 'secure': True}
connect("a", port=80, secure=True)Звёздочкой в списке параметров можно запретить передачу по порядку. Всё, что стоит после неё, можно передать только по имени. Так делают для флагов, чтобы не появлялись вызовы вроде send(msg, True, False).
def send(message, *, retry=False): return f"{message} {retry}"
print(send("hi", retry=True)) # hi True
# TypeError: так передавать нельзя.# send("hi", True)Как вернуть несколько значений
В Java для этого нужен отдельный класс или record. В Python функция просто возвращает несколько значений через запятую, и они упаковываются в кортеж.
record MinMax(int min, int max) {}
MinMax minMax(List<Integer> items) { return new MinMax(Collections.min(items), Collections.max(items));}
var result = minMax(List.of(3, 1, 2));System.out.println(result.min() + " " + result.max()); // 1 3def min_max(items): return min(items), max(items)
# Кортеж сразу раскладывается по двум переменным.low, high = min_max([3, 1, 2])print(low, high) # 1 3
# Лишнее значение принято складывать в переменную _ .low, _ = min_max([3, 1, 2])
# Звёздочка забирает «всё остальное».head, *rest = [1, 2, 3]print(head, rest) # 1 [2, 3]Как записать лямбду и ссылку на метод
Функция в Python — обычный объект: её можно положить в переменную и передать в другую функцию. Поэтому функциональные интерфейсы вроде Function и Comparator не нужны, а вместо .apply() функцию просто вызывают. Лямбда может содержать только одно выражение.
Function<Integer, Integer> twice = x -> x * 2; # Javatwice = lambda x: x * 2 # Python
twice.apply(4) # Java: 8twice(4) # Python: 8
# Ссылка на метод — это просто имя функции без скобок.Function<String, Integer> size = String::length; # Javasize = len # Python
size.apply("Ann") # Java: 3size("Ann") # Python: 3
# Несколько параметров.BinaryOperator<Integer> plus = (a, b) -> a + b; # Javaplus = lambda a, b: a + b # Python
plus.apply(2, 3) # Java: 5plus(2, 3) # Python: 5Самое частое применение лямбды — ключ сортировки. Вместо Comparator, который сравнивает два объекта, в Python передают функцию, которая по одному объекту возвращает то, по чему сортировать.
var names = new ArrayList<>(List.of("Bob", "Al", "Cindy")); # Javanames = ["Bob", "Al", "Cindy"] # Python
names.sort(Comparator.comparingInt(String::length)); # Javanames.sort(key=len) # Python
names # Java: [Al, Bob, Cindy]names # Python: ['Al', 'Bob', 'Cindy']
names.sort(Comparator.comparing(s -> s.charAt(s.length() - 1))); # Javanames.sort(key=lambda s: s[-1]) # Python
names # Java: [Bob, Al, Cindy]names # Python: ['Bob', 'Al', 'Cindy']Как объявить класс
Конструктор называется __init__. Поля заранее не объявляются: они появляются, когда им присваивают значение в конструкторе. Слова new нет, объект создаётся вызовом класса. Первым параметром каждого метода идёт self. Это то же самое, что this в Java, только записывается явно.
class User { private String name; private int age;
User(String name, int age) { this.name = name; this.age = age; }
String greet() { return "Hi, " + this.name; }}
User user = new User("Ann", 30);System.out.println(user.greet()); // Hi, Annclass User: def __init__(self, name, age): self.name = name self.age = age
# self при вызове передавать не нужно, Python подставит его сам. def greet(self): return f"Hi, {self.name}"
user = User("Ann", 30)print(user.greet()) # Hi, Ann
# Точно то же самое, только self передан вручную.print(User.greet(user)) # Hi, AnnМетод с двойными подчёркиваниями по бокам, такой как __init__, называют dunder-методом (сокращение от double underscore). Его не вызывают напрямую: его вызывает сам Python, когда нужно создать объект, напечатать его или сравнить. Дальше таких методов будет много.
Как сделать статические поля и методы
Переменная, объявленная в теле класса, принадлежит классу, как static поле в Java. Для методов есть два декоратора. @staticmethod — это обычный статический метод. @classmethod получает первым параметром сам класс, и на нём обычно делают фабричные методы. Декоратор — это пометка над функцией, похожая на аннотацию в Java. Как он работает, рассказано ниже.
class Money { static final String CURRENCY = "USD"; int cents;
Money(int cents) { this.cents = cents; }
static boolean isValid(int cents) { return cents >= 0; }
static Money ofDollars(int dollars) { return new Money(dollars * 100); }}
System.out.println(Money.CURRENCY); // USDSystem.out.println(Money.isValid(-1)); // falseSystem.out.println(Money.ofDollars(2).cents); // 200class Money: CURRENCY = "USD"
def __init__(self, cents): self.cents = cents
@staticmethod def is_valid(cents): return cents >= 0
# cls — это сам класс Money. У наследника здесь будет наследник. @classmethod def of_dollars(cls, dollars): return cls(dollars * 100)
print(Money.CURRENCY) # USDprint(Money.is_valid(-1)) # Falseprint(Money.of_dollars(2).cents) # 200Как сделать поле закрытым и что вместо геттеров
Модификаторов доступа в Python нет. Одно подчёркивание в начале имени значит «это внутреннее, снаружи не трогайте», но запрета нет, это соглашение.
| Java | Python | Что значит |
|---|---|---|
public int size | size | открытое |
protected int size | _size | внутреннее, по соглашению |
private int size | _size | то же самое, отдельного уровня нет |
Геттеры и сеттеры заранее не пишут, поле просто делают открытым. Если позже понадобится проверка, поле заменяют на @property: снаружи код остаётся тем же, account.balance, но вызывается метод.
class Account { private int balance;
int getBalance() { return balance; }
void setBalance(int balance) { if (balance < 0) { throw new IllegalArgumentException("negative"); } this.balance = balance; }}
var account = new Account();account.setBalance(100);System.out.println(account.getBalance()); // 100class Account: def __init__(self): self._balance = 0
# Геттер: читается как поле, account.balance, без скобок. @property def balance(self): return self._balance
# Сеттер: срабатывает на account.balance = 100. @balance.setter def balance(self, value): if value < 0: raise ValueError("negative") self._balance = value
account = Account()account.balance = 100print(account.balance) # 100Чем заменить toString, equals и hashCode
Это тоже dunder-методы. Есть и отличие от Java: строковых представлений два. __str__ — для пользователя, его использует print. __repr__ — для разработчика, его показывают отладчик и контейнеры. Если объявлен только __repr__, он работает и за __str__, поэтому обычно пишут его.
class Point { int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public String toString() { return "Point(" + x + ", " + y + ")"; }
@Override public boolean equals(Object other) { return other instanceof Point p && p.x == x && p.y == y; }
@Override public int hashCode() { return Objects.hash(x, y); }}
System.out.println(new Point(1, 2)); // Point(1, 2)System.out.println(new Point(1, 2).equals(new Point(1, 2))); // trueclass Point: def __init__(self, x, y): self.x = x self.y = y
def __repr__(self): return f"Point({self.x}, {self.y})"
# NotImplemented значит «я не умею сравнивать с этим типом». def __eq__(self, other): if not isinstance(other, Point): return NotImplemented return (self.x, self.y) == (other.x, other.y)
def __hash__(self): return hash((self.x, self.y))
print(Point(1, 2)) # Point(1, 2)print(Point(1, 2) == Point(1, 2)) # TrueЧем заменить record
Предыдущий класс почти никто так не пишет: для него есть dataclass. Этот декоратор сам создаёт __init__, __repr__ и __eq__ по списку полей. С параметром frozen=True объект становится неизменяемым, как record, и получает __hash__.
record Point(int x, int y) { public Point { if (x < 0) { throw new IllegalArgumentException("x"); } }
Point withX(int newX) { return new Point(newX, y); }}
var point = new Point(1, 2);System.out.println(point); // Point[x=1, y=2]System.out.println(point.x()); // 1System.out.println(point.withX(5)); // Point[x=5, y=2]from dataclasses import dataclass, field, replace
# Здесь подсказки типов обязательны: по ним декоратор находит поля.@dataclass(frozen=True)class Point: x: int y: int = 0
# Компактный конструктор record: вызывается после __init__. def __post_init__(self): if self.x < 0: raise ValueError("x")
point = Point(1, 2)print(point) # Point(x=1, y=2)print(point.x) # 1
# replace делает копию с изменённым полем, это аналог withX.print(replace(point, x=5)) # Point(x=5, y=2)
# point.x = 5 дал бы FrozenInstanceError.
# Изменяемое значение по умолчанию задают через field: список на объект свой.@dataclassclass Cart: items: list = field(default_factory=list)
print(Cart()) # Cart(items=[])Как наследовать класс
Родитель указывается в скобках. Вызов родительского конструктора не обязателен и сам не происходит, поэтому super().__init__() легко забыть. Все методы можно переопределять, слова @Override нет. Классов-родителей может быть несколько, об этом ниже.
class Animal { String name;
Animal(String name) { this.name = name; }
String speak() { return name + " makes a sound"; }}
class Dog extends Animal { Dog(String name) { super(name); }
@Override String speak() { return super.speak() + ": woof"; }}
Animal pet = new Dog("Rex");System.out.println(pet.speak()); // Rex makes a sound: woofSystem.out.println(pet instanceof Animal); // trueclass Animal: def __init__(self, name): self.name = name
def speak(self): return f"{self.name} makes a sound"
class Dog(Animal): def __init__(self, name): super().__init__(name)
def speak(self): return f"{super().speak()}: woof"
pet = Dog("Rex")print(pet.speak()) # Rex makes a sound: woofprint(isinstance(pet, Animal)) # TrueЧем заменить абстрактный класс и интерфейс
Абстрактный класс объявляется через ABC. Создать его экземпляр нельзя, а наследник обязан реализовать абстрактные методы. Отличие от Java в том, что ошибка появится не при компиляции, а при создании объекта.
abstract class Shape { abstract double area();
String describe() { return "area " + area(); }}
class Square extends Shape { double side = 2;
@Override double area() { return side * side; }}
System.out.println(new Square().describe()); // area 4.0from abc import ABC, abstractmethod
class Shape(ABC): @abstractmethod def area(self): pass
def describe(self): return f"area {self.area()}"
class Square(Shape): side = 2.0
def area(self): return self.side * self.side
print(Square().describe()) # area 4.0
# Shape() дал бы TypeError: абстрактный класс создать нельзя.Интерфейсу ближе всего Protocol. Он отличается важной вещью: классу не нужно писать implements. Подходит любой класс, у которого есть такие же методы. Это называют утиной типизацией: если объект умеет то, что нужно, его тип не важен. Проверяет соответствие не Python, а mypy.
interface Notifier { String send(String text);}
class Email implements Notifier { public String send(String text) { return "email: " + text; }}
String alert(Notifier notifier) { return notifier.send("down");}
System.out.println(alert(new Email())); // email: downfrom typing import Protocol
class Notifier(Protocol): def send(self, text: str) -> str: ...
# Ни implements, ни наследования: совпадения методов достаточно.class Email: def send(self, text: str) -> str: return f"email: {text}"
def alert(notifier: Notifier) -> str: return notifier.send("down")
print(alert(Email())) # email: downКак объявить enum
Перечисление — это класс-наследник Enum. Как и в Java, каждый элемент существует в одном экземпляре, поэтому их можно сравнивать через is. Если значения — строки, удобен StrEnum: его элементы и есть строки.
enum Status { NEW("new"), PAID("paid");
final String code;
Status(String code) { this.code = code; }}
System.out.println(Status.PAID.name()); // PAIDSystem.out.println(Status.PAID.code); // paidSystem.out.println(Status.valueOf("NEW").code); // newSystem.out.println(Status.values().length); // 2from enum import Enum, StrEnum
class Status(Enum): NEW = "new" PAID = "paid"
print(Status.PAID.name) # PAIDprint(Status.PAID.value) # paid
# Найти по имени и по значению.print(Status["NEW"].value) # newprint(Status("paid").name) # PAIDprint(len(Status)) # 2print(Status.PAID is Status("paid")) # True
# Элемент StrEnum равен строке, это удобно для JSON и базы.class Color(StrEnum): RED = "red"
print(Color.RED == "red") # TrueЧем заменить анонимные и вложенные классы
Анонимных классов нет, и обычно они не нужны, потому что можно передать функцию. Вложенный класс бывает, но он похож на static class в Java: доступа к объекту внешнего класса у него нет.
Runnable task = new Runnable() { @Override public void run() { System.out.println("run"); // run }};task.run();def run_task(task): task()
# Вместо анонимного класса — функция, объявленная там, где нужна.def task(): print("run") # run
run_task(task)Как написать дженерик
Подсказка типа коллекции пишется в квадратных скобках. Свои обобщённые классы с Python 3.12 объявляются так же, как в Java, только скобки другие. Как и в Java, во время работы от параметра типа ничего не остаётся. Подробнее в разделе о подсказках типов.
List<String> names = new ArrayList<>(); # Javanames: list[str] = [] # Python
Map<String, Integer> ages = new HashMap<>(); # Javaages: dict[str, int] = {} # Python
# Значение или null.String nickname = null; # Javanickname: str | None = None # Pythonclass Box<T> { private final T value;
Box(T value) { this.value = value; }
T get() { return value; }}
<T extends Comparable<T>> T maxOf(T a, T b) { return a.compareTo(b) > 0 ? a : b;}
System.out.println(new Box<>("x").get()); // xSystem.out.println(maxOf(1, 2)); // 2class Box[T]: def __init__(self, value: T) -> None: self._value = value
def get(self) -> T: return self._value
# T: int | str — это ограничение, как T extends в Java.def max_of[T: int | str](a: T, b: T) -> T: return a if a > b else b
print(Box("x").get()) # xprint(max_of(1, 2)) # 2Как обрабатывать исключения
catch называется except, а throw называется raise. Проверяемых исключений нет: слова throws не существует, и перехватывать ничего не обязательно.
int parse(String text) { try { return Integer.parseInt(text); } catch (NumberFormatException | NullPointerException e) { System.out.println("bad: " + text); // bad: abc return 0; } finally { System.out.println("done"); // done }}
parse("abc");def parse(text): try: return int(text)
# Несколько типов перечисляются в скобках. except (ValueError, TypeError) as e: print(f"bad: {text}") # bad: abc return 0 finally: print("done") # done
parse("abc")Своё исключение объявляют как класс. Причина, cause из Java, указывается через from. Ещё есть ветка else: она выполняется, только если в try не было ошибки.
class OrderException extends RuntimeException { OrderException(String message, Throwable cause) { super(message, cause); }}
try { try { Integer.parseInt("abc"); } catch (NumberFormatException e) { throw new OrderException("bad id", e); }} catch (OrderException e) { System.out.println(e.getMessage()); // bad id System.out.println(e.getCause() instanceof NumberFormatException); // true}class OrderError(Exception): pass
try: try: int("abc") except ValueError as e: raise OrderError("bad id") from eexcept OrderError as e: print(e) # bad id
# __cause__ — то же, что getCause(). print(isinstance(e.__cause__, ValueError)) # True
try: value = int("42")except ValueError: print("bad")else:
# Только если ошибки не было. print(value) # 42Названия стандартных исключений другие, но соответствие простое:
| Java | Python |
|---|---|
Throwable | BaseException |
Exception | Exception |
IllegalArgumentException, NumberFormatException | ValueError |
ClassCastException | TypeError |
NullPointerException | AttributeError или TypeError |
IndexOutOfBoundsException | IndexError |
нет ключа в Map | KeyError |
IOException | OSError |
FileNotFoundException | FileNotFoundError |
ArithmeticException | ZeroDivisionError |
UnsupportedOperationException | NotImplementedError |
StackOverflowError | RecursionError |
TimeoutException | TimeoutError |
Чем заменить try-with-resources
Вместо него есть оператор with. Объект для него называют менеджером контекста. Вместо метода close из AutoCloseable у него два метода: __enter__ вызывается на входе, а __exit__ на выходе, даже при ошибке. Порядок выполнения показан цифрами.
class Resource implements AutoCloseable { Resource() { System.out.println("open"); // 1. open }
@Override public void close() { System.out.println("close"); // 3. close }}
try (var resource = new Resource()) { System.out.println("work"); // 2. work}class Resource: def __enter__(self): print("open") # 1. open
# То, что возвращает __enter__, попадает в переменную после as. return self
def __exit__(self, exc_type, exc, tb): print("close") # 3. close
with Resource() as resource: print("work") # 2. workЧаще всего with встречается при работе с файлами:
Path path = Files.createTempFile("demo", ".txt");
try (var writer = Files.newBufferedWriter(path)) { writer.write("hello");}
System.out.println(Files.readString(path)); // helloimport tempfilefrom pathlib import Path
path = Path(tempfile.mkdtemp()) / "demo.txt"
# Файл закроется сам. Кодировку указывайте всегда: иначе берётся системная.with open(path, "w", encoding="utf-8") as file: file.write("hello")
print(path.read_text(encoding="utf-8")) # helloКак устроены импорты и пакеты
Строки package в Python нет: файл сам является модулем, а его имя равно имени файла. Каталог с модулями называется пакетом. В один файл можно положить сколько угодно классов и функций, а отдельный файл на каждый класс здесь не приняты. Импортировать можно как сам модуль, так и отдельное имя из него.
# Импорт одного имени.import java.time.LocalDate; # Javafrom datetime import date # Python
# Статический импорт.import static java.lang.Math.sqrt; # Javafrom math import sqrt # Python
# Импорт всего подряд. В Python так делать не принято.import java.util.*; # Javafrom math import * # Python
# Только в Python: весь модуль, а потом обращение через его имя.import math # Pythonmath.floor(2.5) # Python: 2
# Только в Python: своё короткое имя.import datetime as dt # Pythondt.date(2026, 1, 2).year # Python: 2026orders/ пакет, как package orders├── __init__.py признак пакета, обычно пустой├── domain.py модуль orders.domain└── api/ ├── __init__.py └── routes.py модуль orders.api.routes# Из другого файла проекта.from orders.domain import Order
# Из соседнего файла того же пакета: точка значит «этот пакет».from .domain import Order
# На уровень выше.from ..domain import OrderШпаргалка по синтаксису
| Java | Python |
|---|---|
int a = 1; | a = 1 |
final int MAX = 1; | MAX = 1 |
null | None |
true, false | True, False |
a.equals(b) | a == b |
a == b для ссылок | a is b |
&&, ||, ! | and, or, not |
x > 0 ? a : b | a if x > 0 else b |
7 / 2 | 7 // 2 |
Math.pow(2, 3) | 2 ** 3 |
i++ | i += 1 |
else if | elif |
switch | match |
for (int i = 0; i < n; i++) | for i in range(n): |
for (T x : items) | for x in items: |
do { } while (x); | while True: и break |
void f(int x) | def f(x): |
int... xs | *xs |
x -> x * 2 | lambda x: x * 2 |
String::length | len |
new User("Ann") | User("Ann") |
this | self |
class B extends A | class B(A): |
super.run() | super().run() |
static int f() | @staticmethod |
record Point(int x) | @dataclass(frozen=True) |
interface | Protocol |
abstract class | class A(ABC): |
List<String> | list[str] |
throw | raise |
catch (E e) | except E as e: |
try (var r = ...) | with ... as r: |
toString() | __repr__, __str__ |
equals(), hashCode() | __eq__, __hash__ |
System.out.println(x) | print(x) |
Числа: основные операции
Чем целые числа отличаются от Java
int в Python не переполняется, он растёт, пока хватает памяти. Деление с минусом тоже работает иначе: Java отбрасывает дробную часть, а Python округляет вниз.
Integer.MAX_VALUE + 1 # Java: -21474836482147483647 + 1 # Python: 2147483648
BigInteger.TWO.pow(70) # Java: 11805916207174113034242 ** 70 # Python: 1180591620717411303424
# Деление с минусом: результаты разные.-7 / 2 # Java: -3-7 // 2 # Python: -4
-7 % 3 # Java: -1-7 % 3 # Python: 2
# Подчёркивания в числах, двоичная и шестнадцатеричная запись — как в Java.1_000_000 # Java: 10000001_000_000 # Python: 1000000
0xFF # Java: 2550xFF # Python: 255
0b101 # Java: 50b101 # Python: 5
# Перевод в другую систему счисления. Python добавляет префикс.Integer.toBinaryString(10) # Java: 1010bin(10) # Python: 0b1010
Integer.toHexString(255) # Java: ffhex(255) # Python: 0xff
Integer.parseInt("ff", 16) # Java: 255int("ff", 16) # Python: 255
# Битовые операции те же, только >>> нет: у числа нет фиксированной длины.5 & 3 # Java: 15 & 3 # Python: 1
1 << 4 # Java: 161 << 4 # Python: 16Где искать Math
Самые частые функции встроены в язык и доступны без импорта, а остальные лежат в модуле math.
import math # Python
Math.abs(-5) # Java: 5abs(-5) # Python: 5
Math.max(1, 2) # Java: 2max(1, 2) # Python: 2
# min и max в Python принимают и несколько значений, и список.Collections.min(List.of(3, 1)) # Java: 1min(3, 1) # Python: 1
# floor и ceil в Python возвращают целое, а не дробное.Math.floor(2.7) # Java: 2.0math.floor(2.7) # Python: 2
Math.ceil(2.1) # Java: 3.0math.ceil(2.1) # Python: 3
Math.sqrt(16) # Java: 4.0math.sqrt(16) # Python: 4.0
Math.PI # Java: 3.141592653589793math.pi # Python: 3.141592653589793
# Ловушка: round в Python округляет половину к чётному.Math.round(2.5) # Java: 3round(2.5) # Python: 2
Math.round(3.5) # Java: 4round(3.5) # Python: 4
# Округлить до двух знаков.Math.round(3.14159 * 100) / 100.0 # Java: 3.14round(3.14159, 2) # Python: 3.14
# Частное и остаток сразу.divmod(7, 2) # Python: (3, 1)
Double.isNaN(0.0 / 0) # Java: truemath.isnan(float("nan")) # Python: True
# В Java деление дробного на ноль даёт Infinity, в Python ZeroDivisionError.1.0 / 0 # Java: Infinityfloat("inf") # Python: infКак считать деньги
float в Python — это тот же double, с теми же ошибками округления. Для денег есть Decimal, аналог BigDecimal. Создавать его нужно из строки. Обычные операторы работают, поэтому add и multiply не нужны.
from decimal import Decimal, ROUND_HALF_UP # Python
0.1 + 0.2 # Java: 0.300000000000000040.1 + 0.2 # Python: 0.30000000000000004
new BigDecimal("0.1").add(new BigDecimal("0.2")) # Java: 0.3Decimal("0.1") + Decimal("0.2") # Python: 0.3
new BigDecimal("19.99").multiply(new BigDecimal("3")) # Java: 59.97Decimal("19.99") * 3 # Python: 59.97
# Округлить до копеек.new BigDecimal("2.675").setScale(2, RoundingMode.HALF_UP) # Java: 2.68Decimal("2.675").quantize(Decimal("0.01"), ROUND_HALF_UP) # Python: 2.68
# Сравнение: в Java equals учитывает масштаб, в Python == только значение.new BigDecimal("1.0").equals(new BigDecimal("1.00")) # Java: falseDecimal("1.0") == Decimal("1.00") # Python: TrueКак получить случайное число
Обратите внимание: у randint включены обе границы. Модуль random не подходит для паролей и токенов, для них есть secrets, как SecureRandom в Java.
import random # Pythonimport secrets # Python
# Кубик: от 1 до 6.int dice = ThreadLocalRandom.current().nextInt(1, 7); # Javadice = random.randint(1, 6) # Python
# Дробное от 0 до 1.double share = Math.random(); # Javashare = random.random() # Python
# Случайный элемент и перемешивание.var cards = new ArrayList<>(List.of(1, 2, 3)); # Javacards = [1, 2, 3] # Python
Collections.shuffle(cards); # Javarandom.shuffle(cards) # Python
int card = cards.get(new Random().nextInt(cards.size())); # Javacard = random.choice(cards) # Python
# Токен для безопасности.var token = new java.security.SecureRandom().nextLong(); # Javatoken = secrets.token_hex(16) # PythonСтроки: основные методы
Как записать строку
Как и String в Java, str неизменяема и хранит текст в Unicode. Одинарные и двойные кавычки означают одно и то же. Кроме обычной строки есть несколько видов литералов: буква перед кавычкой меняет смысл.
| Запись | Что это | Аналог в Java |
|---|---|---|
"text", 'text' | обычная строка | "text" |
"""text""" | многострочная строка | """text""" |
f"{name}" | строка с подстановкой значений | String.format |
r"\d+" | сырая строка: обратная косая черта не экранирует | нет |
b"abc" | байты, а не текст | byte[] |
t"{name}" | шаблон: значения отдельно от текста (3.14) | нет |
Буквы можно совмещать: rf"\d{count}" или rb"\x00".
"it's" # Java: it's'it\'s' # Python: it's
# Удобно, когда внутри есть кавычки другого вида."say \"hi\"" # Java: say "hi"'say "hi"' # Python: say "hi"
# Сырая строка экономит обратные косые черты в регулярных выражениях."\\d+" # Java: \d+r"\d+" # Python: \d+
# Символ Unicode по коду."é" # Java: é"é" # Python: éМногострочная строка записывается так же, как текстовый блок в Java, но отступы Python сам не убирает. Для этого есть textwrap.dedent.
String query = """ SELECT * FROM users """;System.out.print(query);from textwrap import dedent
# Обратная косая черта после кавычек убирает первый перевод строки.query = dedent("""\ SELECT * FROM users """)print(query)У соседних литералов есть ловушка. Python склеивает их без всякого плюса. Это удобно для длинных строк, но если забыть запятую в списке, ошибки не будет.
message = ( "first part, " "second part")print(message) # first part, second part
# Забыта запятая: в списке два элемента, а не три.names = [ "Ann", "Bob" "Cid",]print(names) # ['Ann', 'BobCid']Какие методы есть у строки
Здесь собраны те методы, что нужны каждый день. Строка неизменяема, поэтому все они, как и в Java, возвращают новую строку.
String s = "Hello World"; # Javas = "Hello World" # Python
# Длина — функция, а не метод.s.length() # Java: 11len(s) # Python: 11
s.charAt(0) # Java: Hs[0] # Python: H
# Отрицательный индекс считает с конца.s.charAt(s.length() - 1) # Java: ds[-1] # Python: d
s.toUpperCase() # Java: HELLO WORLDs.upper() # Python: HELLO WORLD
s.toLowerCase() # Java: hello worlds.lower() # Python: hello world
s.contains("World") # Java: true"World" in s # Python: True
s.startsWith("He") # Java: trues.startswith("He") # Python: True
s.endsWith("ld") # Java: trues.endswith("ld") # Python: True
s.indexOf("o") # Java: 4s.find("o") # Python: 4
s.lastIndexOf("o") # Java: 7s.rfind("o") # Python: 7
# Не нашли: -1. Метод s.index() в этом случае даст ValueError.s.indexOf("z") # Java: -1s.find("z") # Python: -1
s.replace("World", "Ann") # Java: Hello Anns.replace("World", "Ann") # Python: Hello Ann
# Заменить только первое вхождение.s.replaceFirst("l", "L") # Java: HeLlo Worlds.replace("l", "L", 1) # Python: HeLlo World
# Сколько раз встречается. Готового метода в Java нет.s.chars().filter(c -> c == 'l').count() # Java: 3s.count("l") # Python: 3# Пробелы по краям." a ".strip() # Java: a" a ".strip() # Python: a
" a".stripLeading() # Java: a" a".lstrip() # Python: a
"a ".stripTrailing() # Java: a"a ".rstrip() # Python: a
# В Python strip умеет убирать и заданные символы."--a--".strip("-") # Python: a
# Убрать приставку, только если она есть."v1.2".removeprefix("v") # Python: 1.2
"ab".repeat(2) # Java: abab"ab" * 2 # Python: abab
# Перевернуть строку.new StringBuilder("abc").reverse() # Java: cba"abc"[::-1] # Python: cba
# Пустая ли строка."".isEmpty() # Java: truenot "" # Python: True
" ".isBlank() # Java: truenot " ".strip() # Python: True
# Проверки символов. В Python они работают и для всей строки.Character.isDigit('5') # Java: true"5".isdigit() # Python: True
Character.isLetter('a') # Java: true"a".isalpha() # Python: True
"123".chars().allMatch(Character::isDigit) # Java: true"123".isdigit() # Python: True
# Первая буква заглавная."ann".substring(0, 1).toUpperCase() + "ann".substring(1) # Java: Ann"ann".capitalize() # Python: Ann
# Сравнение: compareTo в Python заменяют обычные операторы."a".compareTo("b") < 0 # Java: true"a" < "b" # Python: True
"ANN".equalsIgnoreCase("ann") # Java: true"ANN".casefold() == "ann".casefold() # Python: TrueЧто такое срез строки
Вместо substring используется срез, записывается он как s[начало:конец:шаг]. Правая граница не входит, как и в substring. Любую часть можно пропустить. Индексы удобно представлять как номера под буквами. Так же срезы работают со списками.
строка: H e l l o индекс: 0 1 2 3 4 с конца: -5 -4 -3 -2 -1String s = "Hello"; # Javas = "Hello" # Python
s.substring(1, 3) # Java: els[1:3] # Python: el
# Без начала: от первого символа.s.substring(0, 2) # Java: Hes[:2] # Python: He
# Без конца: до последнего символа.s.substring(2) # Java: llos[2:] # Python: llo
# Последние три символа.s.substring(s.length() - 3) # Java: llos[-3:] # Python: llo
# Каждый второй символ. В Java это делается циклом.s[::2] # Python: Hlo
# В отличие от substring, выход за границы не даёт ошибки.s[3:100] # Python: loКак разбить и склеить строку
Здесь больше всего различий. split в Java принимает регулярное выражение и выбрасывает пустые строки в конце, а split в Python ничего не выбрасывает и ищет обычный текст. Без аргументов он делит по любым пробелам. join в Python вызывают у разделителя.
Arrays.toString("a,b,,".split(",")) # Java: [a, b]"a,b,,".split(",") # Python: ['a', 'b', '', '']
# В Java точка — регулярное выражение, а в Python обычный символ.Arrays.toString("a.b".split("\\.")) # Java: [a, b]"a.b".split(".") # Python: ['a', 'b']
Arrays.toString(" a b ".trim().split("\\s+")) # Java: [a, b]" a b ".split() # Python: ['a', 'b']
# Ограничить число частей. В Python считаются разрезы, а не части.Arrays.toString("a=b=c".split("=", 2)) # Java: [a, b=c]"a=b=c".split("=", 1) # Python: ['a', 'b=c']
# По строкам."a\nb".lines().toList() # Java: [a, b]"a\nb".splitlines() # Python: ['a', 'b']
# Разделить на три части: до, разделитель, после."key=value".partition("=") # Python: ('key', '=', 'value')
String.join(", ", List.of("a", "b")) # Java: a, b", ".join(["a", "b"]) # Python: a, b
# В Python join принимает только строки, числа надо преобразовать.", ".join(str(n) for n in [1, 2]) # Python: 1, 2
# Перебрать символы и получить их список."abc".toCharArray().length # Java: 3list("abc") # Python: ['a', 'b', 'c']Как подставить значения в строку
Основной способ — f-строка: буква f перед кавычкой и любые выражения в фигурных скобках. После двоеточия идёт формат, очень похожий на тот, что в String.format.
String name = "Ann"; # Javaname = "Ann" # Python
int n = 42; # Javan = 42 # Python
double x = 3.14159; # Javax = 3.14159 # Python
String.format("%s is %d", name, n) # Java: Ann is 42f"{name} is {n}" # Python: Ann is 42
# В скобках можно писать любое выражение.String.format("%d", n * 2) # Java: 84f"{n * 2}" # Python: 84
# Знаки после запятой.String.format(Locale.US, "%.2f", x) # Java: 3.14f"{x:.2f}" # Python: 3.14
# Ширина и нули.String.format("%05d", n) # Java: 00042f"{n:05d}" # Python: 00042
# Выравнивание: > вправо, < влево, ^ по центру.String.format("[%5s]", name) # Java: [ Ann]f"[{name:>5}]" # Python: [ Ann]
String.format("[%-5s]", name) # Java: [Ann ]f"[{name:<5}]" # Python: [Ann ]
f"[{name:^5}]" # Python: [ Ann ]
# Разделитель тысяч.String.format(Locale.US, "%,d", 1234567) # Java: 1,234,567f"{1234567:,}" # Python: 1,234,567
# Шестнадцатеричная запись и проценты.String.format("%x", 255) # Java: fff"{255:x}" # Python: ff
f"{0.25:.0%}" # Python: 25%
# Только в Python: знак = печатает и выражение, и значение. Удобно для отладки.f"{n=}" # Python: n=42
# Фигурная скобка как символ пишется дважды.f"{{{n}}}" # Python: {42}В старом коде встречаются ещё два способа, о них нужно знать, чтобы читать такой код. Формат % ещё и живёт в логировании: log.info("user %s", name). Так сообщение собирается, только если запись реально попадёт в лог, как и с {} в SLF4J. Поэтому f-строки в вызовах логгера не используют.
name = "Ann"
# Как printf. Самый старый способ.print("Hi, %s" % name) # Hi, Ann
# Как MessageFormat. Нужен, когда шаблон задан заранее, например в настройках.print("Hi, {}".format(name)) # Hi, Annprint("Hi, {who}".format(who=name)) # Hi, AnnЧто такое t-строка
f-строка сразу склеивает всё в обычный текст, поэтому подставить в неё данные от пользователя в SQL или HTML опасно: так появляются инъекции. t-строка появилась в Python 3.14. Она не склеивает ничего, а отдаёт текст и значения по отдельности, и вы сами решаете, как их соединить.
name = "x' OR '1'='1"
# f-строка: данные стали частью SQL.print(f"name = '{name}'") # name = 'x' OR '1'='1'
# t-строка: это объект, а не строка.template = t"name = {name}"print(template.strings) # ('name = ', '')print(template.values) # ("x' OR '1'='1",)
# Текст идёт в запрос, значения отдельно, как в PreparedStatement.def to_sql(query): return "?".join(query.strings), query.values
print(to_sql(template)) # ('name = ?', ("x' OR '1'='1",))Как собрать строку из частей
Строка неизменяема, поэтому склейка в цикле каждый раз создаёт новую строку. Как и в Java, на большом объёме это медленно. Отдельного StringBuilder в Python нет: части складывают в список и один раз склеивают через join.
var builder = new StringBuilder();for (int i = 0; i < 3; i++) { builder.append(i).append(",");}System.out.println(builder); // 0,1,2,parts = []for i in range(3): parts.append(f"{i},")print("".join(parts)) # 0,1,2,
# Если текст собирается как поток, есть io.StringIO, это StringWriter.import io
buffer = io.StringIO()buffer.write("a")buffer.write("b")print(buffer.getvalue()) # abЧем __str__ отличается от __repr__ при печати
Разница хорошо видна на самой строке. Функция str даёт текст как есть, а repr показывает, как значение записано в коде: с кавычками и управляющими символами. Контейнеры печатают свои элементы через repr, поэтому в списке строки видны с кавычками.
text = "a\tb"
print(str(text)) # a bprint(repr(text)) # 'a\tb'
# В f-строке !r значит «вызови repr». Удобно в сообщениях об ошибках.print(f"bad value: {text!r}") # bad value: 'a\tb'print(["a", "b"]) # ['a', 'b']Чем текст отличается от байтов
str хранит текст, bytes хранит байты. Python их никогда не смешивает: сложить их нельзя, а переход всегда идёт явно, через кодировку. Для работы с байтами есть три типа.
| Python | Изменяемый | Что это | Аналог в Java |
|---|---|---|---|
bytes | нет | неизменяемые байты | byte[], который никто не меняет |
bytearray | да | изменяемый буфер | byte[] |
memoryview | как у источника | окно в чужие байты без копирования | ByteBuffer.wrap |
import base64 # Python
byte[] data = "héllo".getBytes(StandardCharsets.UTF_8); # Javadata = "héllo".encode("utf-8") # Python
new String(data, StandardCharsets.UTF_8) # Java: héllodata.decode("utf-8") # Python: héllo
# Длина в байтах: é занимает два.data.length # Java: 6len(data) # Python: 6
# Байт: в Java он со знаком, а в Python от 0 до 255.data[1] # Java: -61data[1] # Python: 195
HexFormat.of().formatHex(data) # Java: 68c3a96c6c6fdata.hex() # Python: 68c3a96c6c6f
Base64.getEncoder().encodeToString("hi".getBytes()) # Java: aGk=base64.b64encode(b"hi").decode() # Python: aGk=data = b"abc"
# Печатается с буквой b, чтобы текст и байты не путались.print(data) # b'abc'
# "abc" + b"abc" даст TypeError.
# bytes нельзя менять, bytearray можно.buffer = bytearray(b"abc")buffer[0] = 65print(buffer) # bytearray(b'Abc')
# memoryview смотрит на те же байты: запись видна в источнике.view = memoryview(buffer)view[1:2] = b"B"print(buffer) # bytearray(b'ABc')Чем длина строки отличается от Java
String в Java считает единицы UTF-16, поэтому эмодзи занимает два char. В Python len считает символы Unicode. Сам Python выбирает, сколько байт отвести на символ, поэтому индекс всегда работает быстро. Но один видимый символ всё равно может состоять из нескольких символов Unicode, и перед сравнением такой текст нормализуют.
import unicodedata # Python
"😀".length() # Java: 2len("😀") # Python: 1
"😀".codePointCount(0, 2) # Java: 1len("😀") # Python: 1
# é как один символ и как e плюс знак ударения."é".equals("é") # Java: false"é" == "é" # Python: False
Normalizer.normalize("é", Normalizer.Form.NFC).equals("é") # Java: trueunicodedata.normalize("NFC", "é") == "é" # Python: TrueКак работать с регулярными выражениями
Синтаксис выражений почти тот же, а методы лежат в модуле re. Шаблон принято писать сырой строкой. Если совпадения нет, методы возвращают None.
import re # Python
# Подходит ли вся строка."123".matches("\\d+") # Java: truebool(re.fullmatch(r"\d+", "123")) # Python: True
"a1b2".replaceAll("\\d", "#") # Java: a#b#re.sub(r"\d", "#", "a1b2") # Python: a#b#
Arrays.toString("a1b2c".split("\\d")) # Java: [a, b, c]re.split(r"\d", "a1b2c") # Python: ['a', 'b', 'c']
# Все совпадения.var found = Pattern.compile("\\d").matcher("a1b2").results(); # Javafound.map(r -> r.group()).toList() # Java: [1, 2]re.findall(r"\d", "a1b2") # Python: ['1', '2']Pattern pattern = Pattern.compile("(?<key>\\w+)=(?<value>\\d+)");Matcher matcher = pattern.matcher("size=42");
if (matcher.find()) { System.out.println(matcher.group("key")); // size System.out.println(matcher.group("value")); // 42}import re
# Заранее скомпилированный шаблон, как Pattern. Имя группы: ?P<имя>.pattern = re.compile(r"(?P<key>\w+)=(?P<value>\d+)")
# search ищет в любом месте строки, как find().if match := pattern.search("size=42"): print(match["key"]) # size print(match["value"]) # 42Коллекции: основные методы
Какие коллекции чему соответствуют
Основные коллекции встроены в язык и имеют собственный синтаксис, поэтому new и импорты для них не нужны. Интерфейса List с разными реализациями тоже нет: есть один list, и внутри это динамический массив, как ArrayList.
| Java | Python | Запись |
|---|---|---|
ArrayList | list | [1, 2] |
List.of | tuple | (1, 2) |
LinkedHashMap | dict | {"a": 1} |
HashSet | set | {1, 2} |
Set.of | frozenset | frozenset({1, 2}) |
ArrayDeque, LinkedList | collections.deque | deque([1, 2]) |
PriorityQueue | heapq | функции над списком |
int[] | list или array.array | [0] * 3 |
TreeMap | нет, обычно sorted(d) | |
ConcurrentHashMap | dict и Lock |
Как работать со списком
var items = new ArrayList<>(List.of(3, 1, 2)); # Javaitems = [3, 1, 2] # Python
items.size() # Java: 3len(items) # Python: 3
items.get(0) # Java: 3items[0] # Python: 3
# Последний элемент.items.getLast() # Java: 2items[-1] # Python: 2
items.set(0, 9); # Javaitems[0] = 9 # Python
items.add(4); # Javaitems.append(4) # Python
# Вставить по индексу.items.add(0, 7); # Javaitems.insert(0, 7) # Python
items.addAll(List.of(5, 6)); # Javaitems.extend([5, 6]) # Python
items # Java: [7, 9, 1, 2, 4, 5, 6]items # Python: [7, 9, 1, 2, 4, 5, 6]
# Удалить по индексу и получить значение.items.remove(0) # Java: 7items.pop(0) # Python: 7
# Удалить по значению.items.remove(Integer.valueOf(9)); # Javaitems.remove(9) # Python
items.contains(1) # Java: true1 in items # Python: True
# Если значения нет, index даёт ValueError, а не -1.items.indexOf(2) # Java: 1items.index(2) # Python: 1
items.isEmpty() # Java: falsenot items # Python: False
Collections.max(items) # Java: 6max(items) # Python: 6
items.stream().mapToInt(i -> i).sum() # Java: 18sum(items) # Python: 18
# Перевернуть на месте.Collections.reverse(items); # Javaitems.reverse() # Python
items # Java: [6, 5, 4, 2, 1]items # Python: [6, 5, 4, 2, 1]
# Часть списка. В Java это вид на исходный список, в Python копия.items.subList(0, 2) # Java: [6, 5]items[:2] # Python: [6, 5]
# Копия всего списка.var copy = new ArrayList<>(items); # Javacopy = items.copy() # Python
# Склейка и повторение.[1] + [2] # Python: [1, 2][0] * 3 # Python: [0, 0, 0]
items.clear(); # Javaitems.clear() # PythonСрез можно использовать и слева от присваивания, а умножение таит ловушку на вложенных списках.
items = [1, 2, 3, 4]
# Заменить часть списка.items[1:3] = [20, 30]print(items) # [1, 20, 30, 4]
# Удалить часть списка.del items[1:3]print(items) # [1, 4]
# Ловушка: две ссылки на один и тот же внутренний список.grid = [[0] * 2] * 2grid[0][0] = 1print(grid) # [[1, 0], [1, 0]]
# Правильно: каждый внутренний список создаётся отдельно.grid = [[0] * 2 for _ in range(2)]grid[0][0] = 1print(grid) # [[1, 0], [0, 0]]Что такое кортеж
tuple — это неизменяемый список, ближайший аналог List.of. Его используют для фиксированного набора значений, таких как координаты или строка из базы. А ещё его можно сделать ключом словаря, а список нельзя.
var point = List.of(10, 20); # Javapoint = (10, 20) # Python
point.get(0) # Java: 10point[0] # Python: 10
# point.set(0, 1) в Java и point[0] = 1 в Python дадут ошибку.
# Кортеж из одного элемента: запятая обязательна.var single = List.of(1); # Javasingle = (1,) # Python
# Без запятой это просто число в скобках.type((1)).__name__ # Python: inttype((1,)).__name__ # Python: tuple
# Раскладывание по переменным.var x = point.get(0); # Javax, y = point # Python
x # Java: 10y # Python: 20
# Кортежи сравниваются поэлементно.(1, 2) < (1, 3) # Python: TrueКак работать со словарём
dict — это Map. Он хранит порядок добавления, как LinkedHashMap. Самое важное отличие от Java: ages["Bob"] при отсутствии ключа даёт ошибку KeyError, а не null. Поведение как у get в Java даёт метод get.
var ages = new LinkedHashMap<String, Integer>(); # Javaages = {} # Python
ages.put("Ann", 30); # Javaages["Ann"] = 30 # Python
ages.put("Bob", 25); # Javaages["Bob"] = 25 # Python
ages.get("Ann") # Java: 30ages["Ann"] # Python: 30
# Ключа нет.ages.get("Cid") # Java: nullages.get("Cid") # Python: None
ages.getOrDefault("Cid", 0) # Java: 0ages.get("Cid", 0) # Python: 0
ages.containsKey("Ann") # Java: true"Ann" in ages # Python: True
ages.containsValue(25) # Java: true25 in ages.values() # Python: True
ages.size() # Java: 2len(ages) # Python: 2
ages.keySet() # Java: [Ann, Bob]list(ages) # Python: ['Ann', 'Bob']
ages.values() # Java: [30, 25]list(ages.values()) # Python: [30, 25]
# Записать, только если ключа ещё нет.ages.putIfAbsent("Cid", 40); # Javaages.setdefault("Cid", 40) # Python: 40
# Удалить и получить значение.ages.remove("Cid") # Java: 40ages.pop("Cid") # Python: 40
# Удалить, а если ключа нет, вернуть значение по умолчанию.ages.pop("Cid", None) # Python: None
# Ключи по порядку, вместо TreeMap.new TreeMap<>(ages).firstKey() # Java: Annsorted(ages)[0] # Python: AnnПеребор записывается через items, он отдаёт пары ключ и значение. Он заменяет entrySet.
var ages = new LinkedHashMap<>(Map.of("Ann", 30));
for (var entry : ages.entrySet()) { System.out.print(entry.getKey() + "=" + entry.getValue()); // Ann=30}
for (String name : ages.keySet()) { System.out.print(name); // Ann}
ages.forEach((name, age) -> System.out.print(name + age));ages = {"Ann": 30}
for name, age in ages.items(): print(f"{name}={age}") # Ann=30
# Перебор самого словаря идёт по ключам.for name in ages: print(name) # Ann
# Объединение: при совпадении ключей побеждает правый.print({"a": 1} | {"a": 2, "b": 3}) # {'a': 2, 'b': 3}
# Две звёздочки разворачивают словарь внутри другого.print({**{"a": 1}, "b": 2}) # {'a': 1, 'b': 2}Ключом может быть только неизменяемое значение: строка, число, кортеж, замороженный dataclass. Формально нужен метод __hash__, это тот же hashCode. Список дал бы ошибку TypeError: unhashable type.
Как накапливать значения по ключу
В Java для этого есть computeIfAbsent и merge. В Python есть setdefault и два специальных словаря. defaultdict сам создаёт значение для нового ключа. Counter считает, сколько раз встречается каждый элемент.
var byCity = new TreeMap<String, List<String>>();byCity.computeIfAbsent("Kyiv", k -> new ArrayList<>()).add("Ann");byCity.computeIfAbsent("Kyiv", k -> new ArrayList<>()).add("Bob");System.out.println(byCity); // {Kyiv=[Ann, Bob]}
var counts = new TreeMap<String, Integer>();for (String word : List.of("a", "b", "a")) { counts.merge(word, 1, Integer::sum);}System.out.println(counts); // {a=2, b=1}from collections import Counter, defaultdict
# list здесь — функция, которая создаёт значение для нового ключа.by_city = defaultdict(list)by_city["Kyiv"].append("Ann")by_city["Kyiv"].append("Bob")print(dict(by_city)) # {'Kyiv': ['Ann', 'Bob']}
counts = Counter(["a", "b", "a"])print(dict(counts)) # {'a': 2, 'b': 1}print(counts["a"]) # 2
# Самые частые элементы: топ-1.print(counts.most_common(1)) # [('a', 2)]Как работать с множеством
Методы похожи на Java, а вместо retainAll и removeAll есть обычные операторы. Они возвращают новое множество и не меняют исходные.
var seen = new TreeSet<>(List.of(1, 2)); # Javaseen = {1, 2} # Python
seen.add(3); # Javaseen.add(3) # Python
seen.contains(3) # Java: true3 in seen # Python: True
seen.remove(3); # Javaseen.remove(3) # Python
# В Python remove даст KeyError, если значения нет. discard промолчит.seen.discard(100) # Python
# Пустое множество: {} создаст словарь.var empty = new HashSet<Integer>(); # Javaempty = set() # Python
# Объединение, пересечение, разность.{1, 2} | {2, 3} # Python: {1, 2, 3}{1, 2} & {2, 3} # Python: {2}{1, 2} - {2, 3} # Python: {1}
# Входят ли все элементы, как containsAll.seen.containsAll(List.of(1)) # Java: true{1} <= seen # Python: TrueКак сделать очередь и приоритетную очередь
list.pop(0) работает за O(n), потому что сдвигает весь массив, поэтому для очереди используют deque. Приоритетная очередь сделана не классом, а функциями из модуля heapq. Они работают с обычным списком, а наверху всегда лежит минимальный элемент.
from collections import deque # Pythonimport heapq # Python
var queue = new ArrayDeque<>(List.of(1, 2)); # Javaqueue = deque([1, 2]) # Python
queue.addLast(3); # Javaqueue.append(3) # Python
queue.addFirst(0); # Javaqueue.appendleft(0) # Python
queue.pollFirst() # Java: 0queue.popleft() # Python: 0
queue.pollLast() # Java: 3queue.pop() # Python: 3
# Посмотреть, не удаляя.queue.peekFirst() # Java: 1queue[0] # Python: 1
# Приоритетная очередь.var tasks = new PriorityQueue<>(List.of(3, 1)); # Javatasks = [1, 3] # Python
tasks.add(2); # Javaheapq.heappush(tasks, 2) # Python
tasks.poll() # Java: 1heapq.heappop(tasks) # Python: 1
# С приоритетом: кортеж сравнивается по первому элементу.jobs = [(2, "later"), (1, "urgent")]; heapq.heapify(jobs) # Pythonheapq.heappop(jobs) # Python: (1, 'urgent')
# N самых больших сразу.heapq.nlargest(2, [1, 5, 3]) # Python: [5, 3]Как сортировать
list.sort() меняет сам список, а sorted() возвращает новый и принимает любую коллекцию. Правила сортировки задаёт функция key. Для сортировки по нескольким полям она просто возвращает кортеж.
record User(String name, int age) {}
var users = new ArrayList<>(List.of( new User("Ann", 30), new User("Bob", 25), new User("Cid", 30)));
// По убыванию возраста, затем по имени.users.sort( Comparator.comparingInt(User::age).reversed() .thenComparing(User::name));
System.out.println(users.getFirst().name()); // Annfrom dataclasses import dataclass
@dataclassclass User: name: str age: int
users = [ User("Ann", 30), User("Bob", 25), User("Cid", 30),]
# По убыванию возраста, затем по имени. Минус переворачивает порядок для чисел.users.sort(key=lambda u: (-u.age, u.name))print(users[0].name) # Ann
# sorted не меняет исходный список, а reverse=True переворачивает порядок.print(sorted([2, 3, 1], reverse=True)) # [3, 2, 1]print(sorted("bca")) # ['a', 'b', 'c']Что будет, если менять коллекцию при переборе
В Java это даёт ConcurrentModificationException. У словаря и множества в Python поведение такое же, а вот список молча пропускает элементы.
items = [1, 2, 2, 3]
# Ошибки нет, но вторая двойка осталась.for item in items: if item == 2: items.remove(item)print(items) # [1, 2, 3]
# Правильно: собрать новый список. Запись объясняется в следующем разделе.items = [1, 2, 2, 3]items = [item for item in items if item != 2]print(items) # [1, 3]
ages = {"a": 1}try: for key in ages: ages["b"] = 2except RuntimeError as e: print(e) # dictionary changed size during iterationКакова сложность операций
Значения те же, что у соответствующих коллекций в Java.
| Операция | list | dict, set | deque |
|---|---|---|---|
| Чтение по индексу или ключу | O(1) | O(1) | O(n) |
x in c | O(n) | O(1) | O(n) |
| Добавить в конец | O(1) | O(1) | O(1) |
| Добавить или убрать в начале | O(n) | – | O(1) |
sorted | O(n log n) | O(n log n) | O(n log n) |
Stream API: что вместо него
Что такое списковое включение
Вместо цепочки filter и map в Python есть особая запись, которая называется списковым включением (list comprehension). Читается она как обычная фраза: что положить, откуда взять, при каком условии. Эта запись заменяет большую часть Stream API.
numbers = [1, 2, 3, 4]
# [ что положить откуда взять условие ]squares = [n * n for n in numbers if n % 2 == 0]print(squares) # [4, 16]
# То же самое обычным циклом.squares = []for n in numbers: if n % 2 == 0: squares.append(n * n)print(squares) # [4, 16]Чем заменить операции со стримами
Основные операции с результатом видны сразу. Обратите внимание: комбинация map и filter записана одним выражением, без отдельных вызовов.
import functools # Python
var items = List.of(3, 1, 2, 3); # Javaitems = [3, 1, 2, 3] # Python
# filteritems.stream().filter(i -> i > 1).toList() # Java: [3, 2, 3][i for i in items if i > 1] # Python: [3, 2, 3]
# mapitems.stream().map(i -> i * 2).toList() # Java: [6, 2, 4, 6][i * 2 for i in items] # Python: [6, 2, 4, 6]
# sum, count, min, maxitems.stream().mapToInt(i -> i).sum() # Java: 9sum(items) # Python: 9
items.stream().filter(i -> i > 1).count() # Java: 3sum(1 for i in items if i > 1) # Python: 3
items.stream().max(Integer::compare).get() # Java: 3max(items) # Python: 3
# Среднее значение.items.stream().mapToInt(i -> i).average().getAsDouble() # Java: 2.25sum(items) / len(items) # Python: 2.25
# anyMatch, allMatch, noneMatchitems.stream().anyMatch(i -> i > 2) # Java: trueany(i > 2 for i in items) # Python: True
items.stream().allMatch(i -> i > 2) # Java: falseall(i > 2 for i in items) # Python: False
items.stream().noneMatch(i -> i > 5) # Java: truenot any(i > 5 for i in items) # Python: True
# findFirstitems.stream().filter(i -> i < 3).findFirst().orElse(null) # Java: 1next((i for i in items if i < 3), None) # Python: 1
# sorted, limit, skipitems.stream().sorted().toList() # Java: [1, 2, 3, 3]sorted(items) # Python: [1, 2, 3, 3]
items.stream().limit(2).toList() # Java: [3, 1]items[:2] # Python: [3, 1]
items.stream().skip(2).toList() # Java: [2, 3]items[2:] # Python: [2, 3]
# distinct с сохранением порядка: у словаря ключи уникальны.items.stream().distinct().toList() # Java: [3, 1, 2]list(dict.fromkeys(items)) # Python: [3, 1, 2]
# Без порядка достаточно множества.items.stream().collect(Collectors.toCollection(TreeSet::new)) # Java: [1, 2, 3]{i for i in items} # Python: {1, 2, 3}
# flatMapStream.of(List.of(1), List.of(2)).flatMap(List::stream).toList() # Java: [1, 2][x for row in [[1], [2]] for x in row] # Python: [1, 2]
# joiningvar words = items.stream().map(String::valueOf); # Javawords.collect(Collectors.joining(",")) # Java: 3,1,2,3",".join(str(i) for i in items) # Python: 3,1,2,3
# reduce живёт в functools.items.stream().reduce(1, (a, b) -> a * b) # Java: 18functools.reduce(lambda a, b: a * b, items) # Python: 18Круглые скобки вместо квадратных в выражениях вроде sum(1 for i in items if i > 1) создают генератор. Он не строит список в памяти, а выдаёт значения по одному, как ленивый стрим. Подробнее об этом рассказано ниже.
Как собрать словарь и сгруппировать значения
Есть включения и для словаря, и для множества. А вот у groupingBy простого аналога нет: группировку обычно пишут циклом.
record User(String name, String city, int age) {}
var users = List.of( new User("Ann", "Kyiv", 30), new User("Bob", "Lviv", 25), new User("Cid", "Kyiv", 40));
var agesByName = users.stream() .collect(Collectors.toMap(User::name, User::age));System.out.println(agesByName.get("Ann")); // 30
var byCity = users.stream() .collect(Collectors.groupingBy(User::city));System.out.println(byCity.get("Kyiv").size()); // 2
var countByCity = users.stream() .collect(Collectors.groupingBy(User::city, Collectors.counting()));System.out.println(countByCity.get("Kyiv")); // 2
var adults = users.stream() .collect(Collectors.partitioningBy(u -> u.age() >= 30));System.out.println(adults.get(true).size()); // 2from collections import Counter, defaultdictfrom dataclasses import dataclass
@dataclassclass User: name: str city: str age: int
users = [ User("Ann", "Kyiv", 30), User("Bob", "Lviv", 25), User("Cid", "Kyiv", 40),]
# toMapages_by_name = {u.name: u.age for u in users}print(ages_by_name["Ann"]) # 30
# groupingByby_city = defaultdict(list)for user in users: by_city[user.city].append(user)print(len(by_city["Kyiv"])) # 2
# groupingBy + countingcount_by_city = Counter(u.city for u in users)print(count_by_city["Kyiv"]) # 2
# partitioningByadults = [u for u in users if u.age >= 30]print(len(adults)) # 2Чем заменить Optional
Optional в Python не используют. Отсутствие значения — это None, а тип записывают как User | None. Цепочки заменяются обычными проверками.
record User(String name) {}
Optional<User> findUser(int id) { return id == 1 ? Optional.of(new User("Ann")) : Optional.empty();}
String name = findUser(2) .map(User::name) .orElse("guest");System.out.println(name); // guestfrom dataclasses import dataclass
@dataclassclass User: name: str
def find_user(user_id: int) -> User | None: return User("Ann") if user_id == 1 else None
user = find_user(2)name = user.name if user is not None else "guest"print(name) # guestЧасто нужные классы стандартной библиотеки
Как работать с датой и временем
Вместо java.time есть модуль datetime. Важная деталь: datetime может быть с часовым поясом или без него. Без пояса это аналог LocalDateTime, с поясом — ZonedDateTime. Сравнить такие значения между собой нельзя, а вызов datetime.now() без аргументов отдаёт значение без пояса. В сервисах всегда пишите datetime.now(UTC).
from datetime import UTC, date, datetime, timedelta # Pythonfrom zoneinfo import ZoneInfo # Python
import java.time.format.DateTimeFormatter; # Javaimport java.time.*; # Java
var day = LocalDate.of(2026, 11, 6); # Javaday = date(2026, 11, 6) # Python
# Прибавить и вычесть: вместо plusDays — обычные операторы.day.plusDays(30) # Java: 2026-12-06day + timedelta(days=30) # Python: 2026-12-06
day.getDayOfWeek().getValue() # Java: 5day.isoweekday() # Python: 5
# Разница между датами.Duration.ofHours(36).toDays() # Java: 1timedelta(hours=36).days # Python: 1
# В строку и из строки.day.toString() # Java: 2026-11-06day.isoformat() # Python: 2026-11-06
LocalDate.parse("2026-01-02").getYear() # Java: 2026date.fromisoformat("2026-01-02").year # Python: 2026
day.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) # Java: 06.11.2026day.strftime("%d.%m.%Y") # Python: 06.11.2026
# Текущий момент с поясом.var now = ZonedDateTime.now(ZoneOffset.UTC); # Javanow = datetime.now(UTC) # Python
var kyiv = ZoneId.of("Europe/Kyiv"); # Javavar at = ZonedDateTime.of(2026, 1, 2, 12, 0, 0, 0, kyiv); # Javaat = datetime(2026, 1, 2, 12, tzinfo=ZoneInfo("Europe/Kyiv")) # Python
at.toEpochSecond() # Java: 1767348000int(at.timestamp()) # Python: 1767348000Как работать с файлами
Path и Files в Python объединены в одном классе pathlib.Path. Части пути соединяются оператором /.
import tempfile # Pythonfrom pathlib import Path # Python
Path dir = Files.createTempDirectory("demo"); # Javadir = Path(tempfile.mkdtemp()) # Python
Path file = dir.resolve("logs").resolve("a.txt"); # Javafile = dir / "logs" / "a.txt" # Python
file.getFileName() # Java: a.txtfile.name # Python: a.txt
file.getParent().getFileName() # Java: logsfile.parent.name # Python: logs
Files.exists(file) # Java: falsefile.exists() # Python: False
# Создать каталоги вместе с родителями.Files.createDirectories(file.getParent()); # Javafile.parent.mkdir(parents=True, exist_ok=True) # Python
Files.writeString(file, "one\ntwo\n"); # Javafile.write_text("one\ntwo\n", encoding="utf-8") # Python
Files.readAllLines(file) # Java: [one, two]file.read_text(encoding="utf-8").splitlines() # Python: ['one', 'two']
# Обойти каталог со всеми подкаталогами.Files.walk(dir).filter(Files::isRegularFile).count() # Java: 1sum(1 for f in dir.rglob("*.txt")) # Python: 1
Files.delete(file); # Javafile.unlink() # PythonБольшой файл читают построчно. Объект файла в Python можно перебирать в цикле, а в память при этом попадает одна строка за раз.
Path path = Files.createTempFile("big", ".txt");Files.writeString(path, "1\n2\n");
try (var lines = Files.lines(path)) { System.out.println(lines.mapToInt(Integer::parseInt).sum()); // 3}import tempfilefrom pathlib import Path
path = Path(tempfile.mkdtemp()) / "big.txt"path.write_text("1\n2\n", encoding="utf-8")
with open(path, encoding="utf-8") as file: print(sum(int(line) for line in file)) # 3Как работать с JSON
Модуль json входит в стандартную библиотеку. В отличие от Jackson, он работает только со словарями и списками, а в свои классы не превращает. Для этого в реальных проектах берут Pydantic. Объект dataclass можно превратить в словарь функцией asdict.
ObjectMapper mapper = new ObjectMapper();
String text = mapper.writeValueAsString(new User("Ann", 30));User user = mapper.readValue(text, User.class);Map<String, Object> raw = mapper.readValue(text, Map.class);import jsonfrom dataclasses import asdict, dataclass
@dataclassclass User: name: str age: int
# Объект в JSON: сначала словарь, потом строка.text = json.dumps(asdict(User("Ann", 30)))print(text) # {"name": "Ann", "age": 30}
# JSON в словарь.raw = json.loads(text)print(raw["name"]) # Ann
# Словарь в объект: две звёздочки раскладывают его на именованные аргументы.print(User(**raw)) # User(name='Ann', age=30)
# Типы вроде datetime и Decimal json не знает, нужна функция default.from decimal import Decimal
print(json.dumps({"sum": Decimal("1.5")}, default=str)) # {"sum": "1.5"}Как писать логи
Модуль logging — это SLF4J и Logback в одном флаконе. Логгер создают в каждом модуле, а настраивают один раз при запуске. Переменная __name__ содержит имя текущего модуля. Сообщения пишут через %s, как {} в SLF4J.
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
log.info("order {} paid", id);
try { pay(id);} catch (Exception e) { log.error("payment failed", e);}import logging
log = logging.getLogger(__name__)
# Настройка при запуске программы, вместо logback.xml.logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s",)
log.info("order %s paid", 42)
try: 1 / 0except ZeroDivisionError:
# exception пишет сообщение и весь стек, как log.error("...", e). log.exception("payment failed")Где искать остальное
import hashlib # Pythonimport os # Pythonimport sys # Pythonimport time # Pythonimport uuid # Python
# Переменная окружения или значение по умолчанию.String port = System.getenv().getOrDefault("NO_SUCH_VAR", "80"); # Javaport = os.environ.get("NO_SUCH_VAR", "80") # Python
# Аргументы командной строки. sys.argv[0] — имя файла.String[] args = new String[0]; # Javaargs = sys.argv[1:] # Python
# Измерить время.long start = System.nanoTime(); # Javastart = time.perf_counter() # Python
Thread.sleep(1); # Javatime.sleep(0.001) # Python
# Уникальный идентификатор.UUID.randomUUID().toString().length() # Java: 36len(str(uuid.uuid4())) # Python: 36
# Хэш SHA-256 первые 8 символов.var sha = java.security.MessageDigest.getInstance("SHA-256"); # JavaHexFormat.of().formatHex(sha.digest(new byte[0])).substring(0, 8) # Java: e3b0c442hashlib.sha256(b"").hexdigest()[:8] # Python: e3b0c442| Java | Python |
|---|---|
System.exit(1) | sys.exit(1) |
System.err.println(x) | print(x, file=sys.stderr) |
new Scanner(System.in) | input() |
HttpClient | urllib.request, на практике httpx |
URLEncoder.encode | urllib.parse.quote |
Properties | configparser, tomllib |
@Cacheable | functools.lru_cache |
Thread, ExecutorService | threading, concurrent.futures |
CompletableFuture | asyncio |
JUnit | unittest, на практике pytest |
Чем Python устроен иначе, чем Java
Как выполняется код
Основная реализация языка называется CPython. Как и javac с JVM, она компилирует исходник в байткод и выполняет его на виртуальной машине, только оба шага идут одной командой. Байткод кэшируется в каталоге __pycache__, как файлы .class. Главное отличие от JVM в том, что байткод обычно интерпретируется. JIT-компилятор, который на JVM превращает горячий код в машинный, в CPython только появляется и пока экспериментальный. Отсюда и разница в скорости.
| Java | Python | |
|---|---|---|
| Компиляция | javac заранее | при запуске |
| Ошибка типа видна | при компиляции | при выполнении строки |
| JIT | C2, Graal | экспериментальный |
| Старт программы | дольше | быстрее |
| Сборка мусора | трассирующая | счётчик ссылок и сборщик циклов |
| Параллельные потоки | да | обычно нет, из-за GIL |
Объявление класса или функции в Python — это обычная команда, которая выполняется, когда до неё дошла очередь. В Java классы загружаются JVM, а здесь файл просто идёт сверху вниз. Цифры показывают порядок.
print("start") # 1. start
# Здесь функция только создаётся. Тело не выполняется.def greet(): print("hi") # 4. hi
# А тело класса выполняется сразу.class Config: print("class body") # 2. class body
print("end") # 3. endgreet()Это объясняет многое из дальнейшего. Импорт файла тоже просто выполняет его сверху вниз, поэтому побочный код на верхнем уровне модуля сработает при первом импорте. Поэтому и нужна проверка if __name__ == "__main__".
Что такое переменная
Все переменные в Python устроены как ссылочные переменные в Java. Примитивов нет, поэтому переменная всегда хранит ссылку на объект, даже для числа. Присваивание b = a копирует только ссылку. Удобно представлять переменную как ярлык, приклеенный к объекту.
a = [1, 2] a ──► [1, 2]
b = a a ──► [1, 2] ◄── b
b.append(3) a ──► [1, 2, 3] ◄── b
b = [9] a ──► [1, 2, 3] b ──► [9]var a = new ArrayList<>(List.of(1, 2));var b = a;b.add(3);System.out.println(a); // [1, 2, 3]
b = new ArrayList<>(List.of(9));System.out.println(a); // [1, 2, 3]a = [1, 2]b = ab.append(3)print(a) # [1, 2, 3]
# Ярлык b перевешен на другой объект. Объект a не изменился.b = [9]print(a) # [1, 2, 3]
# id() — это адрес объекта, как System.identityHashCode.c = aprint(id(c) == id(a)) # TrueКак передаются аргументы в функцию
Так же, как объекты в Java: функция получает копию ссылки. Содержимое объекта она поменять может, а заменить переменную у вызывающего кода не может.
void change(List<Integer> items) { items.add(1); items = new ArrayList<>(); items.add(99);}
var data = new ArrayList<Integer>();change(data);System.out.println(data); // [1]def change(items):
# 1. Меняем общий объект: это видно снаружи. items.append(1)
# 2. Перевешиваем локальный ярлык: снаружи не видно. items = [] items.append(99)
data = []change(data)print(data) # [1]Какие типы изменяемые, а какие нет
| Неизменяемые | Изменяемые |
|---|---|
int, float, bool, None | list |
str, bytes | dict |
tuple, frozenset | set, bytearray |
@dataclass(frozen=True) | обычные объекты |
Отсюда важная ловушка с +=. Для изменяемого объекта он меняет сам объект, а для неизменяемого создаёт новый и перевешивает ярлык.
# Список: один объект на двоих, изменение видно через оба ярлыка.a = [1]b = aa += [2]print(b) # [1, 2]
# Строка: a теперь указывает на новую строку, а b на старую.a = "x"b = aa += "y"print(b) # xКак сделать копию объекта
list(x) или x.copy() — это поверхностная копия, как new ArrayList<>(x): копируется сам список, а вложенные объекты остаются общими. Модуль copy умеет копировать всё дерево объектов. Для этого в Java пришлось бы писать код или сериализовать объект.
import copy
original = [[1], [2]]
# Поверхностная копия: новый внешний список, те же внутренние.shallow = original.copy()shallow[0].append(9)print(original) # [[1, 9], [2]]
# Глубокая копия: новое всё.deep = copy.deepcopy(original)deep[0].append(7)print(original) # [[1, 9], [2]]Почему числа нельзя сравнивать через is
Это та же история, что с Integer в Java. Там кэшируются значения от -128 до 127, поэтому == иногда работает, а иногда нет. CPython тоже кэширует небольшие числа, и граница зависит от версии. Поэтому is используют только с None, True, False и элементами enum.
Integer small1 = 127, small2 = 127;System.out.println(small1 == small2); // true
Integer big1 = 1000, big2 = 1000;System.out.println(big1 == big2); // falseSystem.out.println(big1.equals(big2)); // truesmall1 = int("256")small2 = int("256")print(small1 is small2) # True
big1 = int("100000")big2 = int("100000")print(big1 is big2) # Falseprint(big1 == big2) # TrueГде видна переменная
В Java переменная живёт внутри своего блока в фигурных скобках. В Python блоков видимости нет: if и for их не создают. Границы проводят только функции, классы и модули. Переменная ищется по порядку в четырёх местах, этот порядок называют LEGB: Local (своя функция), Enclosing (внешняя функция), Global (файл), Built-in (встроенные имена).
for (int i = 0; i < 3; i++) { int square = i * i;}
// Ошибка компиляции: i и square здесь не существуют.// System.out.println(i);for i in range(3): square = i * i
# Обе переменные живы: цикл не создаёт своей области.print(i, square) # 2 4
# Исключение: переменная внутри включения снаружи не видна.squares = [n * n for n in range(3)]Чтение внешней переменной работает само. А вот присваивание внутри функции создаёт локальную переменную. Чтобы записать во внешнюю, нужно сказать об этом словом global или nonlocal.
count = 0
def broken():
# Присваивание ниже делает count локальной во всей функции, # поэтому чтение падает: UnboundLocalError. count += 1
def works(): global count count += 1
works()print(count) # 1
def counter(): total = 0
def add():
# nonlocal — переменная внешней функции. nonlocal total total += 1 return total
return add
add = counter()add()print(add()) # 2Что считается истинным в условии
Ложью считаются пустые и нулевые значения, остальное считается истиной. Свой класс управляет этим через методы __bool__ или __len__.
print(bool(0), bool(0.0), bool(""), bool(None)) # False False False Falseprint(bool([]), bool({}), bool(set()), bool(())) # False False False Falseprint(bool(1), bool("0"), bool([0])) # True True True
# bool — наследник int, поэтому True равно 1. Так считают совпадения.print(True + True) # 2print(sum(n > 1 for n in [1, 2, 3])) # 2Что значит «просить прощения, а не разрешения»
В Java принято сначала проверить, а потом делать. В Python чаще делают сразу и обрабатывают ошибку. Такой стиль называют EAFP (easier to ask forgiveness than permission). Он ещё и убирает гонку между проверкой и действием. Исключения здесь не считаются дорогой редкостью, ими пользуются обычно.
var config = new HashMap<>(Map.of("port", "80"));int port = 0;
if (config.containsKey("port")) { port = Integer.parseInt(config.get("port"));}System.out.println(port); // 80config = {"port": "80"}
try: port = int(config["port"])except (KeyError, ValueError): port = 0print(port) # 80Как свой класс встраивается в язык
В Java for работает с Iterable, а + работает только со строками и числами. В Python у всех операторов и встроенных функций есть соответствующий dunder-метод. Объявите его в своём классе, и оператор заработает. Этот набор правил называют моделью данных.
| Что пишете | Что вызывается | Аналог в Java |
|---|---|---|
Cart() | __init__ | конструктор |
len(x) | __len__ | size() |
x[i] | __getitem__ | get(i) |
x[i] = v | __setitem__ | set(i, v) |
v in x | __contains__ | contains(v) |
for v in x | __iter__ | iterator() |
a + b | __add__ | метод add |
a == b | __eq__ | equals |
a < b | __lt__ | compareTo |
hash(x) | __hash__ | hashCode |
str(x), repr(x) | __str__, __repr__ | toString |
x() | __call__ | метод функционального интерфейса |
with x: | __enter__, __exit__ | AutoCloseable |
class Cart implements Iterable<String> { private final List<String> items = new ArrayList<>();
void add(String item) { items.add(item); }
int size() { return items.size(); }
String get(int index) { return items.get(index); }
public Iterator<String> iterator() { return items.iterator(); }}
var cart = new Cart();cart.add("tea");System.out.println(cart.size()); // 1System.out.println(cart.get(0)); // teaclass Cart: def __init__(self): self._items = []
def add(self, item): self._items.append(item)
def __len__(self): return len(self._items)
def __getitem__(self, index): return self._items[index]
def __iter__(self): return iter(self._items)
def __contains__(self, item): return item in self._items
cart = Cart()cart.add("tea")
# Объект ведёт себя как встроенная коллекция.print(len(cart)) # 1print(cart[0]) # teaprint("tea" in cart) # Trueprint(list(cart)) # ['tea']
# Пустая корзина ложна, потому что len равна 0.print(bool(Cart())) # FalseТак же перегружают операторы. Сравнения удобно получить из одного метода: __lt__ заменяет Comparable, а декоратор total_ordering достроит <=, > и >=.
record Money(int cents) implements Comparable<Money> { Money add(Money other) { return new Money(cents + other.cents); }
public int compareTo(Money other) { return Integer.compare(cents, other.cents); }}
var a = new Money(100);var b = new Money(50);System.out.println(a.add(b)); // Money[cents=150]System.out.println(a.compareTo(b) > 0); // truefrom dataclasses import dataclassfrom functools import total_ordering
@total_ordering@dataclass(frozen=True)class Money: cents: int
def __add__(self, other): return Money(self.cents + other.cents)
def __lt__(self, other): return self.cents < other.cents
a = Money(100)b = Money(50)print(a + b) # Money(cents=150)print(a > b) # True
# min, max и sorted теперь работают сами.print(min(a, b)) # Money(cents=50)Функции: чего нет в Java
Почему нельзя писать список как значение по умолчанию
Значение по умолчанию вычисляется один раз, при объявлении функции, и хранится в ней. Если это список, все вызовы делят один и тот же объект. Это самая известная ловушка Python.
# 1. Здесь один раз создаётся список.def add_bad(item, items=[]): items.append(item) return items
# 2. Первый вызов кладёт в него 1.print(add_bad(1)) # [1]
# 3. Второй вызов получает тот же список.print(add_bad(2)) # [1, 2]
# Правильно: None по умолчанию, а новый список внутри.def add_good(item, items=None): if items is None: items = [] items.append(item) return items
print(add_good(1)) # [1]print(add_good(2)) # [2]Что такое замыкание
Замыкание — это функция, которая запомнила переменные из места, где была создана. 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.