Вступление

Python — язык, известный своей читаемостью и лаконичностью. В нём есть множество удобных конструкций, которые позволяют писать код короче и понятнее, не теряя при этом выразительности.

В этой статье я собрал 13 полезных возможностей Python, которые часто применяются на практике и помогают писать более чистый код. Материал будет особенно полезен начинающим разработчикам, которые только знакомятся с языком и хотят сразу освоить хорошие приёмы.

Дисклеймер

Эта статья ориентирована на тех, кто только начинает изучать Python или хочет вспомнить базовые, но часто полезные конструкции. Если вы уже давно пишете на Python, скорее всего, всё это вам знакомо — но, возможно, найдёте что-то забытое или посоветуете свои приёмы в комментариях.

1. List comprehension

Что это такое?

List comprehension (генератор списка) — это компактный и читаемый способ создания списков на основе существующих последовательностей или условий.

Как это выглядит:

numbers = [x * x for x in range(5)]
# → [0, 1, 4, 9, 16]

Когда стоит использовать:

  • Когда нужно создать новый список из существующего, применив к каждому элементу выражение.

  • Когда нужно отфильтровать данные в одну строку:

    even_numbers = [x for x in range(10) if x % 2 == 0]
    # → [0, 2, 4, 6, 8]
  • Когда важна читаемость и компактность, а логика преобразования проста.

Когда стоит остерегаться:

  • Если логика слишком сложная (например, несколько условий и вложенные циклы) — код может стать нечитаемым.

  • Для больших списков: comprehension создаёт результат целиком в памяти (если нужна ленивость — используйте генераторы).

2. Dict comprehension

Что это такое?

Dict comprehension (генератор словаря) позволяет создавать словари в одну строку, применяя преобразование или фильтрацию к существующим данным. Это аналог list comprehension, но для пар ключ → значение.

Как это выглядит:

data = {'a': 1, 'b': 2, 'c': 3}
squared = {k: v * v for k, v in data.items()}
# → {'a': 1, 'b': 4, 'c': 9}

Когда стоит использовать:

  • Когда нужно преобразовать значения словаря:

    prices = {'apple': 0.4, 'banana': 0.5}
    prices_in_cents = {k: int(v * 100) for k, v in prices.items()}
    # → {'apple': 40, 'banana': 50}
  • Когда нужно отфильтровать словарь:

    filtered = {k: v for k, v in data.items() if v > 1}
    # → {'b': 2, 'c': 3}
  • Когда важно сделать код короче и читаемее, чем при использовании цикла for.

Когда стоит остерегаться:

  • Если внутри comprehension сложная логика (например, вложенные условия или вызовы функций) — это ухудшит читаемость.

  • Если нужно выполнять сложную обработку, лучше использовать обычный цикл с промежуточными шагами и комментариями.

3. enumerate()

Что это такое?

Функция enumerate() добавляет к итерируемому объекту (списку, кортежу, строке и т.д.) автоматическую нумерацию элементов. Она возвращает пары (индекс, значение), что избавляет от необходимости вручную заводить счётчик.

Как это выглядит:

items = ['apple', 'banana', 'cherry']
for index, value in enumerate(items, start=1):
    print(index, value)
# Вывод:
# 1 apple
# 2 banana
# 3 cherry

Аргумент start=1 задаёт, с какого числа начинать счёт (по умолчанию — с 0).

Когда стоит использовать:

  • Когда нужно одновременно получать и индекс, и значение:

    for i, ch in enumerate("Python"):
        print(i, ch)
  • Когда нужно избавиться от ручного счётчика и сделать код компактнее.

  • Когда счёт нужен не с нуля, а с произвольного числа (например, для нумерации в UI или отчётах).

Когда стоит остерегаться:

  • Если нужен только индекс или только значение, enumerate() не даёт особых преимуществ (можно использовать range(len(...)) или обычный перебор).

  • Если перебираются очень большие последовательности, лишний объект-обёртка может быть минимально дороже по памяти, но на практике это редко важно.

4. zip

Что это такое?

