Все статьи

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

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 ООП.

Источники