Идемпотентность запросов (Idempotency)
Идемпотентный запрос — это запрос, который может быть выполнен несколько раз подряд с одинаковым результатом, без изменения состояния сервера или получения разных ответов. Это критически важное понятие для проектирования надёжных API и обработки сетевых ошибок.
Определение идемпотентности
Запрос идемпотентен, если:
HTTP методы и идемпотентность
Идемпотентные методы:
# GET — только читает данные, ничего не меняет
GET /users/123
GET /users/123 # Второй раз вернёт то же самое
# PUT — заменяет ресурс полностью (предсказуемый результат)
PUT /users/123
{
"name": "John",
"email": "john@example.com"
}
# Выполнено 1 раз или 5 раз — результат одинаковый
Паттерн Builder (Строитель)
Builder — это порождающий паттерн проектирования, который используется для пошагового создания сложных объектов. Он отделяет процесс конструирования объекта от его представления, позволяя создавать различные варианты объекта.
Основная идея
Вместо передачи множества параметров в конструктор, мы создаём промежуточный объект (Builder), который накапливает значения параметров и затем создаёт целевой объект с помощью метода build().
Когда использовать Builder
Пример с классом и Builder
Аутентификация (Authentication)
Аутентификация — это процесс проверки подлинности пользователя. Система убеждается, что пользователь действительно тот, за кого себя выдаёт. Ответ на вопрос: "Ты кто?"
Аутентификация vs Авторизация
Эти два термина часто путают:
# Пример: проверка доступа
1. Аутентификация → login("alice", "password123") → Хорошо! Ты Alice
2. Авторизация → can_edit_post(alice, post_id) → Да, Alice может редактировать
Методы аутентификации
# Самый простой метод
from fastapi import FastAPI, HTTPException, Depends
from fastapi.security import HTTPBasic, HTTPBasicCredentials
import hashlib
app = FastAPI()
security = HTTPBasic()
S3 бакеты: облачное хранилище объектов
S3 (Simple Storage Service) — это облачный сервис хранения объектов от Amazon Web Services (AWS). S3 бакеты — это контейнеры (хранилища) для организации и управления файлами в облаке.
Основные характеристики S3
Структура и терминология
Бакет — это как папка верхнего уровня:
mybucket/
├── images/
│ ├── photo1.jpg
│ └── photo2.jpg
├── documents/
│ ├── report.pdf
│ └── whitepaper.docx
└── videos/
└── intro.mp4
CQRS (Command Query Responsibility Segregation)
CQRS — это архитектурный паттерн, который разделяет операции на две категории: команды (записи) и запросы (чтение). Основная идея заключается в том, что модели для создания и обновления данных отличаются от моделей для чтения данных.
Основные принципы
Традиционный подход использует одну модель для всех операций. CQRS же предлагает:
Пример реализации на Python
from dataclasses import dataclass
from typing import List
from datetime import datetime
# Команды (write model)
@dataclass
class CreateUserCommand:
username: str
email: str
password: str
@dataclass
class UpdateUserCommand:
user_id: int
email: str
Что происходит внутри Python при создании класса
Этот вопрос о метаклассах, дескрипторах и подробном разборе процесса. Это средний-продвинутый уровень, но важно для глубокого понимания Python.
1. Основной процесс: от синтаксиса к объекту
# Когда мы пишем:
class User:
count = 0 # Class attribute
def __init__(self, name):
self.name = name # Instance attribute
User.count += 1
def greet(self):
return f"Hello, {self.name}"
# Внутри Python происходит ЭТО:
2. Фаза 1: Парсинг и выполнение класс-блока
# Синтаксис class создаёт НОВЫЙ scope (пространство имён)
Сильная и слабая типизация в языках программирования
Это фундаментальное понятие, которое часто путают с динамической и статической типизацией. Объясню разницу и покажу примеры на Python.
Определения
Сильная типизация — язык не позволяет неявные преобразования типов между несовместимыми типами. Операции между разными типами либо запрещены, либо требуют явного преобразования.
Слабая типизация — язык автоматически преобразует типы при необходимости, скрывая несовместимость и часто приводя к неожиданному поведению.
Примеры в Python (сильная типизация)
# Python — СТРОГО типизированный язык
# ❌ ОШИБКА: нельзя складывать число и строку
result = 10 + "20" # TypeError: unsupported operand type(s)
# ✅ ПРАВИЛЬНО: явное преобразование
result = 10 + int("20") # 30
# ❌ ОШИБКА: нельзя вызвать метод строки на числе
x = 5
print(x.upper()) # AttributeError: 'int' object has no attribute 'upper'
Оператор DELETE в Python
DELETE — это оператор в Python, который удаляет объект из памяти и делает имя переменной недоступным. Это важный инструмент для управления памятью.
Основное назначение DELETE
Оператор del удаляет ссылку на объект. Если это последняя ссылка, garbage collector удаляет сам объект из памяти.
# Создаём переменную
x = [1, 2, 3]
print(x) # [1, 2, 3]
# Удаляем ссылку на объект
del x
# Теперь переменная не существует
print(x) # NameError
Удаление элементов списка
list_items = ["apple", "banana", "cherry", "date"]
# Удалить элемент по индексу
del list_items[1]
print(list_items) # ["apple", "cherry", "date"]
# Удалить срез
list_items = ["apple", "banana", "cherry", "date", "elderberry"]
del list_items[1:3]
print(list_items) # ["apple", "date", "elderberry"]
# Удалить с шагом
list_items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
del list_items[::2]
print(list_items) # [2, 4, 6, 8, 10]
Удаление из словаря
Диагностика и оптимизация медленных Django endpoints
Ситуация, когда эндпоинты обрабатываются 1+ минуту, явно указывает на проблему. Я бы начал систематическую диагностику, используя проверенный подход.
Шаг 1: Определить узкое место
Использую Django Debug Toolbar для профилирования:
# settings.py для development
INSTALLED_APPS = [
'debug_toolbar',
# ... другие приложения
]
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
# ... другие middleware
]
if DEBUG:
INTERNAL_IPS = ['127.0.0.1']
# urls.py
from django.conf import settings
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
path('__debug__/', include(debug_toolbar.urls)),
] + urlpatterns
Debug Toolbar показывает:
Также использую django-silk для production:
Альтернативы pass в Python
pass — это no-op (пустая операция) в Python, которая требуется для синтаксически пустых блоков кода. Однако существует несколько способов его замены в зависимости от контекста.
Когда используется pass?
# Синтаксис требует тело блока, но логики нет
class MyClass:
pass # Пустой класс
def placeholder_function():
pass # Пустая функция
if condition:
pass # Пока логики нет
Альтернативы pass
1. Документация (docstring) вместо pass
Это самая частая и рекомендуемая альтернатива:
class BaseDatabase:
"""Абстрактный базовый класс для работы с БД"""
def not_implemented_yet():
"""Эта функция будет реализована позже"""
class MyException(Exception):
"""Кастомное исключение для нашего приложения"""
2. Исключение NotImplementedError для абстрактных методов
Это правильный способ для определения интерфейсов:
HTTP Status Code для ошибки авторизации
Ошибка авторизации в HTTP протоколе обозначается кодом 401 Unauthorized. Это наиболее корректный код для использования, когда запрос не содержит необходимых учётных данных или они неверны.
Различие между 401 и 403
Важно понимать разницу между 401 и 403:
Примеры использования в коде
Вот как обрабатываются эти ошибки в FastAPI и других фреймворках:
from fastapi import FastAPI, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthCredentials
app = FastAPI()
security = HTTPBearer()
Как сохранить зависимости библиотек в pip
Сохранение зависимостей проекта критически важно для воспроизводимости окружения. Это предотвращает ситуации, когда код работает у вас, но падает у коллеги или на сервере.
1. requirements.txt через pip freeze
# Сохранить все установленные пакеты в файл
pip freeze > requirements.txt
# Содержимое requirements.txt:
Django==4.2.0
celery==5.3.1
requests==2.31.0
psycopg2-binary==2.9.6
# Установить все зависимости
pip install -r requirements.txt
2. Ручное создание requirements.txt
# Вместо pip freeze можно написать вручную
echo 'Django==4.2.0
celery>=5.3
requests>=2.31
psycopg2-binary>=2.9' > requirements.txt
# Преимущества:
# - Контролируешь версии
# - Более гибкие версии (>=)
# - Явная информация о главных пакетах
3. Разделение по окружениям
requirements/
├── base.txt # основные
├── dev.txt # для разработки
└── prod.txt # для production
Конкатенация списков в Python
Есть несколько способов объединить два или более списков. Выбор зависит от контекста и производительности.
1. Оператор + (самый простой)
list1 = [1, 2, 3]
list2 = [4, 5, 6]
# Оператор + создаёт новый список
result = list1 + list2
print(result) # [1, 2, 3, 4, 5, 6]
# Исходные списки не изменяются
print(list1) # [1, 2, 3] — без изменений
2. Метод extend() (изменяет оригинал)
list1 = [1, 2, 3]
list2 = [4, 5, 6]
# extend() добавляет элементы list2 в list1
list1.extend(list2)
print(list1) # [1, 2, 3, 4, 5, 6]
print(list2) # [4, 5, 6] — не меняется
# extend() изменяет оригинальный список (in-place)
# и не возвращает результат
result = list1.extend(list2) # Это вернёт None!
print(result) # None
3. Распаковка (Unpacking)
list1 = [1, 2, 3]
list2 = [4, 5, 6]
list3 = [7, 8, 9]
# Способ 1: используя *
result = [*list1, *list2, *list3]
print(result) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
Обработка исключений в Python
Обработка исключений (exception handling) — это критический инструмент для написания надежного и устойчивого к ошибкам кода. Python предоставляет мощный механизм try-except-finally.
1. Базовый синтаксис try-except
# Базовая структура
try:
# Код, который может вызвать исключение
result = 10 / 0
except ZeroDivisionError:
# Обработка конкретного исключения
print("Нельзя делить на ноль!")
# Результат: печатает сообщение об ошибке вместо краша
2. Перехват нескольких исключений
try:
value = int("abc") # ValueError
except ValueError:
print("Ошибка: неверное значение")
except TypeError:
print("Ошибка: неверный тип")
except Exception as e:
print(f"Неожиданная ошибка: {e}")
Порядок важен — специфичные исключения должны идти первыми.
3. Несколько исключений в одном блоке
try:
value = int("abc")
except (ValueError, TypeError) as e:
print(f"Ошибка: {e}")
Функция property() в Python
property() — это встроенная функция, которая позволяет создавать управляемые атрибуты класса. Она превращает методы в свойства, позволяя вам вызывать методы как обычные атрибуты, но с дополнительной логикой валидации, преобразования или ограничения доступа.
Проблема без property
# ❌ Плохо: прямой доступ к атрибутам
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
person = Person('Alice', 30)
person.age = -5 # Ничто не помешает!
print(person.age) # -5 — невалидное значение
# Проблемы:
# - Нет валидации
# - Нет логики при установке значения
# - Если позже добавим логику, сломаем существующий код
Решение 1: Методы (старый способ)
Зачем нужен метод save?
Вопрос зависит от контекста. В Python есть несколько интерпретаций метода save().
1. В ORM (SQLAlchemy, Django ORM)
# Django ORM
from django.db import models
class User(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField()
user = User(name="John", email="john@example.com")
user.save() # Сохраняет в БД
# SQLAlchemy
from sqlalchemy.orm import Session
session = Session()
user = User(name="John", email="john@example.com")
session.add(user)
session.commit() # Эквивалент save()
2. В модельных классах (Machine Learning)
from sklearn.externals import joblib
model = train_model(data)
model.save('model.pkl') # Сохраняет модель на диск
# Загрузка
loaded_model = joblib.load('model.pkl')
3. В файловых операциях
import json
data = {"key": "value"}
with open('data.json', 'w') as f:
json.dump(data, f) # Сохраняет JSON
Итог
Описание поведения объекта в Java
Основные способы
Поведение объекта описывается несколькими способами:
1. Interface (Интерфейс)
Самый явный способ — интерфейс определяет контракт (какие методы должны быть и что они делают):
public interface PaymentProcessor {
/**
* Обрабатывает платёж
* @param amount сумма платежа
* @return результат обработки
*/
boolean processPayment(double amount);
/**
* Проверяет статус платежа
*/
PaymentStatus getStatus();
}
2. Abstract Class (Абстрактный класс)
Описывает поведение + состояние через методы и поля:
public abstract class Animal {
private String name;
// Конкретное поведение (реализовано)
public void sleep() {
System.out.println(name + " спит");
}
// Абстрактное поведение (подклассы должны реализовать)
public abstract void makeSound();
}
3. Javadoc (Документация)
Первичный ключ (Primary Key)
Первичный ключ — это одно или несколько полей в таблице, которые однозначно идентифицируют каждую строку. Это фундаментальное понятие в реляционных БД.
Основные функции Primary Key
Синтаксис в SQL
-- Single column Primary Key
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(100),
name VARCHAR(100)
);
-- Или как constraint
CREATE TABLE users (
id INT,
email VARCHAR(100),
PRIMARY KEY (id)
);
-- Composite Primary Key (несколько полей)
CREATE TABLE user_roles (
user_id INT,
role_id INT,
assigned_date DATE,
PRIMARY KEY (user_id, role_id) -- Уникальная комбинация
);
В SQLAlchemy (Python ORM)
Хеширование (Hashing)
Хеширование — это процесс преобразования данных произвольного размера в фиксированный размер строки символов (хеш-сумма или digest) с помощью специальной функции называемой хеш-функцией. Основное назначение хеширования — быстро проверять целостность данных, создавать индексы для быстрого поиска и криптографически защищать информацию. Хеширование является одним из ключевых технологий в информационной безопасности и информатике в целом.
Основные свойства хеш-функций
Качественная хеш-функция должна обладать следующими свойствами:
Паттерн Наблюдатель (Observer Pattern)
Observer Pattern (Наблюдатель) — это поведенческий паттерн проектирования, который создаёт механизм подписки на события. Когда состояние объекта изменяется, все заинтересованные объекты (наблюдатели) автоматически уведомляются об этом. Паттерн реализует loose coupling — объекты слабо связаны между собой.
Основная архитектура
Subject (издатель): объект, который отслеживает и публикует события Observer (наблюдатель): объект, который слушает события и реагирует на них
Простой пример на чистом Python
from abc import ABC, abstractmethod
from typing import List
# Observer Interface
class Observer(ABC):
"""Интерфейс для наблюдателей"""
@abstractmethod
def update(self, subject):
"""Метод, вызываемый при изменении subject"""
pass
Изоляция в ACID
Изоляция (Isolation) — это четвёртый принцип ACID (Atomicity, Consistency, Isolation, Durability), который гарантирует, что одновременно выполняемые транзакции не влияют друг на друга и не видят незавершённые изменения друг друга.
Что такое ACID
ACID — это набор свойств, которые гарантируют надёжность транзакций в базе данных:
Проблема конкурентности без изоляции
Без правильной изоляции возникают следующие проблемы:
Dirty Read (грязное чтение):
Snapshot в PostgreSQL
Snapshot (снимок) в PostgreSQL — это снимок состояния базы данных на конкретный момент времени, который определяет, какие строки видны для конкретной транзакции. Snapshot обеспечивает изоляцию транзакций, позволяя разным транзакциям видеть разные версии одних и тех же данных в зависимости от уровня изоляции.
Проблема многовверсионности
Постгре использует MVCC (Multi-Version Concurrency Control) — каждая транзакция видит свою версию данных:
-- Сессия 1:
BEGIN;
UPDATE users SET balance = 100 WHERE id = 1;
-- Баланс изменяется, но COMMIT ещё не выполнен
-- Сессия 2 (одновременно):
BEGIN;
SELECT balance FROM users WHERE id = 1; -- Видит старое значение!
-- Из-за SNAPSHOT: видит состояние БД на момент начала транзакции
Что такое Snapshot
Snapshot содержит информацию о:
Load Average в Linux: нагрузка на систему
Load Average — это метрика, которая показывает среднее количество процессов, находящихся в очереди на выполнение (или активно выполняющихся) на протяжении определённого времени. Это одна из самых важных метрик для мониторинга производительности серверов.
Где увидеть Load Average
# Команда uptime
$ uptime
14:30:42 up 10 days, 2:15, 3 users, load average: 0.52, 0.48, 0.45
# Команда top
$ top
top - 14:30:45 up 10 days, 2:15, 3 users, load average: 0.52, 0.48, 0.45
# Файл /proc/loadavg
$ cat /proc/loadavg
0.52 0.48 0.45 1/342 12345
Три значения Load Average
Load average всегда показывает три числа:
0.52 0.48 0.45
| | |
1min 5min 15min
Что означают эти цифры
Что будет если не использовать await в асинхронной функции?
Это критичный баг, который приводит к проблемам с race conditions и потере данных. Давайте разберёмся.
Основное правило
Если функция асинхронная — её результат нужно await-ить!
async def fetch_user(user_id: int):
# Имитируем задержку БД (1 сек)
await asyncio.sleep(1)
return {"id": user_id, "name": "John"}
async def main():
# НЕПРАВИЛЬНО: забыли await
result = fetch_user(1) # Возвращает Coroutine объект
print(result) # <coroutine object fetch_user at 0x7f8b8c8c5f40>
print(type(result)) # <class coroutine>
# ПРАВИЛЬНО: используем await
result = await fetch_user(1)
print(result) # {"id": 1, "name": "John"}
Что происходит без await
При отсутствии await функция не выполняется. Вместо этого возвращается Coroutine объект — это как повисшая задача.
import asyncio
from datetime import datetime
DNS маршрутизация: Практическое применение
DNS маршрутизация (DNS routing) — это техника использования DNS для распределения трафика между серверами. Это мощный инструмент для масштабирования и отказоустойчивости.
Как работает DNS
Клиент запрашивает: api.example.com
↓
DNS resolver проверяет:
1. Локальный кэш браузера
2. Кэш ISP
3. Корневой DNS сервер
4. Авторитетный DNS сервер (example.com)
↓
DNS сервер возвращает IP адрес(а)
↓
Клиент подключается к IP адресу
В этом процессе DNS может сделать МНОГО полезного!
Типы DNS маршрутизации
# В DNS записи указываем несколько A records
api.example.com IN A 192.168.1.10
api.example.com IN A 192.168.1.20
api.example.com IN A 192.168.1.30
# Клиент 1 получает IP #1 → 192.168.1.10
# Клиент 2 получает IP #2 → 192.168.1.20
# Клиент 3 получает IP #3 → 192.168.1.30
# Клиент 4 получает IP #1 (круг замкнулся) → 192.168.1.10
Хранение состояния клиента в REST
Принцип Statelessness
Нет, REST не хранит состояние клиента на сервере. Это одна из фундаментальных характеристик REST архитектуры, определённая Roy Fielding в его диссертации. Принцип называется Statelessness (отсутствие состояния).
Что это значит
Statelessness означает, что:
Почему это важно
Масштабируемость:
# ❌ Stateful подход (плохо)
# Сервер 1 запомнил: user_id=123 logged_in=True
# Если запрос пойдёт на Сервер 2, он не знает о клиенте
# ✅ Stateless подход (хорошо)
# Клиент отправляет: Authorization: Bearer token_xyz
# Любой сервер может обработать запрос
При stateless можно использовать любой сервер из пула — не нужна привязка к одному серверу.
Надёжность:
Количество потоков и процессов в asyncio
Ответ: Ни один процесс, один поток
asyncio использует один поток выполнения и ни одного дополнительного процесса по умолчанию. Это критически важно понимать для правильного использования асинхронного программирования в Python.
Как это работает
asyncio работает на базе event loop — встроенного механизма планирования задач, который работает исключительно в одном потоке:
import asyncio
async def task1():
print("Задача 1 начало")
await asyncio.sleep(1)
print("Задача 1 конец")
async def task2():
print("Задача 2 начало")
await asyncio.sleep(1)
print("Задача 2 конец")
async def main():
# Обе задачи работают в ОДНОМ потоке
await asyncio.gather(task1(), task2())
asyncio.run(main())
Вывод:
Задача 1 начало
Задача 2 начало
Задача 1 конец
Задача 2 конец
Обе задачи выполняются в одном потоке, а не параллельно!
Почему это однопоточно?
Валидация входных данных в Django Rest Framework
DRF предоставляет мощный встроенный механизм валидации данных. Валидация — это критически важный процесс для обеспечения целостности и безопасности приложения.
1. Валидация на уровне полей (Field-level validation)
Это первый уровень валидации, который срабатывает автоматически для каждого поля в сериализаторе.
from rest_framework import serializers
class UserSerializer(serializers.Serializer):
username = serializers.CharField(
max_length=150,
min_length=3,
required=True
)
email = serializers.EmailField()
age = serializers.IntegerField(
min_value=0,
max_value=150
)
phone = serializers.CharField(
max_length=20,
allow_blank=False
)
DRF автоматически проверит формат email, диапазоны для целых чисел и строковые ограничения.
2. Методы-валидаторы на уровне полей (validate_field_name)
Функция create_engine() в SQLAlchemy
Движок SQLAlchemy создаётся функцией create_engine() из модуля sqlalchemy. Это основной компонент для работы с базами данных.
Базовое использование
from sqlalchemy import create_engine
# PostgreSQL
engine = create_engine("postgresql://user:password@localhost:5432/mydb")
# SQLite
engine = create_engine("sqlite:///database.db")
# MySQL
engine = create_engine("mysql+pymysql://user:password@localhost:3306/mydb")
Структура URL подключения
dialect+driver://username:password@host:port/database
Параметры create_engine()
from sqlalchemy import create_engine
Принцип действия алгоритма двоичного поиска
Двоичный поиск (binary search) — это фундаментальный алгоритм поиска элемента в отсортированном массиве за логарифмическое время. Это один из самых эффективных алгоритмов в информатике.
Основной принцип
Алгоритм работает по принципу "разделяй и властвуй" — на каждом шаге он делит диапазон поиска пополам и определяет, в какой половине находится искомый элемент.
Пошаговое описание
Шаг 1: Определение границ
left = 0 # Левая граница
right = len(arr) - 1 # Правая граница
Шаг 2: Вычисление середины
mid = (left + right) // 2 # Индекс середины
Шаг 3: Сравнение
if arr[mid] == target:
return mid # Элемент найден
elif arr[mid] < target:
left = mid + 1 # Ищем в правой половине
else:
right = mid - 1 # Ищем в левой половине
Шаг 4: Повторение
Процесс повторяется, пока не найдем элемент или диапазон не исчерпается.
Полная реализация
Какие методы HTTP передают тело запроса
В HTTP тело запроса (request body) может передаваться различными методами, но не все методы предназначены для этого. Разберу каждый.
1. POST
Основной метод для отправки данных.
Назначение: создание нового ресурса на сервере.
import requests
# JSON body
response = requests.post(
"https://api.example.com/users",
json={"name": "John", "email": "john@example.com"}
)
# Эквивалентно
response = requests.post(
"https://api.example.com/users",
headers={"Content-Type": "application/json"},
data='{"name": "John", "email": "john@example.com"}'
)
# Form data
response = requests.post(
"https://api.example.com/login",
data={"username": "john", "password": "secret"}
)
# Файл
with open('file.pdf', 'rb') as f:
response = requests.post(
"https://api.example.com/upload",
files={"file": f}
)
Идемпотентность: ❌ НЕ идемпотентный (несколько вызовов = несколько ресурсов).
Система контроля версий: Git
Мой основной выбор — Git, универсальная система распределённого контроля версий. Её использую для всех проектов: от личных экспериментов до enterprise-приложений.
Почему Git?
Распределённость — каждый разработчик имеет полную историю репозитория локально. Это означает, что можно работать автономно, без постоянного подключения к центральному серверу.
Производительность — все операции выполняются локально с минимальной задержкой:
Ветвление и слияние — система веток в Git настолько лёгкая, что позволяет создавать отдельные ветки для каждой задачи:
# Создание и переключение на новую ветку
$ git checkout -b feature/user-authentication
$ git checkout -b bugfix/memory-leak
$ git checkout -b docs/api-documentation
Какие тесты я умею писать
Тестирование — это искусство и наука. Я имею опыт написания различных видов тестов и понимаю когда и какой тип использовать. Давайте разберемся во всех видах тестов.
Пирамида тестов
E2E (End-to-End) - 5-10%
/\
/ \
/ \ Integration Tests - 15-20%
/ \
/ \ Unit Tests - 70-80%
1. Unit Tests (Модульные тесты)
Цель: Тестировать отдельные функции/методы в изоляции
import pytest
from unittest.mock import Mock, patch
from myapp.services import UserService
from myapp.models import User
Query-параметры в веб-фреймворках
Query-параметры (параметры строки запроса) — это переменные данные, передаваемые в URL адресе после символа ?. Они используются для фильтрации, сортировки и настройки поведения запроса.
Назначение query-параметров
Фильтрация данных:
GET /api/users?status=active — получить только активных пользователейGET /api/posts?author_id=123 — посты конкретного автораGET /api/products?min_price=100&max_price=500 — товары в диапазоне ценПоиск:
GET /api/search?q=python&limit=10 — поиск по ключевому словуGET /api/articles?tag=django — статьи с определённым тегомПагинация:
GET /api/posts?page=2&per_page=20 — вторая страница с 20 элементамиGET /api/comments?offset=100&limit=50 — с пропуском первых 100Сортировка:
GET /api/products?sort=price&order=asc — сортировка по цене по возрастаниюGET /api/posts?sort=-created_at — новые посты первымиDjango: работа с query-параметрами
Как добавляешь URL в Django
Добавление URL в Django осуществляется через систему маршрутизации на основе файлов urls.py. Это мощный и гибкий механизм, который позволяет связать URL-пути с view функциями или классами.
Основные концепции
URL routing в Django работает через сопоставление URL-паттерна с view функцией или классом. Главный файл urls.py обычно находится в папке проекта (конфигурация Django), а отдельные приложения имеют свои файлы urls.py.
Структура проекта
project/
├── project/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py ← Главный файл маршрутизации
│ └── wsgi.py
└── myapp/
├── migrations/
├── templates/
├── views.py
├── urls.py ← URL для приложения
└── models.py
1. Основной файл urls.py (проекта)
# project/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
Локальные vs Глобальные переменные
Это фундаментальная концепция scope (области видимости) в Python. Область видимости определяет, где можно использовать переменную.
Глобальная переменная
global_var = 100 # Доступна везде в модуле
def function1():
print(global_var) # 100 — может читать
def function2():
print(global_var) # 100 — может читать
function1()
function2()
print(global_var) # 100 — доступна на уровне модуля
Характеристики глобальной переменной:
Локальная переменная
def function():
local_var = 50 # Существует только внутри функции
print(local_var)
function() # 50
print(local_var) # NameError: переменная не существует
Почему хочешь сменить компанию?
Это хороший вопрос, который раскрывает не просто недовольство, а карьерные амбиции. Отвечу честно и конструктивно.
Текущая ситуация
В текущей компании я получил отличный опыт: работал с фреймворками на уровне продакшена (Django, FastAPI), оптимизировал критичные системы, участвовал в архитектурных решениях. Это был профессиональный рост.
Почему хочу двигаться дальше
1. Технологические вызовы
Большинство текущих проектов — поддержка legacy-кода. Хочу работать с современным стеком, где тестирование, CI/CD и документация — часть культуры с начала, не костыль в конце.
2. Масштаб и влияние
В текущей компании работаю над внутренними инструментами. Хочу создавать продукты, которые используют тысячи людей. Интересует опыт работы с системами, где производительность и надёжность — не опция.
Виртуальное окружение (Virtual Environment)
Виртуальное окружение (venv) — это изолированная копия Python интерпретатора с собственным набором установленных пакетов. Это позволяет разным проектам использовать разные версии одних и тех же библиотек без конфликтов.
Проблема без виртуального окружения
Все пакеты устанавливаются в глобальную директорию Python:
$ python -m pip install django==3.2
# Установлена глобально в /usr/local/lib/python3.11/site-packages/
$ python -m pip install django==4.2
# Перезаписана на новую версию
# Теперь старый проект, использующий Django 3.2, сломан!
Проблемы:
Решение: Виртуальное окружение
Каждый проект получает собственную копию Python и пакетов:
Зачем нужен try except?
Try-except — это механизм обработки ошибок (исключений) в Python. Без него программа падает при любой ошибке.
Проблема без try-except
# БЕЗ try-except
def divide(a, b):
return a / b
result = divide(10, 0) # ZeroDivisionError: division by zero
print("Программа упала") # Этот код НЕ выполнится
# Программа прерывается здесь!
Решение с try-except
# С try-except
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
print("Нельзя делить на ноль!")
return None
result = divide(10, 0)
print("Программа продолжает работу") # Это выполнится!
Основные случаи использования
Зачем нужна каждая структура данных
Каждая структура данных оптимизирована для определённых операций и сценариев использования. Выбор правильной структуры напрямую влияет на производительность и читаемость кода.
List (Список)
Назначение: упорядоченная коллекция элементов с быстрым доступом по индексу.
# Быстрый доступ O(1)
arr = [1, 2, 3, 4, 5]
print(arr[2]) # 3
# Итерация O(n)
for item in arr:
print(item)
# Медленный поиск O(n) и вставка O(n)
arr.insert(0, 0) # вставка в начало — сдвиг всех элементов
Когда использовать: когда нужен произвольный доступ и итерация, данные меняются часто.
Tuple (Кортеж)
Назначение: неизменяемая последовательность, используется как ключ в словарях.
# Неизменяемость
coord = (10, 20)
coord[0] = 5 # TypeError
# Как ключ
locations = {
(0, 0): "начало",
(10, 20): "точка A",
(5, 15): "точка B"
}
print(locations[(0, 0)]) # начало
Разница между POST и PUT
ПОСТ и PUT — это два разных HTTP метода, и хотя оба отправляют данные на сервер, они используются в совершенно разных сценариях.
POST — Создание нового ресурса
ПОСТ используется для отправки данных, которые изменяют состояние сервера. Обычно это создание нового ресурса.
import requests
# Создание новой записи
response = requests.post(
"https://api.example.com/users",
json={
"name": "Ivan Petrov",
"email": "ivan@example.com"
}
)
# Response: 201 Created
# {
# "id": 123,
# "name": "Ivan Petrov",
# "email": "ivan@example.com"
# }
Важные свойства POST:
PUT — Полное обновление существующего ресурса
Разница между == и is
Это один из фундаментальных вопросов в Python, касающихся сравнения объектов. Оба оператора выполняют сравнение, но работают они совершенно по-разному.
Определение
== сравнивает значения объектов. Он проверяет, содержат ли объекты одинаковые данные.
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True (значения одинаковые)
print(a is b) # False (это разные объекты в памяти)
Оператор == вызывает магический метод __eq__ и зависит от его реализации в классе:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
if not isinstance(other, Person):
return False
return self.name == other.name and self.age == other.age
alice1 = Person('Alice', 30)
alice2 = Person('Alice', 30)
bob = Person('Bob', 25)
Mixin как подход наследования
Mixin — это спорный, но полезный подход наследования в Python. Вопрос о его «правильности» зависит от контекста и того, как он используется.
Что такое Mixin?
Mixin — это класс, который предоставляет дополнительную функциональность, но не предназначен для самостоятельного использования. Он содержит методы и атрибуты, которые могут быть переиспользованы в других классах через множественное наследование.
Пример Mixin
class TimestampMixin:
"""Добавляет временные метки к объектам"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.created_at = datetime.now()
self.updated_at = datetime.now()
def mark_updated(self):
self.updated_at = datetime.now()
class JsonSerializableMixin:
"""Добавляет сериализацию в JSON"""
def to_json(self):
return json.dumps(self.__dict__, default=str)
Шифровальщик (Encryptor)
Шифровальщик — это механизм, алгоритм или программное средство, предназначенное для преобразования открытых (читаемых) данных в зашифрованный (нечитаемый без ключа) вид. В контексте Python и разработки, это может означать как криптографические функции для защиты информации, так и паттерны проектирования для преобразования данных.
Криптографическое шифрование
Криптографический шифровальщик использует математические алгоритмы для преобразования данных так, чтобы их можно было прочитать только с использованием специального ключа.
# Пример симметричного шифрования с использованием Fernet
from cryptography.fernet import Fernet
# Генерация ключа
key = Fernet.generate_key()
cipher = Fernet(key)
# Шифрование данных
plaintext = b"Secret message"
encrypted = cipher.encrypt(plaintext)
print(f"Зашифровано: {encrypted}")
# Расшифровка данных
decrypted = cipher.decrypt(encrypted)
print(f"Расшифровано: {decrypted.decode()}")
Что такое изменяемый объект в Python?
Изменяемый объект (Mutable Object) в Python — это объект, содержимое которого можно изменять после его создания, не создавая новый объект. Это фундаментальное понятие в Python, которое влияет на поведение переменных, функций и структур данных.
Определение и суть
Когда говорят об изменяемости, речь идёт о возможности модифицировать содержимое объекта в памяти. При изменении изменяемого объекта его идентификатор (id) остаётся прежним:
# Список (list) - изменяемый объект
my_list = [1, 2, 3]
original_id = id(my_list)
# Изменяем содержимое
my_list.append(4)
my_list[0] = 100
# id не изменился - это тот же объект
print(id(my_list) == original_id) # True
print(my_list) # [100, 2, 3, 4]
Изменяемые типы данных в Python
# Списки полностью изменяемы
my_list = [1, 2, 3]
# Изменение элемента
my_list[0] = 10
# Добавление элемента
my_list.append(4)
# Удаление элемента
my_list.pop(1)
TCP и IP протоколы
TCP/IP — это набор протоколов, который позволяет компьютерам взаимодействовать в Интернете. IP отвечает за доставку пакетов между компьютерами, а TCP гарантирует, что данные доходят в правильном порядке и без потерь.
IP (Internet Protocol)
IP — это сетевой протокол, который отвечает за маршрутизацию пакетов данных от отправителя к получателю через сеть.
Основные характеристики:
Версии IP:
IPv4: 192.168.1.1 (4 октета, 32 бита)
IPv6: 2001:0db8:85a3::8a2e:0370:7334 (128 бит, больше адресов)
Как работает IP:
Что такое Global Interpreter Lock в Python
Global Interpreter Lock (GIL) — это мьютекс (взаимное исключение), который защищает доступ к объектам в CPython (стандартной реализации Python). GIL гарантирует, что только один поток может выполнять байт-код Python одновременно, даже на многоядерных системах.
Почему существует GIL
CPython управляет памятью через reference counting (подсчёт ссылок). Каждый объект имеет счётчик ссылок:
import sys
x = [1, 2, 3]
print(sys.getrefcount(x)) # Количество ссылок на объект
y = x # Увеличивает счётчик
del x # Уменьшает счётчик
# Когда счётчик = 0, объект удаляется из памяти
Если несколько потоков будут одновременно изменять счётчик, возникнет race condition. Вместо того чтобы использовать дорогой мьютекс для каждого объекта, CPython использует один глобальный мьютекс — GIL.
Последствия GIL
Многопоточность не даёт параллелизм для CPU-bound операций:
import threading
import time
asyncio блокировка при синхронном чтении файла
Это критическая проблема в asyncio приложениях. Синхронное чтение файла замораживает ВСЕ корутины.
Проблема: блокировка event loop
import asyncio
async def read_large_file():
"""❌ НЕПРАВИЛЬНО: блокирует весь event loop"""
with open('huge_file.bin', 'rb') as f:
data = f.read() # БЛОКИРУЕТ! (может быть 1+ секунду)
return len(data)
async def fast_operation():
"""Эта корутина ЗАВИСНЕТ, пока читаем файл"""
print("Start")
await asyncio.sleep(0.1) # Хотим 0.1 сек
print("Done") # Но выведется ДО ТОГО как файл прочитается!
async def main():
# Запускаем обе корутины параллельно
results = await asyncio.gather(
read_large_file(),
fast_operation()
)
return results
# Время выполнения:
# read_large_file: 2 сек (читает файл)
# fast_operation: ЖДЕТ 2 сек (хотя нужно только 0.1)
# Итого: ~2 сек вместо параллельного выполнения
Что происходит в event loop
Развертывание Python проекта в интернете
Это один из самых важных практических навыков. За 10+ лет работал с разными хостингами и подходами.
Стек технологий для production
Основные компоненты
Интернет
↓
HTTPS Load Balancer (nginx, AWS ALB)
↓
APP Server (Gunicorn, uWSGI, Uvicorn)
↓
Python приложение (Django, FastAPI)
↓
Database (PostgreSQL)
↓
Cache (Redis)
1. Веб-сервер приложения (WSGI/ASGI)
Gunicorn (для Django, Flask — WSGI)
# Установка
pip install gunicorn
# Запуск
gunicorn config.wsgi:application --workers 4 --threads 2 --bind 0.0.0.0:8000
# workers: количество process'ов (рекомендация: количество CPU cores)
# threads: потоки per worker
# bind: адрес и порт
# Systemd сервис
Файл /etc/systemd/system/myapp.service
[Unit]
Description=My Python App
After=network.target
Передача статики клиенту на продакшене
Вопрос о доставке статических файлов (CSS, JavaScript, изображений, шрифтов) — один из ключевых аспектов архитектуры веб-приложений. За 10+ лет я использовал разные подходы на разных этапах развития приложений.
Почему это важно
Проблема: Python/Django/FastAPI обработка статики — это напрасная трата ресурсов
- Django в продакшене НЕ должен обрабатывать статику
- Каждый запрос к изображению = блокирует воркер
- На 100 одновременных пользователей это заметно
Решение 1: Nginx/Apache (самое популярное)
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name myapp.com;
client_max_body_size 50M;
# Статика: Nginx обрабатывает напрямую (очень быстро)
location /static/ {
alias /var/www/myapp/static/;
expires 30d; # Кэширование на клиенте
add_header Cache-Control "public, immutable";
}
location /media/ {
alias /var/www/myapp/media/;
expires 7d;
}