Функция zip() объединяет несколько итерируемых объектов (списки, кортежи, строки и т.д.) в один. На выходе получается итератор, который на каждой итерации возвращает кортеж, состоящий из элементов с одинаковым индексом из каждого источника.

Как это выглядит:

names = ['Alice', 'Bob']
scores = [85, 92]
for name, score in zip(names, scores):
    print(f'{name}: {score}')
# Вывод:
# Alice: 85
# Bob: 92

Когда стоит использовать:

  • Когда нужно параллельно обрабатывать несколько последовательностей одинаковой длины:

    ids = [1, 2, 3]
    values = ['a', 'b', 'c']
    pairs = list(zip(ids, values))
    # → [(1, 'a'), (2, 'b'), (3, 'c')]
  • Когда нужно легко превратить несколько списков в структуру данных, например словарь:

    data = dict(zip(names, scores))
    # → {'Alice': 85, 'Bob': 92}
  • Когда читаемость важнее, чем использование индексов и ручных циклов.

Когда стоит остерегаться:

  • Если последовательности разной длины, zip() остановится на минимальной:

    list(zip([1, 2], ['a']))
    # → [(1, 'a')]  # «лишние» элементы отбрасываются
  • Если нужно «дотянуть» короткий список до длины длинного — используйте itertools.zip_longest().

  • Не забывайте, что в Python 3 zip() возвращает итератор, и чтобы получить список — нужно явно вызвать list().

5. map

Что это такое?

Функция map() применяет указанную функцию к каждому элементу итерируемого объекта (списка, кортежа, множества и т.д.) и возвращает итератор с результатами.
Это один из функциональных инструментов Python наряду с filter() и reduce().

Как это выглядит:

numbers = [1, 2, 3, 4, 5]
doubled = list(map(lambda number: number * 2, numbers))
print(doubled)
# → [2, 4, 6, 8, 10]

Когда стоит использовать:

  • Когда нужно одинаково обработать каждый элемент последовательности:

    names = ["Alice", "Bob"]
    upper_names = list(map(str.upper, names))
    # → ['ALICE', 'BOB']
  • Когда уже есть готовая функция, которую нужно применить ко всем элементам:

    def square(x): return x*x
    squared = list(map(square, numbers))
    # → [1, 4, 9, 16, 25]
  • В случаях, когда не требуется сложная логика в теле функции.

Когда стоит остерегаться:

  • Если код становится трудночитаемым из-за использования lambda внутри map — в таком случае list comprehension часто понятнее:

    # То же самое:
    doubled = [number * 2 for number in numbers]
  • map() возвращает итератор в Python 3, поэтому для повторного использования нужно привести к списку или другому контейнеру (list(), tuple() и т.д.).

  • При слишком сложных функциях внутри map лучше использовать обычный цикл для читаемости.

6. f-строки

Что это такое?

F-строки — это специальный синтаксис для форматирования строк, появившийся в Python 3.6. Позволяют вставлять переменные и выражения прямо в строку с помощью фигурных скобок {} и префикса f перед строкой.

Как это выглядит:

name = 'John'
age = 30
print(f'My name is {name} and I am {age} years old.')
# → My name is John and I am 30 years old.

Также можно вставлять выражения:

print(f'Next year I will be {age + 1}')
# → Next year I will be 31

Когда стоит использовать:

  • Для быстрого и читаемого форматирования текста с переменными.

  • Когда нужно форматировать числа:

    pi = 3.14159
    print(f'Pi rounded: {pi:.2f}')
    # → Pi rounded: 3.14
  • Когда важна производительность: f-строки работают быстрее, чем .format() или конкатенация.

Когда стоит остерегаться:

  • F-строки поддерживаются только в Python 3.6+. Для более старых версий нужно использовать .format().

  • Не вставляйте в f-строки сложные выражения с побочными эффектами (например, вызовы функций с изменением состояния) — это ухудшает читаемость и может привести к неожиданным результатам.

  • Для длинных многострочных шаблонов лучше использовать отдельные переменные и форматировать поэтапно, чтобы строка не превращалась в «спагетти».

7. Pathlib

Что это такое?

Модуль pathlib (встроенный в Python с версии 3.4) предоставляет объектно-ориентированный и кроссплатформенный способ работы с путями и файлами.
Он заменяет устаревший подход с os.path, делая код чище и удобнее.

