Итоги недели в мире бэкенда и обзоры новых сервисов: 10 приемов для улучшения Python-кода

Итоги недели в мире бэкенда и обзоры новых сервисов: 10 приемов для улучшения Python-кода

🐍 10 приемов для улучшения Python-кода

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

Использование дескрипторов для управления доступом к атрибутам

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

Пример: класс для проверки типов через дескриптор

class Typed: def __init__(self, name, expected_type): self.name = name self.expected_type = expected_type def __get__(self, instance, owner): if instance is None: return self return instance.__dict__.get(self.name) def __set__(self, instance, value): if not isinstance(value, self.expected_type): raise TypeError(f"Ожидаются данные типа {self.expected_type}, получены данные типа {type(value)}") instance.__dict__[self.name] = value def __delete__(self, instance): raise AttributeError("Невозможно удалить атрибут") class Person: name = Typed("name", str) age = Typed("age", int) def __init__(self, name, age): self.name = name self.age = age try: p = Person("Алиса", 10) print(p.name) p.age = "10" except TypeError as e: print(e)

Вывод:

Алиса Ожидаются данные типа <class 'int'>, получены данные типа <class 'str'>

Привет!

Мы запустили еженедельную email-рассылку, посвященную последним новостям и тенденциям в мире бэкенда. В еженедельных письмах ты найдешь:

  • Языки программирования и фреймворки для бэкенда.
  • Архитектура и проектирование серверных приложений.
  • Базы данных и управление данными.
  • Безопасность и защита данных.
  • Облачные технологии и DevOps.
  • API и интеграции.
  • Тестирование и отладка.
  • Инструменты и утилиты для бэкенд-разработчиков.
  • Лучшие практики и паттерны проектирования.

Упрощение кода классов с помощью dataclass

Использование dataclass в Python – удобный способ упрощения кода для классов, которые предназначены только для хранения данных. Вместо ручного определения методов __init__, __repr__, __eq__ и т. д. можно автоматически генерировать их, используя аннотации типов. Преимущества использования dataclass:

  • Сокращение кода – нет необходимости вручную писать методы вроде __init__ и __repr__, и их поведение не нужно поддерживать.
  • Читаемость – код легче читать, потому что вы видите только список полей, описывающих объект.
  • Гибкость – можно добавлять методы или настройки, чтобы изменять поведение класса.
  • Автоматизация – встроенные методы генерируются автоматически, вероятность ошибок сводится к минимуму.

Пример:

from dataclasses import dataclass @dataclass class Employee: name: str last_name: str age: int position: str emp = Employee(name="Евгений", last_name="Онегин", age=30, position="бэкендер") print(emp) emp2 = Employee(name="Пьер", last_name="Безухов", age=30, position="фронтендер") print(emp == emp2) print(repr(emp2))

Вывод:

Employee(name='Евгений', last_name='Онегин', age=30, position='бэкендер') False Employee(name='Пьер', last_name='Безухов', age=30, position='фронтендер')

🎓☕ Подтянуть свои знания по Java вы можете на нашем телеграм-канале «Библиотека Java для собеса»

Кастомные менеджеры контекста

Когда возможностей стандартного контекстного менеджера (например, open) недостаточно, можно создать свой, чтобы упростить управление специфическими ресурсами.

Пример: собственный контекстный менеджер для работы с базой данных

import sqlite3 from contextlib import contextmanager from pathlib import Path @contextmanager def open_db(db_path): db_path = Path(db_path) if not db_path.exists(): raise FileNotFoundError(f"Файл базы данных {db_path} не найден.") connection = sqlite3.connect(db_path) try: yield connection finally: connection.commit() connection.close() db_file = "example.db" with open_db(db_file) as conn: cursor = conn.cursor() cursor.execute('''CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER)''') cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("Евгений", 30)) cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("Татьяна", 25)) print("Данные внесены в базу.")

Вывод:

Данные внесены в базу.

Аннотации функций

Аннотации функций показывают, какие типы данных ожидаются на входе и какой тип будет возвращен. Они делают код понятнее, безопаснее и удобнее для работы и особенно полезны в крупных проектах, где требуется строгий контроль типов и ясное описание поведения функций. Например, здесь сразу видно, что функция принимает length и width в виде чисел с плавающей точкой float и возвращает значение того же типа:

def calculate_area(length: float, width: float) -> float: return length * width

Многие IDE и инструменты проверки типов (например, MyPy) могут использовать аннотации для поиска ошибок в коде еще до его выполнения. Например, если вы случайно передадите строку вместо числа, то получите предупреждение, что строка str не соответствует ожидаемому типу float:

area = calculate_area("5.0", 3.2) # Ошибка!

➕➕🧩 Интересные задачи по C++ для практики можно найти на нашем телеграм-канале «Библиотека задач по С++»

Декораторы для повторного использования кода

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

Пример: измерение времени выполнения функции

import time def measure_time(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"Функция {func.__name__} выполнена за {end - start:.4f} секунд") return result return wrapper @measure_time def compute(): sum(range(10**7)) compute()

Вывод:

Функция compute выполнена за 0.1000 секунд

Использование functools для оптимизации сложных операций

Модуль functools – мощный инструмент, который:

  • Помогает оптимизировать производительность (за счет кэширования).
  • Облегчает создание гибких и модульных функций.
  • Упрощает работу с функциями высшего порядка.

Пример: оптимизация производительности с помощью кэширования

from functools import lru_cache @lru_cache(maxsize=100) def fibonacci(n): if n < 2: return n return fibonacci(n - 1) + fibonacci(n - 2) print(fibonacci(100))

Без кэширования вычисление больших значений Фибоначчи было бы чрезвычайно медленным из-за экспоненциальной сложности. Но с lru_cache предыдущие результаты сохраняются, что значительно сокращает время выполнения.

🦫🎓 Подтянуть свои знания по Go вы можете на нашем телеграм-канале «Библиотека Go для собеса»

Эффективные структуры данных collections

Модуль collections предоставляет высокоуровневые структуры данных, которые оптимизируют задачи, связанные с обработкой коллекций. Например, defaultdict автоматически создает значения по умолчанию для отсутствующих ключей, что устраняет необходимость в проверках или дополнительных условиях. А еще он очень удобен для группировки или построения вложенных структур:

from collections import defaultdict grouped = defaultdict(list) data = [("фрукты", "яблоко"), ("животное", "кошка"), ("фрукты", "банан"), ("животное", "собака")] for key, value in data: grouped[key].append(value) print(grouped)

Вывод:

defaultdict(<class 'list'>, {'фрукты': ['яблоко', 'банан'], 'животное': ['кошка', 'собака']})

Counter обеспечивает подсчет:

from collections import Counter text = "hello world" char_count = Counter(text) print(char_count.most_common(3))

Вывод:

[('l', 3), ('o', 2), ('h', 1)]

namedtuple упрощает создание неизменяемых объектов с именованными полями:

from collections import namedtuple Point = namedtuple("Point", ["x", "y"]) p = Point(3, 4) print(p.x, p.y)

Вывод:

3 4

Упрощение многопоточности и многопроцессорности

Модуль concurrent.futures предоставляет высокоуровневый интерфейс для параллельного выполнения задач с использованием:

  • ThreadPoolExecutor – для задач ввода-вывода.
  • ProcessPoolExecutor – для вычислительно сложных задач, нагружающих процессор.

Пример: загрузка нескольких веб-страниц в разных потоках

import requests from concurrent.futures import ThreadPoolExecutor def fetch_url(url): response = requests.get(url) return url, response.status_code urls = ["https://example.com", "https://python.org", "https://proglib.io"] with ThreadPoolExecutor(max_workers=3) as executor: results = executor.map(fetch_url, urls) for url, status in results: print(f"{url}: {status}")

Вывод:

https://example.com: 200 https://python.org: 200 https://proglib.io: 200

🧩☕ Интересные задачи по Java для практики можно найти на нашем телеграм-канале «Библиотека задач по Java»

Работа с файловой системой

Модуль pathlib позволяет работать с путями как с объектами, предоставляя интуитивный синтаксис и методы, которые делают код лаконичным и понятным. Преимущества pathlib по сравнению с os.path:

  • Более простые методы и операции.
  • Код работает одинаково на всех операционных системах.
  • Есть функционал для чтения, записи, проверки путей, обхода директорий и работы с файлами.
  • Объектно-ориентированный подход делает код более структурированным.

