18 малоизвестных возможностей Python для чистого кода

Можно годами писать на Python и не знать половины того, что язык умеет из коробки. Многое из этого — не экзотика, а вещи, которые делают код короче и понятнее: заглушки через ..., датаклассы вместо шаблонных классов, включения для словарей и множеств, объединение словарей одной операцией.
Ниже — 18 малоизвестных возможностей Python, сгруппированных по темам. Где возможность часто используют неправильно, я отдельно показываю, как правильно.
Заглушки: Ellipsis и NotImplemented
1. Ellipsis — троеточие как значение
... — это объект Ellipsis, полноценное значение в Python. Его используют как заглушку в теле функции, которое ещё не написано:
def export_report():
...
Это работает так же, как pass, но визуально сразу говорит: «здесь будет реализация». Ещё ... встречается в срезах numpy (array[..., 0]) и в аннотациях типов: tuple[int, ...] — кортеж любой длины из целых, Callable[..., str] — функция с любыми аргументами.
2. NotImplemented и NotImplementedError — не путайте
Это разные вещи. NotImplemented — специальное значение, которое возвращают методы сравнения и арифметики, когда не умеют работать с типом второго операнда. Python тогда пробует операцию с другой стороны:
class Money:
def __init__(self, value):
self.value = value
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.value == other.value
print(Money(1) == Money(1)) # True
print(Money(1) == 1) # False
А чтобы показать, что функция или метод ещё не реализованы, нужно выбрасывать исключение: raise NotImplementedError. Возвращать NotImplemented из обычной функции — ошибка: вызывающий код получит странное значение вместо понятного исключения.
Классы и данные
3. Датаклассы
Для классов, которые в основном хранят данные, есть @dataclass. Он сам генерирует __init__, __repr__ и __eq__:
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
print(User("Анна", 30)) # User(name='Анна', age=30)
print(User("Анна", 30) == User("Анна", 30)) # True
Получается аналог структур из других языков: меньше шаблонного кода, понятный вывод при отладке, сравнение «из коробки», а аннотации типов сразу документируют поля.
4. Модуль this
Выполните import this — и Python выведет «Дзен Python»: 19 принципов о том, как писать код. «Явное лучше неявного», «простое лучше сложного», «читаемость имеет значение». Это не шутка, а хороший ориентир при код-ревью.
Функции
5. Анонимные функции
lambda создаёт функцию без имени. Её можно сохранить в переменную, передать в другую функцию или вызвать сразу на месте:
print((lambda a, b: a + b)(2, 3)) # 5
users.sort(key=lambda user: user.age)
Лямбда живёт, пока на неё есть ссылки. Удобно для коротких ключей сортировки и обработчиков. Если логика длиннее одной строки, лучше обычная функция с именем.
6. Декораторы
Декоратор оборачивает функцию дополнительным поведением: логированием, замером времени, отправкой уведомления. Вместо того чтобы копировать одну и ту же строку в каждую функцию, вы пишете обёртку один раз:
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"call {func.__name__}{args}")
return func(*args, **kwargs)
return wrapper
@log_calls
def add(a, b):
return a + b
Не забывайте functools.wraps: без него у обёрнутой функции пропадут имя и документация. И обязательно возвращайте результат func(...) — иначе декоратор незаметно «сломает» функцию, и она начнёт возвращать None.
7. Несколько возвращаемых значений
Функция может вернуть несколько значений через запятую — на деле это кортеж, который удобно распаковать:
def stats(numbers):
return min(numbers), max(numbers), sum(numbers) / len(numbers)
low, high, average = stats([1, 2, 3])
8. Распаковка аргументов через * и **
Список можно передать как позиционные аргументы через *, а словарь — как именованные через **:
def connect(host, port):
return f"{host}:{port}"
connect(*["db", 5433])
connect(**{"host": "localhost", "port": 5432})
Соответственно, *args и **kwargs в объявлении функции собирают произвольные позиционные и именованные аргументы.
Включения и генераторы
9. Списковые включения
Цикл с append в три строки превращается в одну:
squares = [x * x for x in range(10)]
10. Включения для множеств и словарей
Тот же синтаксис работает с фигурными скобками: без двоеточия получается множество, с двоеточием — словарь:
remainders = {x % 3 for x in range(10)} # {0, 1, 2}
square_map = {x: x * x for x in range(4)} # {0: 0, 1: 1, 2: 4, 3: 9}
11. Генераторные выражения вместо «кортежных включений»
Круглые скобки создают не кортеж, а генератор — ленивую последовательность, которая выдаёт значения по одному:
gen = (x * x for x in range(10))
values = tuple(x * x for x in range(10)) # если нужен именно кортеж
12. Условия во включениях
После цикла можно добавить фильтр, в том числе составной:
evens = [x for x in range(10) if x % 2 == 0 and x > 2] # [4, 6, 8]
Мелочи, которые экономят строки
13. Обмен значений без временной переменной
a, b = 1, 2
a, b = b, a
14. Последний результат в REPL
В интерактивной оболочке Python переменная _ хранит результат последнего выражения: посчитали 2 * 2, затем _ + 2 даёт 6. Работает только в REPL и IPython — в скриптах _ обычная переменная.
15. Проверка версии интерпретатора
Если библиотека или синтаксис доступны только с определённой версии, проверяйте sys.version_info:
import sys
if sys.version_info >= (3, 9):
merged = defaults | overrides
else:
merged = {**defaults, **overrides}
Так можно подключить подходящую реализацию или показать понятное сообщение, а не падать с непонятной ошибкой.
16. Объединение словарей
Распаковка работает в любой современной версии, оператор | — начиная с Python 3.9. При совпадении ключей побеждает значение из правого словаря:
x = {"a": 1}
y = {"b": 2, "a": 3}
print({**x, **y}) # {'a': 3, 'b': 2}
print(x | y) # то же самое
17. Срезы с шагом
Срез [начало:конец:шаг] берёт элементы от начала до конца, не включая конец, с заданным шагом. Отрицательный шаг идёт в обратную сторону:
s = [1, 2, 3, 4, 5]
print(s[0:5:2]) # [1, 3, 5]
print(s[::-1]) # [5, 4, 3, 2, 1]
print("hello"[::-1]) # olleh
18. Удаление дубликатов и размер объекта
Самый короткий способ убрать дубли — множество: list(set(items)). Но множество не сохраняет порядок. Если порядок важен, используйте словарь — с Python 3.7 он сохраняет порядок вставки:
items = [3, 1, 3, 2, 1]
print(list(set(items))) # порядок не гарантирован
print(list(dict.fromkeys(items))) # [3, 1, 2]
А узнать, сколько памяти занимает объект, можно через sys.getsizeof(obj). Учтите, что он показывает только сам объект: для списка это массив ссылок без вложенных элементов.
Итог
Большинство этих возможностей не требует сторонних библиотек и сразу делает код короче. Главное — использовать их по назначению: NotImplemented только в методах сравнения, lambda только для коротких выражений, set для дублей только когда порядок не важен. О том, как подобные мелочи влияют на память, — в статье почему Python list может съесть всю память. А датаклассы, магические методы вроде __eq__ и декораторы подробно разобраны в моём курсе по Python ООП.