Как это выглядит:

from pathlib import Path

path = Path('data') / 'file.txt'  # Используем "/" вместо os.path.join
print(path.read_text())           # Чтение текста из файла

Когда стоит использовать:

  • Когда нужно удобно конструировать пути:

    logs_dir = Path('var') / 'logs' / 'app.log'
  • Когда важна кроссплатформенность (Windows/Linux/macOS).

  • Для быстрого доступа к методам работы с файлами:

    path.write_text("Hello, world!")  # Создание/перезапись файла
    print(path.exists())             # Проверка существования
  • Когда нужно обойтись без «ручных» конкатенаций вроде os.path.join() и string + '/' + name.

Когда стоит остерегаться:

  • Если проект рассчитан на Python < 3.4 (старые версии) — pathlib может быть недоступен.

  • При работе с большими бинарными файлами лучше использовать методы, возвращающие файловый объект (open()), чтобы избежать загрузки всего содержимого в память:

    with path.open('rb') as f:
        chunk = f.read(1024)
  • Если в команде привыкли к старому API os.path, может потребоваться привыкание.

8. args и *kwargs

Что это такое?

В Python можно писать функции, которые принимают переменное количество аргументов:

  • *args собирает все позиционные аргументы в кортеж.

  • **kwargs собирает все именованные аргументы в словарь.

Как это выглядит:

def greet(*names, **kwargs):
    for name in names:
        print(f"Hello, {name}!")
    for key, value in kwargs.items():
        print(f"{key} = {value}")

greet('Alice', 'Bob', age=30, city='New York')
# Вывод:
# Hello, Alice!
# Hello, Bob!
# age = 30
# city = New York

Когда стоит использовать:

  • Когда заранее неизвестно количество аргументов:

    def sum_all(*numbers):
        return sum(numbers)
    print(sum_all(1, 2, 3, 4))
    # → 10
  • Когда нужно передавать параметры дальше в другие функции:

    def wrapper(*args, **kwargs):
        return some_function(*args, **kwargs)
  • Когда делается гибкий API: библиотеки, декораторы, обёртки.

Когда стоит остерегаться:

  • Избыточное использование args и *kwargs может сделать API непонятным:

    def func(*args, **kwargs): pass  # что сюда передавать?
  • Лучше явно указывать параметры, если их количество и назначение известны.

  • Отладка ошибок может быть сложнее: IDE не подскажет, какие аргументы допустимы.

9. any() и all()

Что это такое?

Функции any() и all() — это встроенные инструменты для проверки булевых условий в коллекциях:

  • any(iterable) — возвращает True, если хотя бы один элемент в итерируемом объекте истинный.

  • all(iterable) — возвращает True, если все элементы истинные.

Как это выглядит:

nums = [1, 2, 3, 0]
print(all(nums))  # False, так как есть 0
print(any(nums))  # True, так как есть ненулевые

Пример с условиями:

values = [x > 0 for x in [-1, 2, 3]]
print(all(values))  # False, так как есть отрицательное число
print(any(values))  # True, так как хотя бы одно значение > 0

Когда стоит использовать:

  • Когда нужно проверить выполнение условия для всех элементов:

    passwords = ["abc123", "123456", "qwerty"]
    if all(len(p) >= 6 for p in passwords):
        print("Все пароли достаточно длинные")
  • Когда достаточно проверить, что есть хотя бы одно совпадение:

    flags = [False, False, True]
    if any(flags):
        print("Есть хотя бы один активный флаг")
  • Для короткого и читаемого кода вместо ручных циклов с проверками.

Когда стоит остерегаться:

  • Если передаётся пустая коллекция:

    all([])  # → True
    any([])  # → False

    Это поведение может быть неожиданным для новичков.

  • Если генераторы внутри any() или all() слишком сложные — лучше вынести условие в отдельную переменную для читаемости.

10. filter

Что это такое?

Функция filter() позволяет отфильтровать элементы итерируемого объекта по заданному условию (функции), возвращая только те элементы, для которых условие истинно. Она возвращает итератор, который нужно преобразовать в список или другой контейнер, если нужен конкретный результат.