Пример: поиск .docx-файлов во всех поддиректориях:

from pathlib import Path path = Path("C:/Users/Admin/Documents") for file in path.rglob("*.docx"): print(file)

При использовании os.path код получается более громоздким:

import os path = "C:/Users/Admin/Documents" for dirpath, dirnames, filenames in os.walk(path): for filename in filenames: if filename.endswith(".docx"): print(os.path.join(dirpath, filename))

Мокинг в модульных тестах

Использование мокинга в модульных тестах – мощный способ изолировать тестируемый код от внешних зависимостей (базы данных, веб-сервисы или сторонние API). Это сильно упрощает процесс написания тестов, а также дает возможность тестировать части системы, которые еще не интегрированы с внешними сервисами.

Пример: мокирование API для получения курса биткоина

import requests from unittest.mock import patch def get_bitcoin_price(): response = requests.get("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd") data = response.json() if "bitcoin" not in data: raise ValueError("Ошибка получения данных от API") return data["bitcoin"]["usd"] @patch('requests.get') def test_get_bitcoin_price(mock_get): mock_get.return_value.json.return_value = { "bitcoin": { "usd": 99000 } } price = get_bitcoin_price() print(f"Текущая стоимость биткоина ${price}.") test_get_bitcoin_price()

Вывод:

Текущая стоимость биткоина $99000.

➕➕🎓 Подтянуть свои знания по C++ вы можете на нашем телеграм-канале «Библиотека С++ для собеса»

🗣 Как работают запросы к базе данных

Каждый запрос к базе данных проходит через несколько важных этапов обработки. Давайте разберем этот процесс на примере запроса:

SELECT name, age FROM users WHERE city = 'New York';
Увлекательное путешествие запроса по базе данных
Увлекательное путешествие запроса по базе данных

Шаг 1: Транспортная подсистема

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

Шаг 2: Обработчик запросов

Запрос попадает в обработчик запросов (Query Processor), который состоит из двух основных компонентов:

  • Парсер запроса (Query Parser). Он разбивает SQL-запрос на части (SELECT, FROM, WHERE и т. д.), проверяет синтаксические ошибки и создает дерево разбора (parse tree).
  • Оптимизатор запроса (Query Optimizer). После того как запрос разобран, оптимизатор проверяет его на семантические ошибки (например, существует ли таблица "users"). Он также определяет наиболее эффективный способ выполнения запроса. В результате работы оптимизатора создается план выполнения (execution plan), который описывает, как именно будет выполнен запрос.

🦫🧩 Интересные задачи по Go для практики можно найти на нашем телеграм-канале «Библиотека задач по Go»

Шаг 3: Движок выполнения

План выполнения передается в движок выполнения (Execution Engine). Этот компонент координирует выполнение запроса, используя план, созданный на предыдущем шаге. Он вызывает хранилище данных (Storage Engine), выполняет шаги запроса и собирает результаты, чтобы вернуть их клиенту.

Шаг 4: Хранилище данных

Движок выполнения отправляет низкоуровневые запросы на чтение и запись в хранилище данных в соответствии с планом выполнения. Хранилище данных состоит из нескольких подсистем:

  • Менеджер транзакций (Transaction Manager) обеспечивает выполнение запроса в рамках транзакции, чтобы гарантировать согласованность данных.
  • Менеджер блокировок (Lock Manager) получает блокировки на таблице "users", чтобы избежать конфликтов с другими запросами.
  • Менеджер буферов (Buffer Manager) проверяет, находятся ли нужные данные в памяти. Если их нет, он запрашивает их с диска и загружает в память.
  • Менеджер восстановления (Recovery Manager) записывает операции в журнал для возможности отката или восстановления данных.

Когда все эти шаги выполнены, результат запроса (например, имена и возраст пользователей из города Нью-Йорк) передается обратно клиенту. Вот так и происходит обработка вашего SQL-запроса в базе данных. 🙂

Автор рассылки: Наталья Кайда

11
Начать дискуссию