Как это выглядит:

numbers = [1, -2, 3, -4, 5]
positive = list(filter(lambda number: number > 0, numbers))
print(positive)
# → [1, 3, 5]

Когда стоит использовать:

  • Когда нужно оставить только элементы, удовлетворяющие условию:

    words = ["apple", "", "banana", "", "cherry"]
    non_empty = list(filter(None, words))  # убирает пустые строки
    # → ['apple', 'banana', 'cherry']
  • Когда есть готовая функция для фильтрации:

    def is_even(n): return n % 2 == 0
    even_numbers = list(filter(is_even, numbers))
  • Когда читаемость важнее, чем ручной цикл с if.

Когда стоит остерегаться:

  • Если условие слишком сложное или требует много логики — лучше использовать list comprehension:

    # Понятнее:
    positive = [n for n in numbers if n > 0]
  • В Python 3 filter() возвращает итератор (а не список, как в Python 2), поэтому для повторного использования нужно делать list().

  • Излишнее использование lambda внутри filter() может ухудшить читаемость.

11. functools.lru_cache

Что это такое?

functools.lru_cache — это встроенный декоратор, который автоматически кэширует результаты вызова функции. Если функция вызывается повторно с теми же аргументами, результат берётся из кэша, а не вычисляется заново. Это позволяет ускорять повторяющиеся вычисления без дополнительного кода.

Как это выглядит:

from functools import lru_cache

@lru_cache(maxsize=128)  # кэш до 128 последних вызовов
def fib(n: int) -> int:
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(35))

Здесь первая серия вызовов вычисляет fib() рекурсивно и складывает результаты в кэш. Повторный вызов с теми же аргументами вернёт значение мгновенно.

Когда стоит использовать:

  • Когда функция выполняет тяжёлые вычисления (например, работа с API, сложная математика).

  • Когда входные данные часто повторяются.

  • При оптимизации кода без изменения его логики.

Когда стоит остерегаться:

  • Кэш хранится в памяти — для функций с огромным количеством уникальных аргументов это может быть накладно.

  • Если данные должны меняться динамически (например, запрос к базе), кэш может возвращать устаревшие значения.

  • При использовании в долгоживущих сервисах стоит указывать maxsize или вручную сбрасывать кэш (fib.cache_clear()).

12. collections.Counter

Что это такое?

Counter из модуля collections — это специальный словарь для подсчёта количества элементов в коллекциях. Он упрощает задачи анализа данных, где нужно посчитать встречаемость значений, и делает код короче и понятнее, чем при использовании обычных циклов и словарей.

Как это выглядит:

from collections import Counter

words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
freq = Counter(words)

print(freq)              # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
print(freq.most_common())  # [('apple', 3), ('banana', 2), ('cherry', 1)]
print(freq.most_common(1)) # [('apple', 3)]

Когда стоит использовать:

  • Когда нужно быстро подсчитать встречаемость элементов (например, слов в тексте, событий в логах).

  • Для нахождения самых популярных элементов (most_common()).

  • При работе со статистикой и аналитикой данных.

Когда стоит остерегаться:

  • Counter не сортирует элементы автоматически — результат по умолчанию упорядочен по убыванию количества, но не по алфавиту.

  • При огромных потоках данных может потребоваться использовать дополнительные структуры (например, базы данных или стриминг).

  • Для очень сложных ключей (например, вложенных объектов) может потребоваться предварительное преобразование в хешируемый тип (строку, кортеж).

13. contextlib.contextmanager

Что это такое?

Иногда нужно временно изменить состояние программы (например, открыть ресурс, заблокировать что-то, изменить переменную окружения), а затем гарантированно вернуть его в исходное состояние. В Python для этого есть контекстные менеджеры (with). С помощью декоратора @contextmanager из модуля contextlib можно легко создавать свои собственные.

Как это выглядит:

import logging
from contextlib import contextmanager

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@contextmanager
def log_execution(task_name: str):
    logger.info(f"Начало выполнения: {task_name}")
    try:
        yield
    finally:
        logger.info(f"Завершено: {task_name}")

# Пример использования
with log_execution("обновление данных"):
    # Здесь выполняем основную работу
    logger.info("Выполняю шаги обновления...")

Когда стоит использовать:

  • Когда нужно аккуратно управлять ресурсами (файлы, соединения, временные настройки).

  • Когда нужно временно «подменить» состояние, а затем вернуть его в исходное (например, поменять текущую директорию и вернуть обратно).

  • Когда хочется избавиться от дублирующегося try/finally кода.

Когда стоит остерегаться:

  • Внутри yield нужно быть аккуратным с изменением состояния — оно будет действовать только в блоке with.

  • Для очень сложной логики иногда лучше использовать полноценный класс с методами enter и exit.

  • Следует помнить, что ошибки внутри блока with не подавляются автоматически — если нужно, их придётся обрабатывать.

Заключение

Эти 13 конструкций — лишь небольшая часть возможностей Python, которые делают код проще, чище и читаемее. Используя их в повседневной работе, вы не только сокращаете количество кода, но и повышаете его понятность для других разработчиков и для себя в будущем.

Если вы начинаете изучать Python, попробуйте включить эти приёмы в свои проекты — вы быстро почувствуете разницу. А если уже знакомы с языком, возможно, нашли что-то новое или вспомнили о забытых возможностях.

А какие приёмы вы чаще всего используете? Делитесь в комментариях — соберём ещё полезные трюки.

Комментарии (6)


  1. Lewigh
    06.08.2025 07:59

    Python — язык, известный своей читаемостью и лаконичностью. В нём есть множество удобных конструкций, которые позволяют писать код короче и понятнее, не теряя при этом выразительности.

    Мне кажется уже пора завязывать с этой мантрой про простоту и читаемость. Я понимаю в двухтысячных на фоне C++ и Java, Python казался действительно компактным, но сейчас с учетом развития новых языков и того насколько перегрузили сам Python - это звучит нелепо.

    Вот это читабельно?

    result = [x for x in data if x.get("enabled") and x["value"] > 10]

    или вот это?

    def process(items: list[tuple[str, int | float]]) -> dict[str, float]:

    По мне, Python давно уже потерял и свою компактность и читаемость и стал ровно таким же мейнстрим языком как Kotlin, TS, Swift и т.д. и забавно говорить но уже Java кажется проще.

    множество удобных конструкций, которые позволяют писать код короче и понятнее, не теряя при этом выразительности

    even_numbers = [x for x in range(10) if x % 2 == 0]

    Это точно понятнее и выразительнее чем нормальный цикл?

    logs_dir = Path('var') / 'logs' / 'app.log'

    А если для logs нужно какую то другую операцию выполнить а не join()?


    1. ValeryIvanov
      06.08.2025 07:59

      Вот это читабельно?

      Если отформатировать, то вполне читабельно:

      # такое форматирование выглядит намного лучше, когда имя не однобуквенное
      result = [
          x
          for x in data
          if x.get("enabled") and x["value"] > 10
      ]
      

      В примере с аннотациями стоит вынести анонимные типы в именованные. Для примитивных типов можно даже использовать NewType, но это совсем необязательно:

      type Item = tuple[str, int | float]
      type Response = dict[str, float]
      
      def process(items: list[Item]) -> Response:
      

      Это точно понятнее и выразительнее чем нормальный цикл?

      Наверное у меня профдеформация после стольких лет использования питона, но я не могу сказать, что в приведённом вами примере такого невыразительного, хотя согласен, что многие используют включения там, где это только вредит. Нужно хорошо понимать где использовать цикл, где включение, где функция-генератор.

      А если для logs нужно какую то другую операцию выполнить а не join()?

      Не совсем понял, в чём вопрос. Если нужно выполнить другую операцию, то... вы просто выполняете другую операцию.


      1. sound_right Автор
        06.08.2025 07:59

        Согласен с вами по поводу форматирования и выноса типов, хороший совет. В статье я не стал глубоко уходить в оформление, чтобы не перегружать начинающих, но для production-кода такое форматирование и именованные типы точно делают код понятнее


  1. OlegZH
    06.08.2025 07:59

    • Если передаётся пустая коллекция:

      all([])  # → True
      any([])  # → False

      Это поведение может быть неожиданным для новичков.

    Почему? Тут же всё очень просто: если нужны все элементы, то подойдёт и случай, когда нет ни одного элемента (тогда True), а если нужен некоторый, а нет и одного, то и выбирать нечего (тогда False).

    Вообще, все эти вполне здравые и понятные рекомендации несколько однобоки. Например, функции map/filter/reduce. Да, если использовать lambda-инструкцию, то код может стать не читаемым. Но кто мешает поставлять обычные функции? Это, что называется, во-первых. А во-вторых, есть ещё и замыкания, когда можно какие-то элементы данных запомнить внутри вызываемых функций, что позволяет существенно упростить код. Рассмотрим, для примера, следующий код:

    def TableSelect(TABLE, SelectedNames, Statement=None):
        TableName, ColumnNames, TableRows = TABLE
        if Statement:
            SelectedRows = list(filter(lambda Row: apply(Statement, Row), TableRows))
        else:
            SelectedRows = TableRows[:]
        SelectedRows = SelectColumns(SelectedRows, SelectedNames)        

    Эта функция применяет к каждой строке некоторой таблицы заданное условие (Statement). Само условие формулируется следующим образом (например):

    statemnet = OR(FieldValueEq("Код", '1009'), FieldValueEq("Код", '1010'))

    Реализация используемых здесь функций такова:

    def FieldValueEq(FieldName, FieldValue):
        def EqualTo(Row):
            return Row[FieldName] == FieldValue
        return EqualTo   
    
    def FieldValuesEq(LeftFieldName, RightFieldName):
        def EqualTo(Row):
            return Row[LeftFieldName] == Row[RightFieldName]
        return EqualTo
    
    def OR(*args):
        return (any, args)    
    
    def AND(*args):
        return (all, args) 
    
    def apply(fun, obj):
        # if type(fun) is function:
        if callable(fun):
            return fun(obj)
        else:
            fun, input_args = fun
            output_args = map(lambda arg: apply(arg, obj), input_args)
            return fun(output_args)

    Другими словами, мы упаковываем сложные условия в создаваемую нами налету функцию в момент вызова

    statemnet = OR(FieldValueEq("Код", '1009'), FieldValueEq("Код", '1010'))

    А когда приходит время использовать заданное условие, происходит вызов

    apply(Statement, Row)

    который разбирает заданное условие шаг за шагом, подставляя для каждой фигурирующей в этом условии функции данные текущей строки (Row).

    Наверное, можно было бы проделать этот трюк и с самим вызовом

    apply(Statement, Row)

    написав что-то вроде

    SelectedRows = list(filter(apply_statement(Statement), TableRows))

    чтобы совсем упростить программный код, но здесь мы столкнёмся с трудностями, связанными с замыканиями.


    1. sound_right Автор
      06.08.2025 07:59

      В целом, да, поведение all([]) и any([]) логично, но у новичков оно иногда вызывает вопросы, особенно когда они впервые сталкиваются с пустыми коллекциями.

      Про map и filter вы правы: использование lambda может ухудшать читаемость, в статье я специально отметил, что нужно знать меру:

      Если код становится трудночитаемым из-за использования lambda внутри map — в таком случае list comprehension часто понятнее.


  1. OlegZH
    06.08.2025 07:59

    О чём следовало бы говорить, так это о некоторой ограниченности функции map. Например, мы могли бы написать функцию

    def func_apply(main_func, input_array, filter_func=None, reduce_func=None):
        def process_element(array, i):
            
            input_value = array[i]
            
            value = None
            if not filter_func or (filter_func and filter_func(input_value, array)):
                value = main_func(input_value)
            
            if value is None:
                return None
    
            output_value = None
            if not reduce_func or (reduce_func and reduce_func(value, array)):
                output_value = value      
            
            return output_value
    
        n = len(input_array)
        output_array = [ process_element(input_array, i) for i in range(n) ]
        return output_array

    Здесь, мы также применяем одну и туже функцию к каждому элементу заданного массива, но, предварительно проверяем фильтрующее условие, и, уже постфактум, проверяем выходное значение.