РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ Data Scientist / ML Engineer RaccoonSoft - Junior 90+ тыс
Сегодня мы разберем спокойное и доброжелательное техническое собеседование на джун-ML/ Python позицию, в котором интервьюер аккуратно прощупывает базу кандидата: от Python и ООП до базовых алгоритмов, SQL, NumPy/Pandas и основ машинного обучения. В ходе диалога видно, что кандидат ориентируется в ключевых инструментах и учебных задачах, но местами дает поверхностные или неточные ответы, что подчёркивает формат встречи как «growth potential» интервью, а не жёсткий отбор.
Вопрос 1. Знаком ли ты с тестированием кода и подходом разработки через тестирование (TDD)?
Таймкод: 00:01:36
Ответ собеседника: неполный. Кандидат знаком с тестированием скриптов и функций, но систематически этим не занимался; про TDD напрямую не знает, хотя сталкивался с задачами, где сначала были тесты, а затем он писал решение.
Правильный ответ:
В промышленной разработке на Go тестирование — обязательная часть процесса, а не опция. Важно понимать несколько уровней: unit-тесты, интеграционные тесты, end-to-end тесты, property-based и нагрузочное тестирование, а также подходы вроде TDD.
Основные подходы к тестированию:
-
Unit-тесты:
- Тестируют отдельные функции/методы в изоляции.
- Не ходят во внешние сервисы (БД, сети, файловая система); все внешнее выносится в интерфейсы и подменяется моками/стабами.
- Быстрые, запускаются постоянно (locally, pre-commit, CI).
-
Интеграционные тесты:
- Проверяют связку компонентов: сервис + БД, сервис + очередь, HTTP API и т.д.
- Используют реальные зависимости или их максимально приближенные аналоги (docker-compose в CI, testcontainers).
- Медленнее, запускаются реже, но критичны для уверенности в реальной работоспособности.
-
End-to-end (E2E) и системные тесты:
- Проверяют полный сценарий работы системы “как пользователь”.
- Часто пишутся поверх API/UI.
- Дороже и медленнее, но ловят проблемы, которые не видны на unit-уровне.
-
Property-based тестирование:
- Вместо фиксированных кейсов формулируются свойства, которые должны выполняться для множества входных данных.
- Используется для сложных алгоритмов, парсеров, трансформаций данных.
- В Go — библиотеки вроде
github.com/leanovate/gopterилиtesting/quick.
-
Нагрузочное и перформанс-тестирование:
- Проверяют поведение под нагрузкой, latency, throughput, конкуренцию.
- Для Go —
go test -bench,pprof, внешние инструменты (k6, vegeta, wrk).
Подход TDD (Test-Driven Development):
Суть не в тестах как таковых, а в цикле:
-
Написать падающий тест (red):
- Описать желаемое поведение функции/компонента.
- Тест должен явно проваливаться, потому что реализации еще нет.
-
Реализовать минимальный код (green):
- Написать простейшую реализацию, чтобы тест прошел.
- Не думать сразу о идеальной архитектуре, только корректность.
-
Рефакторинг (refactor):
- Упростить, обобщить, сделать код чище.
- Все тесты должны оставаться зелеными.
Что дает TDD:
- Дизайн через использование: API проектируется “глазами потребителя”.
- Меньше скрытых зависимостей и побочных эффектов.
- Выше покрытие тестами и предсказуемость изменений.
- Легче рефакторить — тесты являются “страховкой”.
Пример unit-теста на Go:
// file: sum.go
package calc
func Sum(a, b int) int {
return a + b
}
// file: sum_test.go
package calc
import "testing"
func TestSum(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"zero", 0, 0, 0},
{"negative", -1, -2, -3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if res := Sum(tt.a, tt.b); res != tt.expected {
t.Fatalf("expected %d, got %d", tt.expected, res)
}
})
}
}
Пример интеграционного теста с БД (SQL):
// Псевдо-пример, упрощенно
func TestUserRepository_CreateAndGet(t *testing.T) {
db, cleanup := setupTestDB(t) // поднимаем тестовую БД (docker/testcontainers)
defer cleanup()
repo := NewUserRepository(db)
ctx := context.Background()
u := User{Name: "John", Email: "john@example.com"}
if err := repo.Create(ctx, &u); err != nil {
t.Fatalf("create user: %v", err)
}
got, err := repo.GetByEmail(ctx, "john@example.com")
if err != nil {
t.Fatalf("get user: %v", err)
}
if got.Email != u.Email {
t.Fatalf("expected email %s, got %s", u.Email, got.Email)
}
}
Соответствующий SQL (миграция для тестовой БД):
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);
Практические моменты в Go:
- Использовать стандартный пакет
testingкак основу. - Для моков — интерфейсы + самописные стабы или библиотеки (
gomock,testify/mock). - Группировка кейсов через
t.Run, использованиеt.Helper()для вспомогательных функций. - Запуск тестов:
go test ./.... - Интеграция в CI/CD: тесты должны запускаться автоматически на каждом коммите/PR.
- Покрытие:
go test ./... -cover -coverprofile=coverage.out.
Важно не просто “знать про тесты”, а системно применять:
- Писать тесты до или во время реализации критичных компонентов.
- Декомпозировать функционал так, чтобы его можно было протестировать.
- Минимизировать глобальное состояние и жесткие зависимости.
Такое отношение к тестированию делает код надежным, сопровождаемым и предсказуемым при изменениях.
Вопрос 2. Что ты знаешь о шаблонах проектирования и применяешь ли их на практике?
Таймкод: 00:02:25
Ответ собеседника: неправильный. Кандидат ассоциирует шаблоны проектирования в основном с ООП и общими принципами, признаёт, что почти не изучал их и не применяет осознанно, ссылаясь на фокус на ML, а не на разработку.
Правильный ответ:
Шаблоны проектирования — это не про «ООП из книжки», а про типовые архитектурные решения, которые помогают делать код удобнее для сопровождения, расширения и тестирования. В Go их важно понимать не формально по “Gang of Four”, а с учетом особенностей языка: интерфейсы, композиция, функции как значения, простые структуры, конкуренция через goroutine и каналы.
Ключевые идеи использования шаблонов в Go:
- Не навешивать паттерны ради паттернов.
- Использовать их как язык общения в команде: “тут у нас Strategy”, “здесь Builder”, “это Decorator”.
- Подбирать решения под стиль Go: простота, явность, минимализм, композиция вместо сложных иерархий.
Ниже — обзор основных шаблонов и то, как они выглядят в Go с примерами.
Порождающие шаблоны:
-
Factory (Simple Factory / Factory Method)
- Когда:
- Нужно создавать объекты с инкапсулированной логикой (конфигурация, валидация, выбор конкретной реализации).
- В Go:
- Часто просто функция-конструктор, возвращающая интерфейс.
Пример:
type Storage interface {
Save(ctx context.Context, key string, data []byte) error
}
type S3Storage struct { /* ... */ }
type FileStorage struct { /* ... */ }
func NewStorage(kind string) (Storage, error) {
switch kind {
case "s3":
return NewS3Storage(/* cfg */), nil
case "file":
return NewFileStorage(/* cfg */), nil
default:
return nil, fmt.Errorf("unknown storage type: %s", kind)
}
} - Когда:
-
Builder
- Когда:
- Объект сложный, много опций, не хочется перегружать конструкторы.
- В Go:
- Часто реализуется через функциональные опции.
Пример:
type Server struct {
addr string
timeout time.Duration
logger Logger
}
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) { s.timeout = d }
}
func WithLogger(l Logger) Option {
return func(s *Server) { s.logger = l }
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{
addr: addr,
timeout: 5 * time.Second,
logger: defaultLogger,
}
for _, opt := range opts {
opt(s)
}
return s
} - Когда:
Структурные шаблоны:
-
Adapter
- Когда:
- Нужно подружить несовместимые интерфейсы (например, внешняя библиотека и ваш код).
- В Go:
- Реализуется тонкой оберткой, удовлетворяющей нужному интерфейсу.
Пример:
type Logger interface {
Info(msg string)
}
type ZapAdapter struct {
l *zap.SugaredLogger
}
func (z ZapAdapter) Info(msg string) {
z.l.Infow(msg)
} - Когда:
-
Decorator
- Когда:
- Нужно добавить поведение (логирование, метрики, кэширование, ретраи) без изменения основной реализации.
- В Go:
- Очень распространен для репозиториев, клиентов, хендлеров.
Пример:
type UserRepo interface {
Get(ctx context.Context, id int64) (*User, error)
}
type loggingUserRepo struct {
next UserRepo
logger Logger
}
func (r loggingUserRepo) Get(ctx context.Context, id int64) (*User, error) {
start := time.Now()
user, err := r.next.Get(ctx, id)
r.logger.Info(fmt.Sprintf("Get user=%d took=%s err=%v", id, time.Since(start), err))
return user, err
}
func WithLogging(repo UserRepo, logger Logger) UserRepo {
return loggingUserRepo{next: repo, logger: logger}
} - Когда:
-
Facade
- Когда:
- Есть сложная подсистема, и вы хотите дать простой API.
- В Go:
- Типичный паттерн для модулей: “внешний” пакет с простыми функциями скрывает сложную инфраструктуру.
- Когда:
Поведенческие шаблоны:
-
Strategy
- Когда:
- Нужно взаимозаменяемо использовать разные алгоритмы с одним интерфейсом.
- В Go:
- Или интерфейсы, или функции как значения.
Пример:
type SortStrategy func([]int)
func SortAscending(nums []int) { sort.Ints(nums) }
func SortDescending(nums []int) { sort.Sort(sort.Reverse(sort.IntSlice(nums))) }
func Process(nums []int, strategy SortStrategy) {
strategy(nums)
// дальше работа с отсортированными данными
} - Когда:
-
Observer (Event-driven)
- Когда:
- Надо уведомлять множество подписчиков об изменении состояния.
- В Go:
- Часто реализуется через каналы или callback-и.
Пример (упрощенно):
type Event struct {
Type string
Data any
}
type Bus struct {
subs []chan Event
}
func (b *Bus) Subscribe() <-chan Event {
ch := make(chan Event, 10)
b.subs = append(b.subs, ch)
return ch
}
func (b *Bus) Publish(e Event) {
for _, ch := range b.subs {
select {
case ch <- e:
default:
}
}
} - Когда:
-
Chain of Responsibility
- Когда:
- Надо пропустить запрос через цепочку обработчиков (валидация, авторизация, логирование).
- В Go:
- Очень естественно смотрится на middleware в HTTP.
Пример:
type Middleware func(http.Handler) http.Handler
func Chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
} - Когда:
Шаблоны и SQL/хранилища:
-
Repository:
- Инкапсулирует доступ к данным, не размывая SQL по коду.
- Облегчает тестирование (моки интерфейсов).
type User struct {
ID int64
Name string
Email string
}
type UserRepository interface {
Create(ctx context.Context, u *User) error
GetByID(ctx context.Context, id int64) (*User, error)
}
type userRepo struct {
db *sql.DB
}
func (r *userRepo) Create(ctx context.Context, u *User) error {
query := `INSERT INTO users(name, email) VALUES($1, $2) RETURNING id`
return r.db.QueryRowContext(ctx, query, u.Name, u.Email).Scan(&u.ID)
}
func (r *userRepo) GetByID(ctx context.Context, id int64) (*User, error) {
var u User
query := `SELECT id, name, email FROM users WHERE id = $1`
err := r.db.QueryRowContext(ctx, id).Scan(&u.ID, &u.Name, &u.Email)
if err != nil {
return nil, err
}
return &u, nil
}SQL-схема:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);
Ключевые практические моменты:
- Шаблоны — это инструмент для:
- осознанного разделения ответственностей;
- уменьшения связности (coupling) и повышения тестируемости;
- гибкости к изменениям (новые алгоритмы, поставщики, хранилища).
- Важно:
- Понимать intent каждого паттерна: какую проблему он решает.
- Уметь узнавать паттерны в коде и объяснить, почему они применены.
- Сдержанно использовать: если решение можно выразить простым кодом без паттерна — так и делаем.
Осознанное владение шаблонами проектирования на практике показывает умение мыслить архитектурно, а не просто писать “чтобы работало”.
Вопрос 3. Объясни, как ты понимаешь принципы SOLID с примерами.
Таймкод: 00:03:21
Ответ собеседника: неполный. Кандидат знает, что SOLID содержит пять принципов, частично пояснил принцип единой ответственности и открытости/закрытости, но не смог внятно объяснить принцип подстановки Лисков и остальные принципы.
Правильный ответ:
SOLID — это набор принципов проектирования, цель которых сделать код:
- устойчивым к изменениям,
- простым для расширения,
- удобным для тестирования,
- понятным для команды.
Эти принципы применимы не только к классическому ООП, но и к Go, где мы работаем с композициями, интерфейсами, функциями и пакетами.
Разберем каждый принцип с акцентом на практику и примеры на Go.
- Single Responsibility Principle (SRP) — Принцип единой ответственности
Формулировка: Компонент (модуль, структура, функция, пакет) должен иметь только одну причину для изменения.
Суть:
- Не смешивать в одном месте разные области ответственности:
- бизнес-логика,
- логирование,
- работа с БД,
- HTTP,
- валидация,
- конфигурация и т.д.
- Это уменьшает связность и упрощает тестирование.
Пример (нарушение SRP):
type UserService struct {
db *sql.DB
logger *log.Logger
}
func (s *UserService) Register(ctx context.Context, name, email string) error {
// валидация
if name == "" || email == "" {
return errors.New("invalid input")
}
// логирование
s.logger.Printf("register user %s", email)
// SQL (доступ к БД)
_, err := s.db.ExecContext(ctx,
`INSERT INTO users(name, email) VALUES($1, $2)`, name, email)
return err
}
Здесь один компонент отвечает за:
- бизнес-правила,
- логирование,
- доступ к БД.
Лучше разделить: выносим хранилище и логгер в зависимости:
type UserRepository interface {
Create(ctx context.Context, name, email string) error
}
type UserService struct {
repo UserRepository
logger Logger
}
func (s *UserService) Register(ctx context.Context, name, email string) error {
if name == "" || email == "" {
return errors.New("invalid input")
}
s.logger.Info("register user " + email)
return s.repo.Create(ctx, name, email)
}
Теперь:
- UserService отвечает за бизнес-логику регистрации.
- Repository — за работу с БД.
- Логгер — за логирование.
- Все части легче тестировать и менять независимо.
- Open/Closed Principle (OCP) — Принцип открытости/закрытости
Формулировка: Модуль должен быть открыт для расширения, но закрыт для изменения.
Суть:
- Чтобы добавить новое поведение, мы должны по возможности не менять существующий код (особенно стабилизированный), а расширять его через:
- интерфейсы,
- композицию,
- конфигурацию,
- плагины/стратегии.
Пример: Допустим, у нас есть отправка уведомлений.
Анти-подход:
func SendNotification(kind, to, msg string) error {
switch kind {
case "email":
// отправляем email
case "sms":
// отправляем sms
default:
return fmt.Errorf("unsupported kind: %s", kind)
}
return nil
}
Каждый новый тип уведомления — правка switch, пересборка, риск поломок.
Лучше через интерфейс:
type Notifier interface {
Notify(to, msg string) error
}
type EmailNotifier struct { /* ... */ }
func (n EmailNotifier) Notify(to, msg string) error { /* ... */ return nil }
type SMSNotifier struct { /* ... */ }
func (n SMSNotifier) Notify(to, msg string) error { /* ... */ return nil }
func SendWelcome(n Notifier, to string) error {
return n.Notify(to, "Welcome!")
}
Теперь:
- Чтобы добавить
SlackNotifier, мы не меняемSendWelcome, а просто реализуем новый Notifier. - Это и есть OCP в духе Go.
- Liskov Substitution Principle (LSP) — Принцип подстановки Лисков
Формулировка (адаптация для Go): Любая реализация интерфейса должна быть взаимозаменяемой без нарушения ожидаемого поведения клиента.
Суть:
- Если у нас есть интерфейс, то любая его реализация:
- не должна ломать инварианты,
- не должна требовать дополнительных предусловий,
- не должна возвращать неожиданные нарушения контракта.
- В Go это критично, потому что интерфейсы неявные: важно документировать поведение, а не только сигнатуры.
Пример нарушения:
type Cache interface {
Get(key string) (string, error)
}
type MapCache struct {
m map[string]string
}
func (c MapCache) Get(key string) (string, error) {
v, ok := c.m[key]
if !ok {
return "", errors.New("not found")
}
return v, nil
}
type PanicCache struct{}
func (PanicCache) Get(key string) (string, error) {
panic("key not found") // жестко
}
Оба типа реализуют интерфейс Cache, но PanicCache нарушает ожидаемое поведение:
- Клиент ожидает error при отсутствии ключа,
- Вместо этого получит панику.
LSP в Go:
- Интерфейсы должны описывать не только сигнатуру, но и семантику (контракт).
- Реализация не должна усиливать предусловия и ломать этот контракт.
- Interface Segregation Principle (ISP) — Принцип разделения интерфейсов
Формулировка: Клиенты не должны зависеть от интерфейсов, которые они не используют.
Суть:
- Лучше несколько маленьких интерфейсов, чем один “толстый”.
- Это идеально ложится на философию Go.
Плохой пример:
type Repository interface {
Create(ctx context.Context, u *User) error
Get(ctx context.Context, id int64) (*User, error)
Update(ctx context.Context, u *User) error
Delete(ctx context.Context, id int64) error
}
Если где-то нужен только Get, приходится тянуть весь интерфейс.
Лучше так:
type UserCreator interface {
Create(ctx context.Context, u *User) error
}
type UserReader interface {
Get(ctx context.Context, id int64) (*User, error)
}
type UserUpdater interface {
Update(ctx context.Context, u *User) error
}
type UserDeleter interface {
Delete(ctx context.Context, id int64) error
}
И собирать композиции там, где нужно.
Плюсы:
- Меньше фиктивных зависимостей.
- Проще мокать конкретные сценарии в тестах.
- Код чище и стабильнее при изменениях.
- Dependency Inversion Principle (DIP) — Принцип инверсии зависимостей
Формулировка:
- Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба типа модулей должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей, детали должны зависеть от абстракций.
В Go:
- Используем интерфейсы и конструкторы, чтобы не "вшивать" конкретные реализации внутрь бизнес-логики.
- Верхний слой (domain / use-cases) определяет интерфейсы.
- Инфраструктура (БД, HTTP-клиенты, очереди) эти интерфейсы реализует.
Пример:
// уровень домена
type Mailer interface {
SendResetPassword(email, token string) error
}
type PasswordService struct {
mailer Mailer
}
func (s *PasswordService) SendResetLink(email string) error {
token := generateToken()
return s.mailer.SendResetPassword(email, token)
}
// инфраструктура
type SMTPMailer struct {
// конфиг, клиент
}
func (m *SMTPMailer) SendResetPassword(email, token string) error {
// отправка через SMTP
return nil
}
Связка:
func NewApp() *PasswordService {
smtpMailer := &SMTPMailer{/*...*/}
return &PasswordService{mailer: smtpMailer}
}
DIP на практике:
- Легко подменять зависимости в тестах (моки вместо реальных SMTP/БД).
- Легко менять реализацию без переписывания бизнес-логики (SMTP → SendGrid → Kafka и т.д.).
- Архитектура становится устойчивее к изменениям инфраструктуры.
Кратко о применении SOLID в Go:
- Не воспринимать SOLID как теорию из OOP-учебников.
- В Go это выражается через:
- маленькие, точные интерфейсы;
- явные зависимости через конструкторы;
- композицию вместо наследования;
- разделение зон ответственности по пакетам и слоям;
- понятные контракты для интерфейсов (LSP).
- Цель — не “соблюсти букву SOLID”, а сделать код:
- легко тестируемым,
- простым для изменения,
- предсказуемым под нагрузкой реального проекта.
Вопрос 4. В чём разница между оператором "is" и оператором "==" в Python?
Таймкод: 00:05:15
Ответ собеседника: правильный. Оператор is проверяет, ссылаются ли переменные на один и тот же объект в памяти, тогда как == сравнивает равенство значений.
Правильный ответ:
В Python:
-
==:- Логическое сравнение значений.
- Вызывает метод
__eq__объекта (если он определён). - Два разных объекта могут быть равны по значению, но не быть одним и тем же объектом.
-
is:- Проверка идентичности объекта.
- Возвращает
True, если обе ссылки указывают на один и тот же объект (один и тот же адрес в памяти). - Используется для:
- сравнения с синглтонами (
None,True,False, иногдаEllipsis); - проверок, что это именно тот же объект, а не только равный по значению.
- сравнения с синглтонами (
Примеры:
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True - значения совпадают
print(a is b) # False - разные объекты в памяти
c = a
print(c is a) # True - одна и та же ссылка
x = None
print(x is None) # Правильно
print(x == None) # Работает, но стиль хуже
Особенность с интернированием (например, маленькие целые числа, строки):
Иногда is даёт True «случайно» из-за оптимизаций интерпретатора, но полагаться на это для сравнения значений нельзя:
a = 256
b = 256
print(a is b) # Может быть True из-за интернирования
a = 1000
b = 1000
print(a is b) # Зависит от реализации, использовать так нельзя
Практический вывод:
- Для сравнения значений использовать
==. - Для проверки на
Noneи идентичность объекта использоватьis. - Не использовать
isдля сравнения обычных значений (строки, числа, коллекции) — это логическая ошибка.
Вопрос 5. В чём разница между методами init и new в Python?
Таймкод: 00:05:37
Ответ собеседника: правильный. new создаёт новый объект, init инициализирует уже созданный; порядок вызова: сначала new, затем init.
Правильный ответ:
В Python создание объекта — это двухшаговый процесс: сначала объект создаётся, затем инициализируется. За это отвечают два разных механизма:
__new__(cls, *args, **kwargs)— создание объекта__init__(self, *args, **kwargs)— инициализация объекта
Основные различия:
-
__new__:- Статически привязанный (по сути, это метод класса).
- На вход получает класс (
cls) и аргументы конструктора. - Обязан вернуть новый объект (обычно через
super().__new__(cls)). - Вызывается первым при создании экземпляра.
- Используется, когда нужно контролировать процесс создания:
- неизменяемые типы (int, str, tuple);
- синглтоны, пулы объектов;
- фабричное поведение: вернуть экземпляр другого класса или кэшированный объект.
-
__init__:- Обычный метод экземпляра.
- На вход получает уже созданный объект (
self) и те же аргументы (как правило). - Ничего не должен возвращать (возврат игнорируется).
- Отвечает за установку состояния объекта: поля, проверки и т.п.
- Вызывается только если
__new__вернул объект данного класса. Если__new__вернул объект другого типа —__init__может не вызываться.
Простой пример стандартного поведения:
class User:
def __new__(cls, *args, **kwargs):
# Создание объекта (обычно так)
instance = super().__new__(cls)
return instance
def __init__(self, name: str):
# Инициализация уже созданного объекта
self.name = name
u = User("Alice")
print(u.name) # Alice
Пример, где нужен __new__: контроль создания (например, синглтон):
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, value):
# Будет вызываться при каждом создании, важно учитывать
self.value = value
a = Singleton(1)
b = Singleton(2)
print(a is b) # True — один и тот же объект
print(a.value) # 2
print(b.value) # 2
Пример с неизменяемым типом:
class MyInt(int):
def __new__(cls, value):
# Можем модифицировать создаваемое значение
return super().__new__(cls, value * 2)
def __init__(self, value):
# Для неизменяемых типов состояние задаётся в __new__,
# __init__ обычно не нужен или пустой.
pass
x = MyInt(5)
print(x) # 10
Ключевые практические выводы:
- В 99% случаев достаточно переопределять
__init__. __new__переопределяется, когда:- работаем с неизменяемыми типами;
- хотим управлять кэшированием/пулами/синглтонами;
- возможно, возвращаем объект другого класса.
- Важно помнить: если
__new__не возвращает экземпляр текущего класса,__init__может не быть вызван.
Вопрос 6. Что такое механизм slots в классах Python и для чего он используется?
Таймкод: 00:06:13
Ответ собеседника: неправильный. Кандидат не знал про slots; интервьюер пояснил, что это способ зафиксировать набор атрибутов.
Правильный ответ:
Механизм __slots__ в Python позволяет:
- Явно задать фиксированный набор атрибутов экземпляра.
- Убрать динамический словарь атрибутов (
__dict__) и, как следствие:- уменьшить потребление памяти,
- немного ускорить доступ к атрибутам,
- запретить создание произвольных новых полей "на лету".
По умолчанию у каждого экземпляра пользовательского класса есть __dict__, где хранятся его атрибуты. Это гибко, но неэффективно по памяти, особенно при большом количестве однотипных объектов.
Если определить __slots__, интерпретатор:
- Создаёт дескрипторы для указанных имён атрибутов.
- Не создаёт
__dict__(если явно не указано). - Не позволяет присваивать неописанные в
__slots__атрибуты (возникаетAttributeError).
Базовый пример:
class User:
__slots__ = ("id", "name", "email")
def __init__(self, user_id: int, name: str, email: str):
self.id = user_id
self.name = name
self.email = email
u = User(1, "Alice", "a@example.com")
u.name = "Bob" # ОК
u.age = 30 # AttributeError: 'User' object has no attribute 'age'
Что это даёт:
- Для больших коллекций объектов экономия памяти может быть значительной.
- Ускоряется доступ к атрибутам (нет словарного поиска, есть фиксированные слоты).
Важно понимать детали и ограничения:
-
Наследование:
- Если подкласс не определяет
__slots__, у него снова появится__dict__, и ограничения исчезнут. - Если определяет, нужно учитывать слоты родителя.
class Base:
__slots__ = ("id",)
class Child(Base):
__slots__ = ("name",)
c = Child()
c.id = 1
c.name = "Alice"
# c.__dict__ не существует - Если подкласс не определяет
-
__dict__и__weakref__:- Если нужно сохранить возможность динамически добавлять поля, можно явно добавить
"__dict__"в__slots__. - Для weakref-ов —
"__weakref__".
class Flexible:
__slots__ = ("id", "__dict__")
f = Flexible()
f.id = 1
f.extra = "ok" # теперь работает, т.к. есть __dict__ - Если нужно сохранить возможность динамически добавлять поля, можно явно добавить
-
Когда использовать:
- Классы, от которых создаются миллионы объектов (например, структуры данных, модели для in-memory обработок).
- Performance- и memory-sensitive участки кода.
- Если модель данных фиксирована и не предполагает динамических полей.
-
Когда не стоит:
- В прототипах и динамичных моделях, где добавляются поля на лету.
- Там, где важна гибкость больше, чем экономия памяти.
- В сложных иерархиях, где
__slots__делает код менее очевидным.
Кратко:
__slots__— инструмент оптимизации и ограничения структуры объектов.- Он фиксирует список допустимых атрибутов, уменьшает память и ускоряет доступ.
- Важно осознанно применять его там, где есть реальная нагрузка, а не “по привычке”.
Вопрос 7. Что делают функции map, filter и reduce и как ими правильно пользоваться?
Таймкод: 00:06:38
Ответ собеседника: неполный. Кандидат знает о существовании этих функций и что они используются для преобразований, но не помнит детали работы и не демонстрирует уверенное применение.
Правильный ответ:
Функции map, filter и reduce — это классические инструменты функционального стиля программирования для работы с коллекциями. Они помогают выразить операции над данными декларативно: что мы хотим сделать, а не как по шагам.
В общем виде:
map— трансформация элементов.filter— отбор элементов по условию.reduce— свёртка/агрегация всех элементов к одному значению.
Хотя в Python часто предпочтительнее использовать списковые включения и генераторы, понимание этих функций важно — они встречаются и в Python-коде, и в других языках / платформах (включая концептуальные аналоги в Go).
- map
Назначение:
- Применяет функцию к каждому элементу последовательности, возвращает новый итерируемый объект с результатами.
Сигнатура:
map(func, iterable, *iterables)
Пример:
nums = [1, 2, 3, 4]
# Удвоение каждого элемента:
res = list(map(lambda x: x * 2, nums))
# res = [2, 4, 6, 8]
С несколькими итерируемыми:
a = [1, 2, 3]
b = [10, 20, 30]
res = list(map(lambda x, y: x + y, a, b))
# res = [11, 22, 33]
Идиоматичный Python:
- В простых случаях лучше списковые включения:
res = [x * 2 for x in nums]
- filter
Назначение:
- Отбирает элементы, для которых функция-предикат возвращает
True.
Сигнатура:
filter(func, iterable)
Пример:
nums = [1, 2, 3, 4, 5, 6]
# Оставить только чётные:
res = list(filter(lambda x: x % 2 == 0, nums))
# res = [2, 4, 6]
Идиоматичный Python:
- Через генераторы:
res = [x for x in nums if x % 2 == 0]
- reduce
Назначение:
- Пошагово "сворачивает" последовательность в одно значение, применяя бинарную функцию.
Подключение:
- В Python 3 находится в
functools.
Сигнатура:
functools.reduce(func, iterable[, initializer])
Пример — сумма элементов:
from functools import reduce
nums = [1, 2, 3, 4]
total = reduce(lambda acc, x: acc + x, nums, 0)
# total = 10
Пример — конкатенация строк:
words = ["Go", "is", "fast"]
sentence = reduce(lambda acc, w: acc + " " + w, words)
# "Go is fast"
Типичный паттерн работы:
acc(accumulator) хранит промежуточный результат.- Для каждого элемента
xвычисляем новыйacc.
Во многих случаях reduce в Python менее читаем, чем явный цикл:
total = 0
for x in nums:
total += x
- Практические моменты и подводные камни
-
Читаемость:
- В Python сообщества часто предпочитает list/generator comprehensions вместо
map/filterсlambda, если это делает код понятнее.
- В Python сообщества часто предпочитает list/generator comprehensions вместо
-
map/filter без lambda:
- Можно передавать существующие функции:
res = list(map(str.upper, ["a", "b", "c"]))
# ["A", "B", "C"]
res = list(filter(str.isdigit, ["1", "x", "3"]))
# ["1", "3"]
- Ленивая семантика:
mapиfilterвозвращают итераторы в Python 3, вычисляются по мере обхода.
it = map(lambda x: x * 2, range(10**9))
# вычисление произойдёт только при итерировании
- Аналогии в Go (важно для мышления)
В Go нет встроенных map/filter/reduce как функций высшего порядка для слайсов, но подходы те же:
map→ цикл, создающий новый слайс.filter→ цикл с условием.reduce→ цикл с аккумулятором.
Пример map/filter/reduce на Go (явно):
// map: умножить каждый элемент на 2
func MapMul2(nums []int) []int {
res := make([]int, len(nums))
for i, v := range nums {
res[i] = v * 2
}
return res
}
// filter: оставить только чётные
func FilterEven(nums []int) []int {
res := make([]int, 0, len(nums))
for _, v := range nums {
if v%2 == 0 {
res = append(res, v)
}
}
return res
}
// reduce: сумма
func ReduceSum(nums []int) int {
sum := 0
for _, v := range nums {
sum += v
}
return sum
}
Ключевой вывод:
map,filter,reduce— базовые абстракции для обработки коллекций.- Нужно уверенно:
- знать их сигнатуры;
- понимать семантику;
- уметь показать пару простых примеров;
- осознавать, когда они уместны, а когда лучше явный цикл ради читаемости.
Вопрос 8. Для чего используется функция id() в Python?
Таймкод: 00:07:24
Ответ собеседника: неполный. Сначала отвечает расплывчато, затем после подсказки соглашается, что функция возвращает адрес (идентификатор) объекта в памяти.
Правильный ответ:
Функция id(obj) в Python возвращает уникальный идентификатор объекта на момент его существования.
Ключевые моменты:
-
id(x):- Возвращает целое число, которое:
- в CPython обычно соответствует адресу объекта в памяти;
- в других реализациях (PyPy, Jython и т.п.) может быть реализовано иначе, но гарантируется уникальность для "живых" объектов.
- Уникальность:
- Пока объект существует, его
idуникален среди всех текущих объектов. - После удаления объекта (GC) его идентификатор теоретически может быть переиспользован.
- Пока объект существует, его
- Возвращает целое число, которое:
-
Основные применения:
- Отладка:
- Проверить, ссылаются ли две переменные на один и тот же объект.
- Проследить, когда создаются/копируются/шарятся объекты.
- Демонстрация отличий между:
is(сравнение идентичности) и==(сравнение значений).- мутабельными и иммутабельными объектами при операциях.
- Отладка:
-
Важное замечание:
id()не следует использовать как бизнес-идентификатор объекта или ключ в системной логике.- Это внутренний технический идентификатор времени жизни объекта в рамках конкретного процесса интерпретатора.
Примеры:
a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(id(a), id(b), id(c))
# id(a) == id(b), потому что b ссылается на тот же объект
# id(c) отличается, хотя значения равны
print(a is b) # True
print(a is c) # False
print(a == c) # True
Пример для иллюстрации интернирования:
x = 256
y = 256
print(id(x), id(y)) # часто совпадают в CPython
x = 1000
y = 1000
print(id(x), id(y)) # могут быть разными
Практический вывод:
id()полезна для понимания/диагностики поведения ссылок и оптимизаций, но не должна использоваться как часть прикладной логики.
Вопрос 9. Как работает конструкция try-except-else в Python и когда выполняется блок else?
Таймкод: 00:08:07
Ответ собеседника: правильный. Кандидат верно передал суть: блок except выполняется при ошибке в try, блок else — если в блоке try исключения не произошло.
Правильный ответ:
Конструкция try-except-else в Python позволяет явно разделить:
- код, в котором потенциально могут возникнуть исключения;
- обработку исключений;
- код, который должен выполниться только при отсутствии ошибок.
Общий порядок выполнения:
try:
# 1. Код, где может возникнуть исключение
except SomeError:
# 2a. Обработка конкретного(ых) исключения(ий), если они произошли в try
else:
# 2b. Выполняется ТОЛЬКО если:
# - в блоке try НЕ было исключений
# - и ни один except не отрабатывал
Подробно по шагам:
- Выполняется блок
try. - Если в
tryпроизошло исключение:- Python ищет подходящий
exceptпо типу исключения; - если найден — выполняется соответствующий блок
except; - блок
elseпри этом пропускается.
- Python ищет подходящий
- Если в
tryисключений не было:- блоки
exceptпропускаются; - выполняется блок
else(если он есть).
- блоки
Важно:
- Блок
elseне ловит исключения внутри себя. - Исключения, возникшие в
else, идут дальше вверх по стеку, как обычно.
Практический смысл else:
- Повышение читаемости и точности обработки ошибок.
- В
tryоставляем только код, от которого ожидаем исключения. - В
elseпишем действия, которые должны выполняться только в случае успешного завершенияtryи не относятся к обработке исключений.
Пример:
def read_int_from_file(path):
try:
with open(path, "r") as f:
data = f.read().strip()
value = int(data) # здесь может быть ValueError
except FileNotFoundError:
return None # файл не найден — обрабатываем
except ValueError:
return None # некорректное число — обрабатываем
else:
# Выполняется только если:
# - файл успешно открыт
# - значение успешно прочитано и преобразовано в int
return value
С добавлением finally (для полноты картины):
try:
# код
except SomeError:
# обработка
else:
# если ошибок не было
...
finally:
# выполняется ВСЕГДА (были ошибки или нет)
# обычно освобождение ресурсов
...
Ключевая идея:
else— это "код успеха", который запускается только тогда, когдаtryпрошёл без исключений, и не содержит логики обработки ошибок. Это делает контроль потоков и ошибок более явным и чистым.
Вопрос 10. Что такое декоратор в Python и зачем он нужен?
Таймкод: 00:08:42
Ответ собеседника: неполный. Кандидат знает, что декораторы используются во фреймворках, после подсказки соглашается, что это способ расширять функциональность функций без изменения их кода, но не формулирует чётко и не приводит собственных примеров.
Правильный ответ:
Декоратор в Python — это паттерн и синтаксический сахар для функции (или класса), которая принимает другую функцию/метод/класс и возвращает изменённую или обёрнутую версию, тем самым добавляя дополнительное поведение без изменения исходного кода целевой функции.
Ключевые идеи:
- Декоратор:
- принимает функцию (или метод, или класс) как аргумент;
- возвращает новую функцию (обёртку), которая:
- вызывает оригинальную,
- добавляет что-то до/после/вокруг вызова (логирование, метрики, кеширование, проверки и т.д.).
- Это классический пример "decorator pattern" из проектирования, реализованный средствами функций высшего порядка.
- Важен для:
- кросс-срезочной логики (cross-cutting concerns): логирование, аутентификация, ретраи, транзакции;
- декларативных API во фреймворках (Flask, FastAPI, Django, etc.).
Базовый пример декоратора:
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(2, 3)
# Выведет:
# Calling add with args=(2, 3), kwargs={}
# add returned 5
Эквивалент без синтаксического сахара:
def add(a, b):
return a + b
add = log_calls(add) # вручную "задекорировали" функцию
Типичные сценарии применения:
-
Логирование и трассировка:
- Добавить логирование входов/выходов, не меняя бизнес-логику.
-
Авторизация и аутентификация:
- Веб-фреймворки используют декораторы для проверки прав доступа:
- например,
@login_required.
-
Ретраи:
- Повторять вызов функции при временных ошибках (сети, БД, внешние API).
-
Кеширование:
functools.lru_cache— стандартный декоратор для memoization.
from functools import lru_cache
@lru_cache(maxsize=1024)
def fib(n: int) -> int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2) -
Валидация, транзакции, метрики:
- Оформление "обёрток" вокруг бизнес-функций.
Технические детали и best practices:
- Использовать
functools.wrapsдля сохранения метаданных оригинальной функции (имя, docstring, аннотации), иначе диагностика и документация ломаются.
from functools import wraps
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
- Декораторы с параметрами:
- Декоратор, который сам принимает аргументы, реализуется как фабрика декораторов.
from functools import wraps
def repeat(times: int):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def hello():
print("hi")
hello()
# "hi" будет выведено 3 раза
Аналогия с Go (как концепт):
В Go нет синтаксического сахара для декораторов, но идея широко используется:
- Оборачивание HTTP-хендлеров, клиентов, репозиториев дополнительным поведением (логирование, метрики, трейсинг, ретраи).
- Это тот же "decorator pattern", только реализованный явно через функции/структуры.
Пример "декоратора" для HTTP-хендлера в Go:
type Middleware func(http.Handler) http.Handler
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}
func Hello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
}
func main() {
http.Handle("/hello", LoggingMiddleware(http.HandlerFunc(Hello)))
http.ListenAndServe(":8080", nil)
}
Ключевой вывод:
- Декораторы — это удобный, декларативный способ расширять поведение функций/методов/классов, не нарушая принцип открытости/закрытости и не дублируя код.
- Умение:
- объяснить, как они работают;
- написать простой декоратор;
- показать реальные сценарии (логирование, авторизация, кеширование) — ожидается как базовый навык.
Вопрос 11. Как устроены dict и set в Python и какова сложность доступа к элементам?
Таймкод: 00:09:46
Ответ собеседника: неправильный. Кандидат верно описал назначение set и dict (уникальные значения и пары ключ-значение), но неверно оценил сложность доступа, указав линейную для dict и константную для set, не показав понимания их общей хеш-структуры.
Правильный ответ:
И dict, и set в Python реализованы на основе хеш-таблиц. Это фундаментально важно для оценки их производительности и понимания поведения.
Ключевые свойства:
-
dict:- Хранит пары ключ-значение.
- Ключи уникальны.
- Доступ по ключу организован через хеш-таблицу.
-
set:- Хранит только уникальные элементы (по сути, только ключи без значений).
- Реализован на той же идее хеш-таблицы, что и
dict, но упрощённо.
- Внутреннее устройство на уровне концепции
Упрощённо механизм таков:
-
При добавлении элемента:
- Для ключа (или элемента в set) вычисляется хеш-функция:
hash(key). - На основе хеша вычисляется индекс ячейки в массиве (bucket) через взятие остатка по размеру таблицы.
- В эту ячейку (или ближайшую свободную) помещается запись.
- Для ключа (или элемента в set) вычисляется хеш-функция:
-
При поиске элемента:
- Снова вычисляется
hash(key). - Определяется индекс.
- Сравниваются:
- сохранённый хеш,
- затем (при совпадении хеша) сам ключ (операция равенства), чтобы избежать коллизий.
- Снова вычисляется
-
При коллизиях (разные ключи с одинаковым индексом или хешем):
- Python использует открытую адресацию (разные варианты probing — поиск следующей свободной позиции по определённому алгоритму).
- Таблица периодически перераспределяется (resize/rehash), чтобы сохранять низкий коэффициент заполнения (load factor) и O(1) амортизированную сложность операций.
- Асимптотическая сложность
Амортизированно (в среднем случае при адекватном количестве коллизий):
-
Для dict:
- Доступ по ключу: получение
value = d[key]— O(1). - Вставка:
d[key] = value— O(1). - Проверка наличия ключа:
key in d— O(1). - Удаление по ключу — O(1).
- Доступ по ключу: получение
-
Для set:
- Проверка принадлежности:
x in s— O(1). - Добавление:
s.add(x)— O(1). - Удаление:
s.remove(x)— O(1) (если элемент есть).
- Проверка принадлежности:
В худшем случае (патологические коллизии):
- Сложность может деградировать до O(n), но:
- встроенный алгоритм хеширования и перераспределения спроектирован так, чтобы в реальных системах это было крайне редким случаем;
- при нормальном распределении хешей операции считаются O(1) амортизированно.
Важно:
- И dict, и set используют хеширование и имеют схожие характеристики.
- Ошибка думать, что dict — O(n) по доступу, а set — O(1). Оба — хеш-структуры, оба рассчитаны на O(1) доступ.
- Особенности dict в современных версиях Python
Современный CPython (3.6+ фактически, 3.7+ гарантированно):
- dict сохраняет порядок вставки ключей.
- Внутри используется:
- массив индексов (hash table),
- компактный массив записей (keys/values),
- что позволяет:
- оптимизировать память,
- сохранить порядок и быстрый доступ.
Пример:
d = {}
d["a"] = 1
d["b"] = 2
d["c"] = 3
print(d) # {'a': 1, 'b': 2, 'c': 3} — порядок вставки сохраняется
Но:
- Порядок — это дополнительная гарантия реализации, не влияет на асимптотику доступа.
- Особенности set
- Реализован поверх хеш-таблицы, концептуально близок к словарю:
- каждый элемент — это, грубо, "ключ без значения".
- Требования к элементам:
- элементы должны быть хешируемыми (immutable или с корректной реализацией
__hash__и__eq__); - изменение объекта, участвующего как элемент set или ключ dict, так что меняется его хеш/равенство, приводит к некорректному поведению.
- элементы должны быть хешируемыми (immutable или с корректной реализацией
- Требования к ключам (dict) и элементам (set)
Ключ (или элемент множества) должен:
- быть хешируемым: иметь стабильный
__hash__по времени жизни в коллекции; - иметь согласованный
__eq__:- если
a == b, тоhash(a) == hash(b)(обратное не обязательно).
- если
Нарушение этих контрактов:
- приводит к невозможности найти элемент или ключ,
- логическим багам.
- Аналогия с Go (для контраста мышления)
В Go:
- map[K]V реализован как хеш-таблица с амортизированной O(1) сложностью для get/set/delete.
- Требования к ключам:
- ключ должен быть comparable (в том числе участвовать в операциях ==, !=);
- нет произвольной переопределяемости hash/eq, как в Python, что упрощает гарантии.
Сходство:
- Python
dict/setи Gomap— концептуально одна и та же идея: хеш-таблица для эффективного доступа.
Краткий вывод:
-
dict:
- ассоциативный массив: ключ → значение;
- реализован как хеш-таблица;
- доступ, вставка, проверка наличия по ключу — O(1) амортизированно.
-
set:
- множество уникальных элементов;
- реализован как хеш-таблица;
- добавление, удаление, проверка наличия — O(1) амортизированно.
-
И dict, и set:
- используют хеширование,
- требуют корректных хешируемых ключей/элементов,
- могут деградировать до O(n) в теории, но в нормальных условиях работают как O(1).
Вопрос 12. Как устроены dict и set в Python и какова их временная сложность доступа к элементам?
Таймкод: 00:09:46
Ответ собеседника: неполный. Кандидат верно описал, что set хранит уникальные значения, а dict — пары ключ-значение. Изначально ошибочно назвал сложность доступа к dict линейной, затем после подсказок начал говорить о зависимости от числа элементов в бакете. В конце уловил идею, что обе структуры реализованы на хеш-таблицах, упомянул O(1) в лучшем случае и O(N) в худшем, но объяснил неуверенно и противоречиво.
Правильный ответ:
Для эффективной разработки важно чётко понимать, как устроены базовые структуры данных. В Python и dict, и set основаны на хеш-таблицах и имеют схожую временную сложность операций.
Общее устройство:
-
dict:- Хранит пары "ключ → значение".
- Ключи уникальны.
- Доступ к значению по ключу реализован через хеш-таблицу.
-
set:- Хранит только уникальные элементы.
- Фактически похож на
dict, но хранит только ключи (значение концептуально "пустое").
Как это работает (концептуально):
-
При вставке:
- Вычисляется
hash(key). - По хешу определяется индекс в массиве бакетов.
- Элемент размещается в соответствующей позиции или ближайшей свободной (используется открытая адресация и probing-стратегия).
- При росте заполненности таблица автоматически расширяется (resize), чтобы сохранять низкую плотность и O(1) доступ.
- Вычисляется
-
При поиске:
- Снова вычисляется
hash(key). - По индексу ищется запись:
- сначала по хешу,
- затем (при совпадении хеша) по
==(для разрешения коллизий).
- В случае коллизий алгоритм просматривает последовательность позиций (probe sequence), пока не найдёт ключ или свободную ячейку.
- Снова вычисляется
-
При коллизиях:
- Используются детерминированные стратегии обхода (open addressing).
- Хорошее распределение хешей и поддержание разумного load factor делают количество проб небольшим.
Асимптотика:
-
Амортизированная (средний случай при нормальных хешах):
dict:- доступ по ключу:
d[k],k in d— O(1) - вставка:
d[k] = v— O(1) - удаление:
del d[k]— O(1)
- доступ по ключу:
set:- проверка принадлежности:
x in s— O(1) - добавление:
s.add(x)— O(1) - удаление:
s.remove(x)— O(1)
- проверка принадлежности:
-
Худший случай:
- O(N), если почти все ключи попали в коллизию или произошли атаки на хеши.
- На практике за счёт:
- хорошего хеширования,
- перераспределений,
- рандомизации
- такие случаи редки, и структуры считаются O(1) амортизированно.
Ключевой вывод:
- И
dict, иset— хеш-таблицы с амортизированной O(1) сложностью для доступа/вставки/проверки. - Нельзя считать, что
dict— O(N) по доступу "по определению", это неправильно. - У обеих структур один и тот же фундаментальный механизм, с разницей в том, что
dictхранит пары ключ-значение, аset— только ключи.
Требования к ключам и элементам:
- Ключ
dictи элементsetдолжны быть:- хешируемыми (имеют стабильный
__hash__пока используются в структуре), - с согласованным
__eq__:- если
a == b, тоhash(a) == hash(b).
- если
- хешируемыми (имеют стабильный
- Нарушение этого контракта (например, изменяемые объекты как ключи) ведет к некорректному поведению (невозможность найти элемент и логические баги).
Дополнительные детали:
- Современный CPython (3.7+):
- гарантирует сохранение порядка вставки для
dict. - Это реализовано отдельными структурами (индексы + массив записей), поверх всё ещё хеш-таблица.
- Порядок — это гарантия API, но асимптотика доступа по ключу остаётся O(1) амортизированно.
- гарантирует сохранение порядка вставки для
Аналогия с Go:
- В Go встроенный
map[K]Vреализован как хеш-таблица:- доступ, вставка, удаление — O(1) амортизированно;
- ключи должны быть сравнимыми (comparable).
- Концептуально то же, что
dict/setв Python, но с жёстче заданной моделью типов.
Правильная формулировка для интервью:
dictиsetв Python реализованы через хеш-таблицы.- В среднем:
- операции доступа, вставки, удаления и проверки наличия — O(1) амортизированно.
- В худшем случае возможна деградация до O(N), но это редкая и нежелательная ситуация.
dictхранит пары ключ-значение,setхранит только уникальные ключи.
Вопрос 13. Объясни принцип работы сборщика мусора в Python.
Таймкод: 00:12:48
Ответ собеседника: неполный. Кандидат описывает общий механизм: периодический проход по памяти, поиск неиспользуемых объектов и их удаление. Не упоминает ключевую роль подсчёта ссылок и отдельный механизм для обработки циклических ссылок.
Правильный ответ:
В Python (на примере CPython, т.к. это стандартная и наиболее важная реализация) сборка мусора основана на двух ключевых механизмах:
- Подсчёт ссылок (reference counting) — основной и немедленный.
- Дополнительный циклический сборщик (cycle GC) — для обнаружения и очистки объектных циклов, которые не может освободить один подсчёт ссылок.
Важно понимать оба, чтобы корректно оценивать поведение памяти, утечки и особенности долгоживущих сервисов.
Основной механизм: подсчёт ссылок
Каждый объект в CPython хранит счётчик ссылок ob_refcnt.
Идея:
- Когда создаётся новый объект — его счетчик ссылок = 1.
- Когда создаётся новая ссылка на объект (присваивание, передача в список, dict, func-аргумент и т.п.) — счетчик увеличивается.
- Когда ссылка уничтожается или переназначается — счетчик уменьшается.
- Когда счётчик ссылок объекта становится 0:
- объект немедленно освобождается,
- его память возвращается менеджеру памяти.
Пример (концептуально):
a = [1, 2, 3] # refcount(a) = 1
b = a # refcount(a) = 2
del b # refcount(a) = 1
del a # refcount(a) = 0 -> список освобождается сразу
Плюсы:
- Предсказуемость: большинство объектов освобождаются сразу, как только становятся недостижимыми.
- Это важно для файловых дескрипторов, соединений, блокировок и т.п., когда мы полагаемся на своевременное освобождение.
Минус:
- Подсчёт ссылок не умеет справляться с циклическими ссылками.
Проблема: циклические ссылки
Если два (или более) объекта ссылаются друг на друга, но больше недостижимы из "корней" программы (глобальные переменные, стек вызовов и т.п.), то их refcount не упадет до нуля, и один только подсчёт ссылок не освободит их.
Пример:
class Node:
def __init__(self):
self.ref = None
a = Node()
b = Node()
a.ref = b
b.ref = a
# После этого:
del a
del b
# Узлы продолжают ссылаться друг на друга.
# Их refcount > 0, хотя доступны они уже только друг через друга.
Чтобы такие циклы не приводили к утечкам, в CPython есть отдельный циклический сборщик.
Циклический сборщик (cycle GC)
Он реализован в модуле gc и работает поверх подсчёта ссылок.
Ключевые идеи:
- Отслеживает только "container objects":
- объекты, которые могут содержать ссылки на другие объекты: list, dict, set, tuple (если внутри есть объекты), классы, инстансы, и т.д.
- Организует их по поколениям (generational GC):
- поколение 0: новые объекты;
- поколение 1, 2: объекты, пережившие несколько циклов сборки.
- Предположение: "большинство объектов живут недолго" (как и в большинстве современных GC).
Алгоритм (упрощённо):
- Периодически, когда количество аллокаций/объектов превышает порог, запускается сборка для "младшего" поколения.
- GC просматривает объекты-контейнеры, строит граф ссылок.
- Пытается определить объекты, недостижимые из "корней" (глобальные переменные, стек, регистры и т.п.).
- Объекты, которые образуют цикл и недостижимы извне, помечаются как мусор.
- Они освобождаются, даже если их refcount > 0 из-за внутренних циклических ссылок.
Сложности с финализаторами (__del__):
- Если объекты в цикле имеют методы
__del__, стандартный механизм не всегда может безопасно определить порядок уничтожения. - В старых версиях такие объекты могли попадать в "uncollectable" (неподлежащий сборке мусор).
- Современный CPython улучшил поведение, но общее правило:
- Стараться избегать сложной логики в
__del__. - Для управления ресурсами использовать контекстные менеджеры (
with) иcontextlib.
- Стараться избегать сложной логики в
Управление и настройка (модуль gc):
Можно:
- Посмотреть пороги и статистику:
import gc
print(gc.get_threshold())
print(gc.get_count()) - Временно отключать GC (например, в performance-критичных местах):
gc.disable()
# ... код ...
gc.enable() - Принудительно запускать сборку:
gc.collect()
Практические выводы:
-
Немедленное освобождение:
- В отличие от многих языков с чистым "stop-the-world" GC, в CPython большинство объектов освобождается сразу при обнулении ссылок.
- Это удобно для управления ресурсами, но важно помнить: это деталь реализации CPython, а не абстрактная гарантия всех реализаций Python.
-
Утечки памяти:
- Основные источники проблем — не GC как таковой, а:
- глобальные структуры, кеши, синглтоны;
- длинноживущие ссылки на объекты (замыкания, лямбды, классы, кэши);
- циклы с
__del__.
- Для диагностики полезны:
gc.get_objects(),gc.get_referrers(); сторонние инструменты (objgraph и т.п.).
- Основные источники проблем — не GC как таковой, а:
-
Сравнение мышления с Go:
- В Go:
- используется три-color mark-and-sweep, concurrent, non-moving GC (в современных версиях);
- программист не управляет подсчётом ссылок вручную, GC периодически обходит граф объектов.
- В CPython:
- основа — reference counting + дополнительный cycle GC.
- Для разработчика, работающего с обоими языками, важно понимать:
- в Python многие ресурсы освобождаются детерминированно (через refcount),
- в Go — недетерминированно, но с мягкими паузами и concurrent GC.
Краткая формулировка для интервью:
- В CPython сборка мусора основана на подсчёте ссылок: когда счётчик ссылок объекта падает до 0, он сразу уничтожается.
- Для обнаружения циклических ссылок используется отдельный циклический сборщик с поколениями.
- В среднем это даёт предсказуемое освобождение памяти + защиту от утечек из-за циклов.
Вопрос 14. Как наиболее простым способом удалить дубликаты из списка в Python?
Таймкод: 00:13:24
Ответ собеседника: правильный. Предложил преобразовать список в множество и затем обратно в список.
Правильный ответ:
Самый простой способ убрать дубликаты из списка в Python — преобразовать его во множество (set), а затем обратно в список:
items = [1, 2, 2, 3, 3, 3]
unique = list(set(items))
# unique может быть, например, [1, 2, 3]
Но важно понимать нюансы и уметь выбирать способ в зависимости от требований.
Ключевые моменты:
- Базовое решение через set:
- Плюсы:
- Простое, короткое, наглядное.
- Операции на множестве работают амортизированно за O(1), общее время — O(N).
- Минус:
- Не сохраняется порядок элементов (до Python 3.7 порядок dict/set был не гарантирован; даже сейчас на порядок set полагаться не следует).
- Подходит, если порядок не важен.
- Сохранение порядка элементов:
Если важно сохранить первый порядок появления уникальных элементов, лучше использовать связку множества и прохода по списку:
Вариант 1 (явный цикл):
def unique_preserve_order(seq):
seen = set()
res = []
for x in seq:
if x not in seen:
seen.add(x)
res.append(x)
return res
items = [3, 1, 2, 3, 2, 1]
print(unique_preserve_order(items)) # [3, 1, 2]
- Время: O(N) амортизированно.
- Память: O(N) на множество и результирующий список.
- Порядок первых вхождений сохраняется.
Вариант 2 (Python 3.7+ с dict):
items = [3, 1, 2, 3, 2, 1]
unique = list(dict.fromkeys(items))
# dict сохраняет порядок ключей → результат: [3, 1, 2]
Это лаконичный и идиоматичный способ для "уникальных с сохранением порядка".
- Требования к элементам:
Во всех вариантах:
- Элементы должны быть хешируемыми (для использования в set/dict).
- Если элементы не хешируемы (например, списки), можно:
- привести их к хешируемому виду (tuple),
- или использовать более сложную логику (например, сериализацию или сравнение по ключу).
- Сопоставление с Go-мышлением:
В Go нет встроенного множества, но аналог можно сделать на базе map:
func UniqueInts(nums []int) []int {
seen := make(map[int]struct{})
res := make([]int, 0, len(nums))
for _, v := range nums {
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
res = append(res, v)
}
}
return res
}
Идея та же:
- map/set на основе хеш-таблицы,
- O(N) по времени,
- контроль над порядком (порядок в результирующем слайсе определяется порядком обхода исходного списка).
Итог:
- "Простой способ" для интервью:
list(set(lst))— если порядок не важен.
- "Правильный и практичный способ":
list(dict.fromkeys(lst))или цикл сseen = set()— если порядок важен и нужно предсказуемое поведение.
Вопрос 15. Как правильно разделить датасет на обучающую, валидационную и тестовую выборки и какие пропорции использовать?
Таймкод: 00:13:46
Ответ собеседника: неполный. Кандидат верно отмечает необходимость разделения на train/val/test и ссылается на train_test_split, но допускает неточность про "первые элементы", не акцентирует важность рандомизации и стратификации. Пропорции 80/20 и 90/10 называет, зависимость от размера датасета в целом описывает верно, но без чёткой структуры.
Правильный ответ:
При разделении датасета ключевые цели:
- Оценивать обобщающую способность модели на данных, которые она не видела при обучении.
- Избежать утечек данных (data leakage).
- Иметь репрезентативные подвыборки по целевой переменной и важным признакам.
Обычно мы оперируем тремя наборами:
- Обучающая выборка (train): обучение и подбор параметров модели.
- Валидационная (validation): подбор гиперпараметров, выбор архитектуры/регуляризации, ранняя остановка.
- Тестовая (test): финальная, "честная" оценка качества, не используется в процессе настройки.
Базовые пропорции:
Типичные варианты (зависят от объёма данных):
- Малый/средний датасет:
- 60% train / 20% val / 20% test
- 70% train / 15% val / 15% test
- Большой датасет (сотни тысяч+ объектов):
- 80% train / 10% val / 10% test
- 90% train / 5% val / 5% test
- Очень большой датасет:
- Можно взять относительно маленький, но репрезентативный validation/test (например, по 1–5%), остальное — train.
Ключевые моменты:
- Не существует "магического" числа; важно:
- чтобы валидация и тест были достаточно большими и репрезентативными для статистически устойчивой оценки;
- не экономить валидацию до бессмысленных размеров, но и не отбирать у train больше, чем нужно.
Рандомизация:
- Нельзя просто "брать первые N%" для train, а остаток — в test, если данные упорядочены (по времени, по источнику, по пользователям и т.д.).
- Если данные перемешаны и не имеют временной или иной структуры:
- используем случайное разбиение (random shuffle).
- Если данные временные:
- нельзя перемешивать произвольно.
- используется time-based split:
- train — более ранний период;
- val/test — более поздние периоды.
- Это имитирует реальный сценарий предсказания будущего по прошлому.
Стратификация:
- Если задача классификации:
- использовать стратифицированное разбиение по целевой переменной:
- чтобы доля классов в train/val/test примерно совпадала с исходной.
- Особенно важно при несбалансированных классах.
- использовать стратифицированное разбиение по целевой переменной:
Data leakage (утечка данных):
При любом разбиении критично:
- Любые операции, зависящие от статистик данных (масштабирование, нормализация, PCA, отбор признаков по корреляции, целевой encoding и т.п.) обучаются только на train.
- Потом те же трансформации применяются к val/test.
- Нельзя:
- считать среднее/стандартное отклонение/квантили/частоты по всему датасету и потом делить;
- использовать информацию о целевой переменной из test/val при построении признаков train.
- Для временных рядов:
- особенно строго следить за тем, чтобы "информация из будущего" не попадала в train.
Практический пример (Python, sklearn):
Разделение train/test:
from sklearn.model_selection import train_test_split
X_train, X_temp, y_train, y_temp = train_test_split(
X, y,
test_size=0.3, # 30% во временный набор
random_state=42,
stratify=y # для классификации
)
# Теперь делим временный набор на val и test пополам
X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp,
test_size=0.5, # половина от 30% -> 15%/15%
random_state=42,
stratify=y_temp
)
Результат: 70% / 15% / 15% с рандомизацией и стратификацией.
Для временных рядов (упрощенно):
# Данные отсортированы по времени
n = len(X)
train_end = int(n * 0.7)
val_end = int(n * 0.85)
X_train, y_train = X[:train_end], y[:train_end]
X_val, y_val = X[train_end:val_end], y[train_end:val_end]
X_test, y_test = X[val_end:], y[val_end:]
Тут:
- Никакого перемешивания.
- Валидация и тест — более поздние периоды.
Связь с инженерным мышлением (важная практика):
- В проде критичен не только сплит, но и воспроизводимость:
- фиксировать
random_state; - документировать стратегию разбиения;
- не переиспользовать тестовую выборку для многократного "тюнинга", иначе она перестает быть честной.
- фиксировать
- При сложных продуктах:
- часто делают k-fold cross-validation на train для подбора гиперпараметров;
- выделяют отдельный hold-out test, который не трогают до финальной оценки.
Краткая формулировка для интервью:
- Разбиваем на train/val/test.
- Используем случайное разбиение (shuffle) с фиксацией seed, кроме временных рядов — там time-based split.
- Для классификации — стратификация по целевой.
- Типичные пропорции:
- 70/15/15, 80/10/10 или 90/5/5 — в зависимости от размера данных.
- Любые преобразования, зависящие от данных, обучаются только на train, затем применяются к val/test, чтобы избежать утечки данных.
Вопрос 16. Почему при маленьком датасете долю валидационной выборки имеет смысл делать относительно больше и какие проблемы возникают при слишком маленькой валидации?
Таймкод: 00:16:03
Ответ собеседника: правильный. Объяснил, что при слишком малом числе примеров валидационные метрики становятся нерепрезентативными и сильно зависят от случайности, искажают реальную картину.
Правильный ответ:
При небольшом датасете главный риск — высокая дисперсия оценки качества модели. Поэтому:
- если сделать валидационную выборку слишком маленькой,
- метрики на ней будут:
- нестабильными,
- чувствительными к конкретным примерам,
- плохо отражать реальную обобщающую способность модели.
Почему доля валидации должна быть относительно больше:
-
Статистическая устойчивость:
- Метрики (accuracy, F1, ROC-AUC, MAPE и т.д.) являются статистическими оценками.
- При очень малом числе примеров:
- одна-две аномальные точки или смещение по классам могут сильно исказить результат.
- Более крупная валидация уменьшает дисперсию оценки: мы получаем более стабильный сигнал, на основе которого выбираем модель и гиперпараметры.
-
Репрезентативность распределения:
- В маленькой валидации могут:
- не встретиться редкие классы,
- не попасть важные паттерны,
- не отражаться реальные пропорции.
- В результате модель может казаться хорошей на валидации, но проваливаться на реальных данных.
- В маленькой валидации могут:
-
Риск оверфита на валидацию:
- Если валидация маленькая, а вы многократно перебираете гиперпараметры/архитектуры:
- модель (или ваш процесс выбора) "подгоняется" под конкретные несколько десятков/сотен примеров.
- Вы бессознательно начинаете переобучаться на validation set, так же как можно переобучиться на train.
- Чем меньше выборка, тем легче её "заучить" и тем менее честный сигнал она даёт.
- Если валидация маленькая, а вы многократно перебираете гиперпараметры/архитектуры:
Какие проблемы при слишком маленькой валидации:
-
Шумные метрики:
- Нельзя уверенно отличить:
- "эта модель действительно лучше"
- от "нам просто повезло на этих 30 примерах".
- Нельзя уверенно отличить:
-
Неправильный выбор модели:
- Можно выбрать архитектуру/гиперпараметры, которые случайно показали лучший результат на маленькой валидации, но в реальности хуже.
-
Ложное чувство уверенности:
- Маленькая выборка даёт красивые числа, но они статистически незначимы.
Как действовать на практике при малом датасете:
-
Увеличивать относительный размер validation/test:
- вместо, скажем, 90/5/5 можно сделать 70/15/15 или близкие схемы;
- важно не абсолютное процентное значение, а достаточное количество примеров для устойчивых метрик.
-
Использовать перекрёстную проверку (k-fold cross-validation):
- Разбиваем данные на k фолдов.
- Для каждой модели делаем k прогонов (train на k-1 фолдах, validate на оставшемся).
- Усредняем метрики.
- Это:
- повышает устойчивость оценки,
- позволяет эффективнее использовать небольшой датасет,
- уменьшает зависимость от конкретного случайного сплита.
-
Следить за стратификацией:
- Особенно при несбалансированных классах:
- маленькая non-stratified валидация легко "теряет" редкий класс,
- метрики становятся бессмысленными.
- Особенно при несбалансированных классах:
Краткая формулировка:
- При маленьком датасете:
- нужна относительно более крупная и стратифицированная валидационная выборка,
- чтобы оценка качества была статистически значимой и не скакала от случайных выбросов.
- Слишком маленькая валидация:
- даёт шумные и нерепрезентативные метрики,
- повышает риск выбрать неправильную модель,
- стимулирует переобучение на несколько случайных наблюдений.
Вопрос 17. На что обратить внимание при подготовке многоклассового датасета (например, с тремя классами) перед обучением модели?
Таймкод: 00:18:23
Ответ собеседника: правильный. Указал на необходимость контролировать баланс классов: при сильном преобладании одного класса модель будет склонна чаще предсказывать его, что нежелательно.
Правильный ответ:
При подготовке многоклассового датасета (3+ классов) важно не ограничиваться только проверкой баланса. Нужно системно пройтись по нескольким аспектам, чтобы модель обучалась корректно и метрики были честными.
Ключевые моменты:
- Баланс классов и репрезентативность
- Проверить распределение классов:
- нет ли ситуации, когда один класс занимает 80–95% данных, а остальные почти не представлены;
- для всех классов должно быть достаточно примеров для обучения и валидации.
- Если сильный дисбаланс:
- модель может игнорировать редкие классы и “выигрывать” по accuracy, предсказывая только мажоритарный класс.
- Подходы:
- стратифицированный train/val/test split, чтобы сохранить распределение классов во всех выборках;
- oversampling редких классов (например, случайное или продвинутое вроде SMOTE);
- undersampling мажоритарных классов;
- использование взвешенных лоссов (class weights) для компенсации дисбаланса;
- метрики, чувствительные к балансу: macro/micro F1, balanced accuracy, per-class recall.
- Корректная разметка и качество таргета
- Проверить:
- нет ли перепутанных меток;
- нет ли классов с дублирующим смыслом;
- нет ли класса “свалка всего” без чётких критериев.
- Минимум:
- ручная проверка части данных по каждому классу;
- одинаковые правила разметки для всех источников данных.
- Разделение на выборки без утечки данных
- Строго следить, чтобы объекты, логически связанные, не разошлись по train/val/test так, что создаётся утечка:
- один и тот же пользователь (или устройство, или сессия) не должен одновременно быть и в train, и в test;
- одинаковые или почти одинаковые элементы (дубликаты) не должны оказаться в train и test в разных классах.
- Для многоклассовых задач особенно важно, чтобы:
- все классы были представлены в train и val/test;
- разделение было стратифицированным:
- в sklearn:
StratifiedKFold,train_test_split(..., stratify=y).
- в sklearn:
- Качество признаков и информативность для всех классов
- Убедиться, что признаки позволяют отличать все классы, а не только мажоритарный:
- визуализация (t-SNE/UMAP) для sanity-check;
- базовый анализ: корреляции, распределения признаков по классам.
- Важно:
- отсутствие признаков, “подсматривающих” истинный класс напрямую (data leakage);
- отсутствие признаков, завязанных на индексы, имена файлов, сырой ID и т.п., если они коррелируют с классом случайно.
- Кодирование таргета и классов
- Явно определить:
- mapping между именами классов и их ID (например: {0: "class_A", 1: "class_B", 2: "class_C"}).
- Следить за:
- консистентностью этого mapping во всех стадиях пайплайна: обучение, валидация, инференс;
- правильной работой one-vs-rest / softmax при обучении.
- Метрики для многоклассовой задачи
Ещё до обучения стоит определить:
- какие метрики релевантны:
- accuracy может быть недостаточной;
- macro-F1 (чувствителен к редким классам),
- weighted-F1,
- per-class precision/recall.
- Анализировать confusion matrix:
- помогает увидеть, какие классы путаются между собой;
- позорный кейс: модель всегда предсказывает один класс → высокая accuracy при полном игноре других.
- Практический пример на Python (стратифицированный сплит)
from sklearn.model_selection import train_test_split
import numpy as np
# X — признаки, y — метки классов (0, 1, 2)
X_train, X_temp, y_train, y_temp = train_test_split(
X, y,
test_size=0.3,
random_state=42,
stratify=y # сохраняем распределение классов
)
X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp,
test_size=0.5,
random_state=42,
stratify=y_temp
)
# Быстрая проверка распределения
def class_dist(name, labels):
unique, counts = np.unique(labels, return_counts=True)
print(name, dict(zip(unique, counts)))
class_dist("train", y_train)
class_dist("val", y_val)
class_dist("test", y_test)
- Мышление с точки зрения инженерии
- Не ограничиваться абстрактным "баланс классов":
- проверить распределения;
- правильно разбить;
- заложить выбор адекватных метрик;
- убедиться, что пайплайн честный (без утечек).
- Особенно при маленьком числе примеров для некоторых классов:
- использовать стратифицированный k-fold cross-validation для устойчивой оценки;
- внимательно смотреть на per-class метрики, а не только на средние значения.
Кратко:
- Контролировать баланс и репрезентативность классов.
- Делать стратифицированное разбиение на train/val/test.
- Избегать утечки данных.
- Выбирать правильные метрики, учитывающие многоклассовую природу и дисбаланс.
- Проверять качество разметки и признаки для всех классов, а не только для самого частого.
Вопрос 18. Что можно сделать при несбалансированном датасете для выравнивания классов?
Таймкод: 00:19:22
Ответ собеседника: неполный. Кандидат интуитивно предлагает уменьшать доминирующий класс или увеличивать редкий, но путает техники: смешивает заполнение признаков и балансировку классов, не называет явно oversampling/undersampling, data augmentation, взвешенные лоссы и другие стандартные подходы.
Правильный ответ:
При несбалансированном датасете цель — не просто “сделать равные количества”, а добиться того, чтобы модель:
- не игнорировала редкие классы;
- хорошо различала все целевые классы, особенно важные бизнес-классы (fraud, отказ, критические события и т.п.);
- не переобучилась на искусственно созданных данных.
Ключевые стратегические подходы (часто комбинируются):
- Undersampling (уменьшение мажоритарного класса)
Идея:
- Случайно (или осмысленно) уменьшить количество объектов доминирующего класса, чтобы приблизить баланс.
Плюсы:
- Быстро и просто.
- Уменьшает размер данных → ускоряет обучение.
Минусы:
- Потеря информации:
- выбрасываем реальные данные, потенциально полезные для границы решений.
- Не всегда приемлемо при небольших датасетах.
Пример (Python):
from sklearn.utils import resample
import numpy as np
X_major = X[y == 0]
X_minor = X[y == 1]
y_major = y[y == 0]
y_minor = y[y == 1]
X_major_down, y_major_down = resample(
X_major, y_major,
replace=False,
n_samples=len(y_minor),
random_state=42,
)
X_balanced = np.vstack([X_major_down, X_minor])
y_balanced = np.concatenate([y_major_down, y_minor])
- Oversampling (увеличение миноритарного класса)
Идея:
- Добавить больше примеров редких классов:
- простое дублирование (random oversampling),
- генерация синтетических примеров (SMOTE, ADASYN и др.).
Плюсы:
- Сохраняем все данные мажоритарного класса.
- Даем модели больше сигналов по редким классам.
Минусы:
- Простое дублирование может привести к переобучению на редкий класс:
- модель “зазубрит” несколько одинаковых примеров.
- Синтетические методы требуют аккуратности:
- могут создавать неестественные объекты, если признаки сложные или нечисловые.
Простой oversampling:
from sklearn.utils import resample
X_minor_upsampled, y_minor_upsampled = resample(
X_minor, y_minor,
replace=True,
n_samples=len(y_major),
random_state=42,
)
X_balanced = np.vstack([X_major, X_minor_upsampled])
y_balanced = np.concatenate([y_major, y_minor_upsampled])
- Data augmentation (особенно для изображений, текста, аудио)
Идея:
- Генерировать новые, правдоподобные примеры для редких классов:
- изображения: геометрические трансформации, шум, изменение яркости/контраста;
- текст: перефразирование, синонимы, back-translation;
- аудио: шум, питч, скорость.
- Это форма осмысленного oversampling.
Плюсы:
- Повышает разнообразие данных.
- Может улучшить обобщающую способность модели.
Минусы:
- Нужна доменная экспертиза, чтобы не исказить семантику.
- Некачественный augmentation даст шум и ухудшит модель.
- Взвешенные лоссы (class weights)
Идея:
- Вместо изменения данных — изменить функцию потерь:
- ошибки на редком классе штрафуются сильнее.
- В бинарной и многоклассовой классификации:
- добавляем веса классов в loss (например, в логистической регрессии, деревьях, нейросетях).
Плюсы:
- Не меняем распределение данных.
- Гибкий и часто предпочтительный способ для продакшена.
- Хорошо поддерживается во многих фреймворках (sklearn, XGBoost, PyTorch, TF).
Минусы:
- Требует аккуратного подбора весов:
- слишком большие веса → переобучение/нестабильность.
Пример (sklearn):
from sklearn.linear_model import LogisticRegression
clf = LogisticRegression(class_weight="balanced")
clf.fit(X_train, y_train)
Или вручную:
class_weight = {
0: 1.0, # мажоритарный
1: 5.0, # миноритарный
2: 5.0,
}
clf = LogisticRegression(class_weight=class_weight)
- Изменение порогов классификации
Идея:
- Для вероятностных моделей (логистическая регрессия, градиентный бустинг, нейросети):
- не использовать фиксированный порог 0.5 для всех классов.
- подобрать пороги отдельно для классов (в one-vs-rest или через калибровку), исходя из:
- бизнес-стоимости ошибок,
- precision/recall trade-off.
Плюсы:
- Особо полезно, если важно максимально ловить редкий класс (fraud detection, дефекты).
Минусы:
- Требует стабильных вероятностных оценок и отдельной настройки.
- Выбор правильных метрик
При несбалансированных данных:
- accuracy почти бесполезна.
- Нужны:
- precision, recall, F1-score (macro/weighted),
- ROC-AUC (в т.ч. macro/micro),
- PR-AUC для редких классов,
- per-class метрики,
- confusion matrix.
Это не “выравнивает” классы, но меняет критерий, по которому принимаются решения об архитектуре / гиперпараметрах / порогах.
- Корректный split и стратификация
Обязательно:
- Стратифицированный train/val/test split:
- чтобы редкие классы были представлены в каждой выборке.
- Никаких утечек данных:
- нельзя, чтобы один и тот же объект/сущность попадал в train и test под разными видами.
Пример:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42,
stratify=y
)
- Комбинации подходов
На практике часто комбинируют:
- stratified split + class weights;
- oversampling редких классов в train + взвешенный loss;
- data augmentation + разумные пороги + корректные метрики.
Главное:
- Делать балансировку только по train:
- validation и test должны отражать реальное распределение, иначе вы получите “красивые, но ложные” метрики.
- Не смешивать задачи:
- заполнение пропусков и генерация признаков — это не oversampling классов;
- важно чётко разделять балансировку классов и обработку feature-level пропусков.
Краткая формулировка для интервью:
- При несбалансированном датасете используют:
- undersampling мажоритарного класса,
- oversampling и/или data augmentation для миноритарных классов,
- взвешенные функции потерь,
- корректные метрики (F1, PR-AUC, per-class),
- стратифицированный split.
- Балансировку делают на train, а val/test оставляют с реальным распределением, чтобы честно оценить модель.
Вопрос 19. Как увеличить количество примеров миноритарного класса в датасете изображений (например, кошек vs собак)?
Таймкод: 00:20:18
Ответ собеседника: неполный. Сначала предлагает дублировать изображения редкого класса или чаще выбирать их при формировании выборки. После подсказки частично подводится к идее аугментации (зеркалирование и трансформации), но не формулирует её полно и системно.
Правильный ответ:
Для увеличения количества примеров миноритарного класса в задаче классификации изображений (например, кошек меньше, чем собак) используют два основных подхода:
- прямое увеличение (oversampling),
- аугментация данных (data augmentation).
Ключевая цель — дать модели больше разнообразных и репрезентативных примеров редкого класса, не нарушая реалистичность данных и не создавая сильного переобучения.
Основные подходы:
- Простое дублирование (naive oversampling)
- Суть:
- Просто копируем существующие изображения редкого класса (кошек), чтобы увеличить их долю в train.
- Плюсы:
- Реализуется очень просто.
- Минусы:
- Лёгкое переобучение:
- модель “зазубрит” одни и те же изображения;
- плохо обобщает на новые примеры.
- Лёгкое переобучение:
- Используется как временная мера, но для реальных задач обычно предпочитают осмысленную аугментацию.
- Data augmentation (осмысленное расширение данных)
Это правильный и стандартный подход.
Идея:
- На базе каждого исходного изображения генерировать несколько новых, применяя реалистичные трансформации, сохраняющие класс объекта.
Типичные трансформации:
- Геометрические:
- отражение по горизонтали (horizontal flip);
- небольшие повороты;
- масштабирование (zoom);
- сдвиги (shift);
- кропы (random crop);
- лёгкая аффинная трансформация.
- Фотометрические:
- изменение яркости, контраста, насыщенности;
- небольшой шум.
- Пространственные:
- лёгкое размытие или sharpening, если уместно.
Важно:
- Трансформации должны быть физически и семантически корректными:
- отзеркаливание кошки — ок;
- сильное искажение до неузнаваемости или инверсия классов — нельзя.
- Для мед. изображений, OCR, некоторых промышленных задач список безопасных трансформаций сильно зависит от домена.
Пример (Python, Keras-style псевдокод):
from tensorflow.keras.preprocessing.image import ImageDataGenerator
datagen = ImageDataGenerator(
rotation_range=15,
width_shift_range=0.1,
height_shift_range=0.1,
zoom_range=0.1,
horizontal_flip=True,
brightness_range=(0.8, 1.2),
fill_mode="nearest",
)
# X_cats — изображения кошек (миноритарный класс)
datagen.fit(X_cats)
# При обучении:
# модель будет видеть различные аугментированные версии кошек
model.fit(datagen.flow(X_cats, y_cats, batch_size=32))
Здесь:
- генератор динамически создаёт новые варианты изображений на лету;
- мы эффективно увеличиваем разнообразие примеров кошек без ручного копирования.
- Комбинированный подход
На практике часто делают:
- для миноритарного класса:
- oversampling + агрессивная (но валидная) аугментация;
- для мажоритарного:
- умеренная/симметричная аугментация или без неё;
- плюс:
- балансировка лосса (class weights),
- корректные метрики (per-class F1, macro-F1, confusion matrix).
Ключевые правила:
- Балансировку (oversampling/augmentation) применяем только к тренировочной выборке:
- validation и test оставляем с реальным распределением, чтобы честно оценивать модель.
- Следим за тем, чтобы аугментация не меняла класс и не добавляла нереалистичных артефактов.
- Важно не только “количество картинок”, но и разнообразие:
- 10 000 копий одной кошки не равно 10 000 разным кошкам под разными условиями.
Краткая формулировка для интервью:
- Для увеличения примеров меньшего класса (кошек) используют:
- oversampling (но простое дублирование ограниченно полезно),
- data augmentation: отражение, повороты, кропы, изменения яркости/контраста и другие реалистичные трансформации,
- при необходимости в сочетании с взвешенным лоссом.
- Все изменения делаются на train, а валидация/тест остаются неизменёнными и отражают реальное распределение данных.
Вопрос 20. Как в pandas заменить и удалить строки с отсутствующими значениями (NaN)?
Таймкод: 00:22:02
Ответ собеседника: неполный. Кандидат упоминает общую идею использовать методы заполнения и удаления, но не называет конкретные функции (fillna, dropna) и не объясняет варианты их применения.
Правильный ответ:
В pandas работа с пропусками (NaN/None) — базовый навык при подготовке данных. Основные операции:
- найти пропуски,
- заполнить пропуски осмысленными значениями,
- удалить строки/столбцы с пропусками — при необходимости.
Ключевые функции:
isna()/isnull()— найти пропуски.fillna()— заменить пропуски.dropna()— удалить строки/столбцы с пропусками.
- Поиск пропусков
import pandas as pd
df = pd.DataFrame({
"age": [25, None, 30],
"city": ["Moscow", "SPB", None],
})
df.isna() # True/False по ячейкам
df.isna().sum() # количество NaN по столбцам
- Удаление строк с пропусками: dropna
Базовый вариант: удалить все строки, где есть хотя бы один NaN:
df_clean = df.dropna()
Опции:
how:how="any"(по умолчанию): удаляет строку, если есть хотя бы один NaN.how="all": удаляет строку только если все значения NaN.
subset:- ограничить проверку конкретными столбцами.
Примеры:
# Удалить строки, где в столбце age пропуск
df_age = df.dropna(subset=["age"])
# Удалить строки, где ВСЕ значения NaN
df_all = df.dropna(how="all")
Важно:
- Удаление строк уменьшает датасет, может исказить распределения.
- Используем, если:
- пропусков мало,
- или строка без ключевых полей бессмысленна.
- Замена пропусков: fillna
fillna() позволяет заменить NaN на заданные значения.
Простые варианты:
# Заполнить все NaN нулями
df_filled = df.fillna(0)
# Заполнить NaN в одном столбце
df["age"] = df["age"].fillna(df["age"].mean()) # числовой столбец: среднее
df["city"] = df["city"].fillna("Unknown") # категориальный: специальное значение
Словарь по столбцам:
df = df.fillna({
"age": df["age"].median(),
"city": "Unknown",
})
Использование методов заполнения по соседним значениям:
method="ffill"(forward fill) — тянуть предыдущее значение вниз.method="bfill"(backward fill) — тянуть следующее значение вверх.
df_ffill = df.fillna(method="ffill")
df_bfill = df.fillna(method="bfill")
Аккуратность:
ffill/bfillлогичны для временных рядов или логически упорядоченных данных.- Не использовать слепо, если порядок строк не несёт смысла.
- inplace и безопасная практика
Обе функции поддерживают inplace=True, но для чистоты и предсказуемости кода лучше возвращать новый DataFrame:
df = df.fillna(0) # предпочтительнее, чем df.fillna(0, inplace=True)
df = df.dropna(subset=[...])
- Осмысленный выбор стратегии
Продвинутый уровень — не просто знать fillna/dropna, а понимать, когда и как применять:
-
Числовые признаки:
- среднее/медиана,
- константа (например, 0 или -1, если это семантически корректно),
- модельная имputation (KNN, регрессия и т.п.) — для более сложных кейсов.
-
Категориальные признаки:
- отдельная категория типа "Unknown"/"Missing",
- наиболее частое значение (mode),
- осторожно: может смещать статистику.
-
Временные ряды:
ffill/bfill, интерполяция (df.interpolate()),- важно учитывать физический смысл.
-
Удаление строк:
- только если пропуски редкие или критически нарушают смысл записи.
Краткая формулировка для интервью:
- Для удаления строк/столбцов с NaN используется
dropna()с опциямиhow,subset. - Для замены пропусков используется
fillna():- константы, среднее/медиана, мода,
ffill/bfillи т.д.
- константы, среднее/медиана, мода,
- Важно:
- сначала понять природу пропусков,
- затем выбирать стратегию так, чтобы не вносить искажений в данные и не выкидывать ценные примеры без необходимости.
Вопрос 21. Какие алгоритмы машинного обучения ты знаешь и какие из них уместно применять на практике?
Таймкод: 00:22:21
Ответ собеседника: неполный. Перечисляет KNN, линейную регрессию, линейную классификацию, деревья решений, случайный лес и градиентный бустинг, но неуверенно. Из практики называет в основном учебные задачи; продвинутые алгоритмы (случайный лес, бустинг) фактически не применял.
Правильный ответ:
Корректный и зрелый ответ на этот вопрос — не просто список алгоритмов, а понимание:
- их классов (линейные, деревья, ансамбли, инстанс-базированные, вероятностные, нейросетевые),
- задач, для которых они подходят,
- сильных/слабых сторон,
- базовых гиперпараметров и практических нюансов.
Ниже — структурированный обзор часто применяемых алгоритмов, с акцентом на практическое использование.
Линейные модели
- Линейная регрессия (Ordinary Least Squares)
- Задача: регрессия (прогноз непрерывной величины).
- Идея:
- аппроксимируем целевое значение линейной комбинацией признаков.
- Плюсы:
- интерпретируемость (веса как вклад признаков),
- быстрая,
- базовый strong baseline.
- Минусы:
- линейность: плохо моделирует сложные нелинейные зависимости без инженерии признаков,
- чувствительна к мультиколлинеарности и выбросам.
- На практике:
- почти всегда используем регуляризацию:
- Ridge (L2), Lasso (L1), Elastic Net.
- почти всегда используем регуляризацию:
- Логистическая регрессия
- Задача: бинарная и многоклассовая классификация.
- Выход: вероятность класса через сигмоиду/softmax.
- Плюсы:
- интерпретируемость,
- быстрая,
- хорошо работает с нормированными признаками,
- baseline для многих задач (кредитный скоринг, churn, fraud).
- Минусы:
- линейность границы решений,
- требует инженерии признаков для сложных зависимостей.
- Линейные SVM (и kernel SVM)
- Используются для классификации; линейные — для больших sparse задач (NLP, high-dimensional).
- Kernel SVM:
- мощные для небольших датасетов,
- плохо масштабируются по данным.
Деревья решений и ансамбли
- Дерево решений (Decision Tree)
- Задачи: классификация, регрессия.
- Идея:
- рекурсивное разбиение пространства признаков по правилам вида “feature <= threshold”.
- Плюсы:
- интерпретируемость (правила),
- умеет работать с нелинейностями и взаимодействиями признаков без их явного задания.
- Минусы:
- склонно к переобучению,
- нестабильно к небольшим изменениям данных.
- Случайный лес (Random Forest)
- Ансамбль деревьев решений:
- bootstrap выборки + случайный поднабор признаков на каждом сплите.
- Плюсы:
- устойчив к переобучению из-за усреднения,
- хорошо работает “из коробки”,
- умеет оценивать важность признаков,
- часто сильный baseline для табличных данных.
- Минусы:
- менее интерпретируем, чем одиночное дерево,
- медленнее на очень больших данных,
- размер модели может быть большим.
- Градиентный бустинг (XGBoost, LightGBM, CatBoost)
- Ансамбль деревьев, обучающихся последовательно:
- каждое новое дерево исправляет ошибки предыдущих.
- Современный стандарт для табличных данных.
- Плюсы:
- высокая точность,
- гибкость (много гиперпараметров),
- хорошо работает с разными типами признаков.
- Минусы:
- требует аккуратной настройки (learning_rate, глубина, регуляризация),
- чувствителен к quality данных,
- может переобучаться при неправильной конфигурации.
- Практика:
- XGBoost/LightGBM/ CatBoost — частый выбор для production ML по табличным данным;
- CatBoost особенно удобен для категориальных признаков.
Инстанс-базированные методы
- KNN (k-ближайших соседей)
- Задачи: классификация, регрессия.
- Идея:
- предсказание по ближайшим объектам в пространстве признаков.
- Плюсы:
- простота,
- не требует обучения (ленивая модель).
- Минусы:
- медленный на предсказании для больших датасетов,
- чувствителен к масштабу признаков и размерности,
- плохо масштабируется в продакшене.
- Часто используется как baseline или в обучающих задачах; реже — в боевых системах (если не применяются специальные структуры для поиска ближайших соседей).
Вероятностные модели
- Наивный Байес (Naive Bayes)
- Часто для текстовой классификации (spam detection, sentiment).
- Плюсы:
- очень быстрый,
- хорошо работает на sparse-векторах,
- устойчив, когда признаки условно независимы.
- Минусы:
- “наивное” предположение независимости редко соблюдается,
- точность ограничена, но как baseline — часто хорош.
Кластеризация
- K-means
- Задача: кластеризация.
- Идея:
- минимизировать внутрикластерное расстояние до центров.
- Плюсы:
- простота, скорость,
- полезен для сегментации, предварительного анализа.
- Минусы:
- предположение сферических кластеров,
- чувствительность к масштабированию и инициализации,
- надо заранее задавать k.
Другие: hierarchical clustering, DBSCAN и т.п. — для поиска сложных кластеров.
Нейросетевые модели (в общих чертах)
- Полносвязные сети (MLP)
- Для табличных/обобщённых данных, но обычно уступают бустингу на классических табличных задачах.
- CNN
- Для изображений, видео.
- RNN/LSTM/GRU / Transformer-based модели
- Для последовательностей, текста, временных рядов.
- Современные трансформеры
- NLP, CV, multi-modal — BERT, GPT-подобные, ViT и т.д.
Практическая зрелость ответа:
Хороший ответ должен показывать:
- Умение подобрать алгоритм под задачу:
- табличные данные → деревья, бустинг, линейные модели;
- тексты → TF-IDF + логистическая регрессия / Linear SVM / трансформеры;
- изображения → CNN/трансформеры;
- временные ряды → специальные модели или buстинг с лаговыми признаками.
- Понимание ограничений:
- KNN — плохо для больших данных;
- деревья/ансамбли — мало интерпретируемы, но мощные;
- линейные — быстрые и стабильные, но требуют feature engineering.
- Понимание важности:
- нормализации/масштабирования (для KNN, линейных, SVM),
- работы с дисбалансом классов (class weights, sampling),
- корректного сплита и метрик.
Краткая формулировка для интервью:
- Осмысленно перечислить: линейные модели (регрессия, логистическая), деревья, случайный лес, градиентный бустинг, KNN, наивный Байес, базовые нейросети.
- Уметь коротко сказать, где какой алгоритм обычно применим и какие у него сильные/слабые стороны.
- Подчеркнуть опыт хотя бы с:
- линейными моделями,
- деревьями/случайным лесом,
- градиентным бустингом (XGBoost/LightGBM/CatBoost),
- а также разумный выбор метрик и предобработки под каждый алгоритм.
Вопрос 22. Объясни своими словами принцип работы одного из известных алгоритмов (например, KNN или линейной регрессии).
Таймкод: 00:25:12
Ответ собеседника: правильный. Корректно описал KNN как алгоритм, который не обучается явно, а ищет ближайших соседей по метрике расстояния и решает по ним. Также верно сформулировал идею линейной регрессии как подбора коэффициентов для аппроксимации зависимости.
Правильный ответ:
Разберём два алгоритма — KNN и линейную регрессию — так, как это ожидается при глубоком понимании.
KNN (k-ближайших соседей)
Идея:
- Это instance-based (ленивая) модель:
- не строит явной параметрической функции на этапе обучения;
- “обучение” — фактически запоминание обучающей выборки.
- Для нового объекта:
- считаем расстояние до всех обучающих примеров;
- выбираем k ближайших;
- в классификации:
- голосование по меткам соседей (часто с возможным взвешиванием по расстоянию);
- в регрессии:
- усреднение значений целевой переменной соседей.
Ключевые моменты:
- Выбор метрики:
- Евклидово расстояние для непрерывных признаков;
- Манхэттенское, косинусное, Hamming и т.д. в зависимости от природы признаков.
- Масштабирование:
- Обязательно нормализовать/стандартизовать признаки, иначе признаки с большим масштабом доминируют в расстоянии.
- Параметр k:
- Малый k → модель шумная, склонна к переобучению.
- Большой k → сглаживание, риск недообучения и игнорирования локальной структуры.
- Сложность:
- Наивный предикт: O(N * d) на запрос (N — объектов, d — признаков).
- Для больших данных нужны структуры ускоренного поиска (k-d tree, ball tree, ANN-индексы), но в очень высоких размерностях они деградируют.
- Когда использовать:
- Маленькие/средние датасеты,
- Чёткое определение метрики близости,
- Как baseline или для задач рекомендаций/поиска похожих объектов.
Пример (Python, sklearn):
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
model = make_pipeline(
StandardScaler(),
KNeighborsClassifier(n_neighbors=5, metric="euclidean")
)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
Линейная регрессия
Идея:
-
Модель предполагает линейную зависимость целевой переменной y от набора признаков x:
y ≈ w0 + w1 * x1 + w2 * x2 + ... + wd * xd
-
Задача обучения:
- подобрать вектор весов w, минимизирующий функцию потерь (обычно сумму квадратов ошибок).
Формально:
-
Ищем w, минимизирующий:
sum_i (y_i - (w0 + w · x_i))^2
Способы решения:
- Закрытая формула (Normal Equation) для небольших задач.
- Градиентный спуск и его варианты — в реальных системах и при больших данных.
Расширения и практические моменты:
-
Регуляризация:
- Обычная линейная регрессия (OLS) может:
- переобучаться,
- страдать от мультиколлинеарности.
- Используются:
- Ridge (L2): добавляем λ * ||w||^2;
- Lasso (L1): добавляем λ * ||w||_1 (даёт разреженные веса);
- Elastic Net: комбинация L1 и L2.
- Это:
- стабилизирует модель,
- уменьшает variance,
- иногда помогает в отборе признаков.
- Обычная линейная регрессия (OLS) может:
-
Масштабирование и признаки:
- Для регуляризованных моделей нормализация важна (веса сравнимы).
- Важен feature engineering:
- полиномиальные признаки,
- взаимодействия,
- лог-преобразования и т.п.
- Это позволяет линейной модели приближать нелинейные зависимости.
-
Свойства:
- Интерпретируемость:
- знак и величина w_j — вклад признака;
- удобно для бизнес-аналитики, скоринговых моделей.
- Чувствительность к выбросам:
- квадратичная ошибка сильно штрафует экстремальные точки;
- можно использовать робастные варианты (Huber, quantile regression).
- Интерпретируемость:
Пример (Python, sklearn):
from sklearn.linear_model import LinearRegression, Ridge
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
# Базовая линейная регрессия
lr = LinearRegression()
lr.fit(X_train, y_train)
# Регуляризованная модель (Ridge)
ridge_model = make_pipeline(
StandardScaler(),
Ridge(alpha=1.0)
)
ridge_model.fit(X_train, y_train)
С точки зрения зрелого ответа:
- Важно не только описать словами, но и показать понимание:
- у KNN нет явной фазы обучения, предсказание дорогое, критично масштабирование и выбор метрики;
- у линейной регрессии:
- явно задаётся функциональная форма,
- оптимизируется квадратичная ошибка,
- регуляризация и обработка признаков — неотъемлемая часть практического использования.
- Можно уверенно выбрать один алгоритм и объяснить его так, чтобы было видно понимание шагов, ограничений и реальных применений.
Вопрос 23. Что ты знаешь о нейронных сетях и какие типы сетей уместно использовать в разных задачах?
Таймкод: 00:26:22
Ответ собеседника: неполный. Упоминает полносвязные сети и RNN, знает про функции активации. RNN описывает частично верно (зависимость от прошлых состояний), но неуверенно, LSTM не знает. Практический опыт ограничен учебными примерами и использованием готовых моделей.
Правильный ответ:
Нейронные сети — это класс моделей, которые аппроксимируют сложные (часто сильно нелинейные) зависимости между входом и выходом за счёт композиции простых вычислительных блоков (нейронов) с обучаемыми параметрами (весами). Важны две вещи:
- архитектура (как слои связаны);
- алгоритм обучения (градиентный спуск / его варианты через backpropagation).
Ниже — системный обзор основных типов сетей и их практического применения.
Базовые элементы нейросетей:
-
Линейный слой:
- Вычисляет y = W x + b.
- Без нелинейности вся сеть эквивалентна одной линейной модели.
-
Функции активации:
- Добавляют нелинейность, чтобы модель могла приближать сложные функции.
- Типичные:
- ReLU: max(0, x) — де-факто стандарт в современных архитектурах.
- LeakyReLU, GELU, ELU — сглаженные/улучшенные варианты ReLU.
- Sigmoid — исторически популярна, сейчас в скрытых слоях используется редко (vanishing gradients), актуальна в выходах для бинарной классификации.
- Tanh — схожие проблемы, но иногда применима.
- Правильный выбор активации критичен для стабильного обучения.
-
Обучение: backpropagation + градиентный спуск:
- Считаем loss (например, cross-entropy или MSE).
- Вычисляем градиенты по всем параметрам.
- Обновляем веса (SGD, Adam, RMSProp и т.п.).
- Важны:
- нормализация (BatchNorm/LayerNorm),
- регуляризация (dropout, weight decay),
- корректный learning rate.
Типы нейронных сетей и где они применяются:
- Полносвязные (Feedforward, MLP)
- Архитектура:
- несколько плотных слоев (Dense), каждый связан со всеми нейронами предыдущего.
- Применения:
- табличные данные,
- простые регрессионные и классификационные задачи,
- небольшие/структурированные признаки.
- Плюсы:
- универсальная аппроксимация (теорема универсальной аппроксимации),
- простая реализация.
- Минусы:
- плохо масштабируются на высокоразмерные сложные структуры (картинки, длинные последовательности),
- сильно подвержены переобучению на сыром сложном сигнале без хорошего feature engineering.
- Свёрточные сети (CNN)
- Архитектура:
- свёрточные слои, pooling, часто с skip-коннектами (ResNet и др.).
- Применения:
- компьютерное зрение:
- классификация изображений,
- детекция и сегментация объектов,
- обработка медицинских снимков,
- OCR и т.п.
- компьютерное зрение:
- Ключевые идеи:
- локальные рецептивные поля (учёт локальной структуры),
- shared weights (одни и те же фильтры по всему изображению),
- инвариантность к сдвигам.
- Практически:
- современные CV-системы часто используют или CNN, или vision transformers, или их комбинации.
- Рекуррентные сети (RNN, LSTM, GRU)
- Архитектура:
- последовательно обрабатывают вход, передавая скрытое состояние h_t от шага к шагу.
- Применения (классически):
- временные ряды,
- последовательности токенов (текст, события),
- задачи генерации последовательностей.
- Проблемы у vanilla RNN:
- vanishing/exploding gradients,
- плохо держат долгосрочные зависимости.
- LSTM и GRU:
- добавляют gating-механизмы (input/output/forget gates),
- лучше запоминают долгосрочный контекст,
- были стандартом для NLP и последовательностей до прихода трансформеров.
- Сейчас:
- в новых задачах чаще используются трансформеры, но понимание RNN/LSTM/GRU всё ещё важно.
- Трансформеры (Transformers)
- Архитектура:
- основана на механизме self-attention:
- модель “смотрит” на все элементы последовательности одновременно и учится важным зависимостям.
- основана на механизме self-attention:
- Применения:
- NLP: машинный перевод, summarization, QA, чат-боты, поиск.
- CV: Vision Transformer.
- Мультимодальные модели.
- Практически:
- современные большие языковые модели (включая те, что вы используете сейчас) — это трансформеры.
- ключевой стандарт для сложных последовательных и текстовых задач.
- Автоэнкодеры, вариационные автоэнкодеры, GAN
- Автоэнкодеры:
- сжать вход в латентное представление и восстановить его обратно.
- Применения:
- уменьшение размерности,
- аномалия детекция,
- генерация.
- VAE:
- вероятностная формулировка, даёт гладкое латентное пространство.
- GAN:
- состязательные сети для генерации реалистичных данных (изображения, речь и т.п.).
Практические моменты, которые стоит уметь проговорить:
-
Выбор типа сети по задаче:
- табличные данные:
- чаще бустинг (XGBoost/LightGBM/CatBoost), MLP реже;
- изображения:
- CNN или vision transformers;
- текст:
- трансформеры (BERT, GPT-подобные, seq2seq),
- для простых задач — TF-IDF + линейные модели;
- временные ряды:
- специализированные архитектуры, RNN/LSTM/GRU, 1D-CNN, трансформеры.
- табличные данные:
-
Регуляризация и устойчивость:
- dropout,
- weight decay (L2),
- BatchNorm/LayerNorm,
- ранняя остановка,
- data augmentation.
-
Практический опыт:
- использование фреймворков:
- PyTorch, TensorFlow/Keras;
- умение:
- построить базовую архитектуру (MLP/CNN),
- настроить лосс, оптимизатор, learning rate schedule,
- контролировать overfitting (train vs val curves).
- использование фреймворков:
Краткая формулировка для интервью:
- Объяснить, что:
- полносвязные сети — базовый строительный блок;
- CNN — стандарт для изображений;
- RNN/LSTM/GRU — классический подход к последовательностям;
- трансформеры — современный стандарт для текста и многих последовательных задач;
- Уметь описать:
- роль функций активации,
- идею backpropagation,
- базовые методы борьбы с переобучением и выбора архитектуры под задачу.
Вопрос 24. Что ты делал с диффузионными моделями и как понимаешь принцип их работы?
Таймкод: 00:30:30
Ответ собеседника: неполный. Использовал готовые диффузионные модели для генерации изображений по тексту; описывает в общих чертах: модель итеративно восстанавливает картинку из шума. Технические детали и ключевые понятия не раскрывает.
Правильный ответ:
Диффузионные модели — класс генеративных моделей, которые учатся обращать стохастический процесс "испорчивания" данных шумом. Интуитивно:
- Прямой (forward) процесс: постепенно добавляем шум к реальным данным, пока они не превратятся в почти чистый шум.
- Обратный (reverse) процесс: обученная модель шаг за шагом убирает шум, восстанавливая структуру данных — изображение, звук и т.д.
- Для text-to-image: в обратном процессе модель не просто убирает шум, а делает это так, чтобы итоговое изображение соответствовало текстовому запросу (condition).
Это ключевая идея, но для уверенного технического ответа важно понимать архитектуру процесса и роль обучения.
Базовая схема работы (упрощенно)
- Forward diffusion (обучающий процесс "порчи" данных):
-
У нас есть реальные данные x₀ (например, изображения).
-
Определяем последовательность шагов t = 1..T, на каждом из которых добавляем небольшой гауссовский шум:
x_t = sqrt(α_t) * x_{t-1} + sqrt(1 - α_t) * ε, где ε ~ N(0, I)
-
При достаточном количестве шагов T получаем x_T, распределение которого близко к N(0, I) — почти чистый шум.
-
Параметры {α_t} образуют "шедулер" шума (линейный, косинусный и т.п.).
- Обучение модели (reverse process):
- В идеале хотим модель, которая умеет восстанавливать p(x_{t-1} | x_t), то есть один шаг "обратной" диффузии.
- На практике:
- обучается нейросеть ε_θ(x_t, t, condition), которая по зашумлённому x_t, шагу t (и опциональному условию, например тексту) предсказывает добавленный шум ε.
- Лосс:
- чаще всего MSE между истинным шумом и предсказанным: L = E[||ε - ε_θ(x_t, t, condition)||²]
- это упрощает обучение и хорошо работает на практике.
- Генерация (sampling):
Процесс генерации — это имитация обратного процесса:
- Начинаем с x_T ~ N(0, I) — случайный шум.
- Для t = T..1:
- подаём (x_t, t, condition) в модель ε_θ;
- вычисляем оценку "очищенного" x_{t-1} по формуле из диффузионного процесса (учитывая α_t и предсказанный шум);
- при необходимости добавляем стохастический компонент.
- После серии шагов получаем x_0 — сгенерированное изображение.
В text-to-image моделях:
- condition — это текстовое описание:
- текст кодируется encoder-ом (например, CLIP text encoder);
- эмбеддинг текста подается в диффузионную сеть (U-Net) через:
- cross-attention,
- или конкатенацию/адаптивные слои.
- В результате модель учится:
- при обратной диффузии "формировать" структуру шума так, чтобы итоговое изображение соответствовало семантике текста.
Ключевые компоненты современных диффузионных моделей:
- Архитектура сети:
- Как правило, U-Net:
- downsampling → bottleneck → upsampling,
- skip-коннекты для сохранения деталей.
- Добавляются:
- positional/time embeddings (кодирование шага t),
- механизмы condition (text/image/label).
- Типы диффузионных моделей:
- DDPM (Denoising Diffusion Probabilistic Models):
- базовая формулировка, вероятностный процесс.
- DDIM:
- детерминистический sampling, ускоряет генерацию.
- Latent diffusion (как в Stable Diffusion):
- диффузия происходит не в пространстве пикселей, а в латентном пространстве autoencoder-а:
- encoder: сжимает изображение в латент;
- U-Net-диффузия: работает в компактном пространстве;
- decoder: разворачивает латент обратно в изображение.
- Это сильно ускоряет обучение и генерацию.
- диффузия происходит не в пространстве пикселей, а в латентном пространстве autoencoder-а:
- Контроль и условности (conditioning):
- Text-to-image:
- CLIP/текстовый encoder → эмбеддинг → cross-attention в U-Net.
- Class-conditioned:
- условие — one-hot/class embedding.
- Image-conditioned:
- inpainting, super-resolution, style transfer:
- используем часть изображения или низкое разрешение как condition.
- inpainting, super-resolution, style transfer:
- Практические моменты и почему это важно понимать:
-
Преимущества диффузионных моделей:
- Высокое качество и разнообразие сэмплов:
- они хорошо моделируют сложные многомодальные распределения.
- Более устойчивая и стабильная тренировка, чем у классических GAN.
- Высокое качество и разнообразие сэмплов:
-
Недостатки:
- Многократные итерации sampling → высокая стоимость генерации.
- Это частично решается:
- улучшенными schedulers (DDIM, DPM-Solver),
- уменьшением числа шагов (10–50 вместо сотен/тысяч).
-
Инженерные аспекты:
- high-load inference:
- оптимизация U-Net (mixed precision, quantization),
- батчинг запросов,
- кэширование текстовых эмбеддингов.
- high-load inference:
Краткая формулировка для интервью:
- Диффузионная модель учится обращать процесс добавления шума:
- на обучении: к реальным данным по шагам добавляется шум, сеть учится по зашумлённым данным предсказывать шум (или "очищенную" версию);
- на генерации: начинаем с шума и многократно применяем обученную модель удаления шума, получая реалистичный образ.
- В text-to-image моделях:
- обратный процесс дополнительно кондиционируется текстом, чтобы результат соответствовал описанию.
- Важно:
- понимать forward/reverse процессы,
- роль U-Net и conditioning,
- отличие latent diffusion (Stable Diffusion) от пиксельных моделей.
Вопрос 25. Для чего используются NumPy и pandas, и в чём различия их назначения?
Таймкод: 00:32:29
Ответ собеседника: правильный. Корректно пояснил, что NumPy используется для эффективной работы с числами и массивами/матрицами (под капотом C, база для многих ML-библиотек), а pandas — для работы с табличными данными, выборок и анализа, часто вместе с визуализацией.
Правильный ответ:
NumPy и pandas решают разные, но взаимосвязанные задачи в Python-экосистеме для научных вычислений и анализа данных.
NumPy:
Основное назначение:
- Эффективные численные вычисления:
- многомерные массивы (ndarray),
- векторизованные операции,
- линейная алгебра, работа с матрицами,
- генерация случайных чисел,
- базовые численные алгоритмы.
Ключевые особенности:
- Реализован на C/Fortran, работает значительно быстрее чистых Python-циклов.
- Операции над массивами выполняются пакетно (vectorized), минимизируя Python overhead.
- Широко используется как "низкоуровневая" основа:
- для pandas,
- для scikit-learn,
- для многих DL-фреймворков (исторически),
- для численных/научных библиотек.
Пример (NumPy как матричная арифметика):
import numpy as np
a = np.array([[1, 2], [3, 4]])
b = np.array([[10, 20], [30, 40]])
# Поэлементные операции
c = a + b # [[11, 22], [33, 44]]
# Матричное умножение
d = a @ b # [[70, 100],
# [150, 220]]
Когда использовать NumPy:
- Если нужно:
- быстро и эффективно считать по массивам/тензорам,
- реализовывать математику, линейную алгебру, оптимизацию,
- писать низкоуровневые численные компоненты, на которых строятся другие библиотеки.
pandas:
Основное назначение:
- Анализ и обработка табличных и временных данных:
- DataFrame (таблица: строки-наблюдения, столбцы-признаки),
- Series (одномерный labeled-вектор),
- индексация, фильтрация, groupby-агрегации, join/merge, ресемплинг.
Ключевые особенности:
- Структуры с метаданными:
- осмысленные имена столбцов и индексов;
- удобные выборки по меткам, а не только по позициям.
- Высокоуровневые операции:
- groupby / aggregation,
- join/merge,
- pivot / pivot_table,
- работа с датами и временными рядами,
- чтение/запись CSV, Parquet, SQL и т.д.
- Внутри активно использует NumPy для хранения данных, но даёт более богатый API для "data wrangling".
Пример (pandas как табличный инструмент):
import pandas as pd
df = pd.DataFrame({
"user_id": [1, 2, 3, 4],
"city": ["Moscow", "SPB", "Moscow", "Kazan"],
"amount": [100, 200, 150, 50],
})
# Фильтрация
df_moscow = df[df["city"] == "Moscow"]
# Группировка и агрегация
stats = df.groupby("city")["amount"].sum()
# city
# Kazan 50
# Moscow 250
# SPB 200
# Join с другой таблицей (как в SQL)
cities = pd.DataFrame({
"city": ["Moscow", "SPB", "Kazan"],
"region": ["RU-MOW", "RU-SPE", "RU-TA"]
})
df_merged = df.merge(cities, on="city", how="left")
Когда использовать pandas:
- Если задача:
- загрузить датасет (CSV, SQL, Parquet),
- очистить данные (NaN, дубликаты, типы),
- трансформировать признаки,
- агрегировать/джойнить по ключам,
- подготовить данные для обучения моделей (обычно → NumPy или прямо в ML-библиотеки).
Связь и различия:
-
NumPy:
- про численные массивы, скорость и математику;
- индексы — позиционные, без семантики колонок;
- удобен для матричных операций, линейной алгебры и внутренних реализаций.
-
pandas:
- про табличные, бизнес-ориентированные данные;
- имеет имена столбцов, индексы, мощный API для трансформаций;
- строится поверх NumPy.
Простая практическая линия:
- "pandas для подготовки данных" → "NumPy / массивы / тензоры для модели".
Краткая формулировка для интервью:
- NumPy — базовая библиотека для высокопроизводительных численных вычислений и работы с многомерными массивами.
- pandas — высокоуровневая библиотека для анализа и трансформации табличных данных, построена поверх NumPy и предоставляет DataFrame/Series и удобный API для работы с реальными датасетами.
Вопрос 26. Для чего используется pandas и чем он удобнее обычных структур при работе с табличными данными?
Таймкод: 00:33:41
Ответ собеседника: правильный. Отмечает, что pandas позволяет удобно работать с табличными данными и столбцами, проще искать зависимости и визуально воспринимать данные, чем с “сырыми” массивами/матрицами.
Правильный ответ:
pandas — это специализированная библиотека для работы с табличными и временными данными, которая существенно упрощает анализ, очистку и трансформацию данных по сравнению с использованием базовых структур Python (list, dict) или "голого" NumPy.
Ключевые преимущества pandas при работе с табличными данными:
- Семантика табличных данных
- Основные структуры:
- DataFrame — аналог таблицы: строки = объекты/наблюдения, столбцы = признаки.
- Series — одномерный labeled-вектор (столбец или индекс).
- В отличие от обычных списков и NumPy-массивов:
- у столбцов есть имена;
- у строк есть индексы (которые могут не быть просто 0..N-1);
- операции можно выражать через имена колонок, а не через "магические" индексы.
Пример:
import pandas as pd
df = pd.DataFrame({
"user_id": [1, 2, 3],
"city": ["Moscow", "SPB", "Kazan"],
"amount": [100, 200, 150],
})
# Понятно по смыслу:
high = df[df["amount"] > 150]
- Удобная фильтрация, агрегации и группировки
pandas даёт декларативный, компактный и читаемый API для операций, которые на list/dict/чистом NumPy писались бы вручную циклами:
-
Фильтрация:
df_moscow = df[df["city"] == "Moscow"] -
Группировка и агрегация:
total_by_city = df.groupby("city")["amount"].sum() -
Сводные таблицы:
pivot = df.pivot_table(values="amount", index="city", aggfunc="mean")
- Join/merge как в SQL
pandas позволяет естественно объединять таблицы по ключам:
cities = pd.DataFrame({
"city": ["Moscow", "SPB"],
"region": ["RU-MOW", "RU-SPE"],
})
df_merged = df.merge(cities, on="city", how="left")
Это намного проще и безопаснее, чем ручная синхронизация нескольких списков или словарей.
- Работа с пропусками и типами данных
- Пропуски:
isna(),fillna(),dropna()— компактные и выразительные операции для обработки NaN.
- Типы:
- удобное преобразование типов (
astype), - специализированная поддержка дат/времени (
datetime64, ресемплинг, time-based индекс).
- удобное преобразование типов (
Пример:
df["amount"] = df["amount"].fillna(0)
df["date"] = pd.to_datetime(df["date"])
- Интеграция с NumPy, matplotlib и ML-библиотеками
- DataFrame легко конвертируется в NumPy-массивы:
X = df[["amount"]].to_numpy() - Хорошо работает с визуализацией:
df["amount"].hist() - Является стандартным входом/выходом для многих ML-инструментов (scikit-learn, statsmodels и др.).
- Производительность и лаконичность
- Под капотом pandas опирается на NumPy и векторизованные операции:
- множество операций реализовано на C/компилируемых частях;
- избавляет от Python-циклов при обработке больших объемов данных.
- То, что в чистом Python потребовало бы десятки строк циклов и условий, в pandas пишется в 1–3 строки и легче читается и ревьюится.
- Читаемость и сопровождение кода
- Код на pandas ближе к SQL/аналитическому стилю:
- проще анализировать, дебажить, обсуждать в команде аналитиков и разработчиков.
- По сравнению с наборами list/dict:
- меньше шансов сделать ошибку в индексах,
- проще рефакторить (имеются имена колонок и явные операции).
Краткая суть:
- pandas используется для:
- загрузки, очистки, трансформации, агрегации и анализа табличных данных.
- Он удобнее обычных структур и "чистого" NumPy тем, что:
- предоставляет осмысленные табличные абстракции (DataFrame/Series),
- имеет мощный высокоуровневый API (groupby, merge, pivot, resample),
- интегрируется с остальной экосистемой анализа данных,
- делает код короче, понятнее и менее ошибкоопасным.
Вопрос 27. Расскажи о своём опыте работы в прошлой компании.
Таймкод: 00:34:28
Ответ собеседника: правильный. Кратко описал опыт поддержки ERP-системы Microsoft в ритейле: исправление багов, небольшие фичи, выполнение запросов бизнеса, активная работа с базой данных; отметил, что опыт частично нерелевантен ML, но даёт практику промышленной разработки.
Правильный ответ:
При ответе на такой вопрос важно структурированно показать:
- реальный боевой опыт,
- работу с продакшн-системами,
- ответственность,
- навыки, которые транслируются в текущую позицию (разработка, качество, работа с данными, взаимодействие с бизнесом),
- а не пересказывать резюме в общем виде.
Хороший развёрнутый ответ мог бы выглядеть так.
- Контекст проекта
Опиши систему кратко, но по-деловому:
- Что это за ERP/информационная система.
- Масштаб: количество пользователей, точек, интеграций.
- Критичность: деньги, склады, заказы, логистика.
Например:
- Это централизованная ERP-система крупной розничной сети, завязанная на:
- управление заказами,
- остатками на складах и в магазинах,
- ценами и акциями,
- интеграции с кассами, бухгалтерией, логистикой.
- Зона ответственности
Подчеркни, что занимался не “абстрактным кодом”, а конкретными задачами с влиянием на бизнес.
Типичные направления (можно адаптировать под факт):
-
Поддержка и развитие ядра ERP:
- поиск и исправление багов, влияющих на:
- корректность расчетов,
- целостность данных,
- стабильность интеграций.
- примеры:
- неправильный расчёт скидок,
- некорректное отображение остатков,
- рассинхронизация между модулями.
- поиск и исправление багов, влияющих на:
-
Реализация небольших фич по запросам бизнеса:
- доработки отчётов,
- новые поля/атрибуты в справочниках,
- дополнительные проверки и валидации,
- настройка бизнес-правил.
-
Интеграции:
- взаимодействие с внешними системами:
- выгрузки/загрузки данных,
- обмен через файлы, API, очереди.
- обеспечение надёжности и идемпотентности таких процессов.
- взаимодействие с внешними системами:
- Работа с базой данных
Сделай акцент, что это не просто "писал SELECT", а системная работа с данными:
- Написание и оптимизация SQL-запросов:
- сложные join-ы,
- агрегации для отчётности,
- выборки для аналитики и поддержки бизнеса.
- Разбор проблем производительности:
- поиск медленных запросов,
- анализ планов выполнения,
- индексы, денормализация, корректировка запросов.
- Обеспечение целостности:
- транзакции,
- ограничения,
- аккуратные миграции и правки данных в проде.
Пример SQL-фрагмента (концептуально):
SELECT s.store_id,
p.sku,
SUM(m.quantity) AS total_stock
FROM stock_movements m
JOIN products p ON p.id = m.product_id
JOIN stores s ON s.id = m.store_id
WHERE m.movement_date >= CURRENT_DATE - INTERVAL '7 days'
GROUP BY s.store_id, p.sku;
- Качество, процессы и инженерная зрелость
Важно показать, что ты привык работать как инженер, а не как "техподдержка":
- Code review:
- участие в ревью изменений,
- соблюдение coding style и внутренних стандартов.
- Тестирование:
- регрессионные проверки при изменении критичных модулей,
- написание или хотя бы запуск тестов (юнит/интеграционные, если были).
- Работа с инцидентами:
- разбор продакшн-проблем,
- поиск корневых причин (root cause analysis),
- фиксы без нарушения работы системы.
- CI/CD:
- участие в процессе релизов,
- понимание, как изменения проходят путь до продакшена.
- Взаимодействие с бизнесом
Подчеркни опыт коммуникаций:
- Работа с требованиями:
- получение задач от бизнеса (аналитики, операционный блок),
- уточнение требований,
- объяснение ограничений и последствий.
- Приоритизация:
- умение балансировать между “починить срочно” и “сделать правильно”.
- Понимание домена:
- логистика, скидки, остатки, отчётность — это полезный опыт для любых data-driven систем.
- Связь с дальнейшим развитием (в сторону разработки и ML/данных)
Даже если стек был не ML и не Go, важно показать трансфер навыков:
- Работа с реальными, “грязными” данными:
- понимание, что данные в продакшене:
- неполные,
- противоречивые,
- требуют валидации и очистки.
- понимание, что данные в продакшене:
- Ответственность за стабильность:
- изменения нельзя вносить "на удачу";
- нужна внимательность, откатные сценарии, тесты.
- Опыт в оптимизации:
- и по коду, и по базе.
- Умение входить в чужой легаси-код:
- читать,
- разбираться,
- аккуратно расширять.
Краткая, сильная версия ответа для интервью:
- Кратко описать контекст ERP-системы и масштабы.
- Чётко перечислить:
- поддержка продакшн-функционала (багфиксы, мелкие фичи),
- много SQL и работа с производительностью,
- интеграции и отчёты,
- взаимодействие с бизнес-заказчиками.
- Сделать акцент: этот опыт научил аккуратной промышленной разработке, работе с критичными данными и продакшн-системами, что напрямую переносится в любую инженерную роль, включая разработку backend/ML-сервисов.
Вопрос 28. Как у тебя обстоят дела с SQL и какие задачи ты выполнял?
Таймкод: 00:35:14
Ответ собеседника: правильный. Уверенно говорит, что хорошо владеет SQL, часто писал сложные запросы в контексте ERP-системы; базы данных сам не администрировал, но имеет существенный практический опыт написания запросов.
Правильный ответ:
Сильный ответ по SQL должен показать не только умение писать базовые SELECT’ы, но и системное владение:
- реляционной моделью,
- сложными запросами,
- оптимизацией,
- транзакционной семантикой,
- пониманием, как это встраивается в приложения (в т.ч. на Go).
Ключевые аспекты, которые стоит отражать.
- Типы задач, которые ожидаемо решать уверенно
- Получение отчетов и агрегатов:
GROUP BY, оконные функции, вложенные запросы.
- Поиск и анализ проблемных данных:
- неконсистентность, дубликаты, нарушения ограничений.
- Сложные выборки:
- множественные
JOIN(INNER/LEFT/RIGHT/FULL), - комбинирование условий,
- подзапросы в
WHERE/FROM/SELECT.
- множественные
- Подготовка данных для других систем:
- выгрузки, представления (VIEW), временные таблицы.
Пример комплексного запроса:
SELECT
o.customer_id,
COUNT(*) AS orders_count,
SUM(o.amount) AS total_amount,
MAX(o.created_at) AS last_order_at
FROM orders o
JOIN customers c
ON c.id = o.customer_id
WHERE
o.status = 'completed'
AND o.created_at >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY
o.customer_id
HAVING
SUM(o.amount) > 10000
ORDER BY
total_amount DESC
LIMIT 100;
Такие запросы типичны для ERP/BI-кейсов и демонстрируют владение базовой аналитикой.
- Глубина владения: JOIN’ы, агрегаты, оконные функции
- JOIN:
- понимание, когда использовать INNER vs LEFT JOIN;
- умение писать запросы без дубликатов и "взрывов" данных.
- Агрегации:
GROUP BY,HAVING, агрегационные функции (SUM,COUNT,AVG,MAX,MIN).
- Оконные функции (важный маркер уровня):
ROW_NUMBER(),RANK(),DENSE_RANK(),- оконные агрегаты
SUM() OVER (...),AVG() OVER (...), - разделение по партциям (PARTITION BY).
Пример оконной функции:
SELECT
o.customer_id,
o.id AS order_id,
o.amount,
SUM(o.amount) OVER (
PARTITION BY o.customer_id
ORDER BY o.created_at
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM orders o
WHERE o.created_at >= CURRENT_DATE - INTERVAL '30 days';
- Целостность данных и транзакции
Ожидается понимание:
- Первичные и внешние ключи, уникальные и check-ограничения.
- Транзакции:
BEGIN / COMMIT / ROLLBACK;- зачем нужны;
- что такое атомарность, согласованность, изолированность, долговечность (ACID).
- Уровни изоляции:
- хотя бы на концептуальном уровне:
- грязное чтение, non-repeatable read, phantom read;
- как СУБД балансирует между изоляцией и конкурентностью.
- хотя бы на концептуальном уровне:
- Оптимизация запросов
Зрелый опыт включает:
- Понимание индексов:
- B-tree индексы, составные индексы, выбор правильного порядка полей;
- влияние индексов на фильтрацию и сортировку.
- Анализ планов выполнения:
EXPLAIN/EXPLAIN ANALYZE:- Nested Loop, Hash Join, Seq Scan, Index Scan.
- Типичные практики:
- избегать "SELECT *" в тяжёлых запросах;
- фильтровать как можно раньше;
- не злоупотреблять подзапросами, когда JOIN читаемее и быстрее.
Пример использования индекса в прикладном коде (Go):
type Order struct {
ID int64
CustomerID int64
Amount float64
CreatedAt time.Time
}
const findOrdersByCustomerSQL = `
SELECT id, customer_id, amount, created_at
FROM orders
WHERE customer_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3;
`
func (r *OrderRepo) FindByCustomer(
ctx context.Context,
customerID int64,
limit, offset int,
) ([]Order, error) {
rows, err := r.db.QueryContext(ctx, findOrdersByCustomerSQL, customerID, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
var res []Order
for rows.Next() {
var o Order
if err := rows.Scan(&o.ID, &o.CustomerID, &o.Amount, &o.CreatedAt); err != nil {
return nil, err
}
res = append(res, o)
}
return res, rows.Err()
}
Под это место обычно создаётся индекс:
CREATE INDEX idx_orders_customer_created_at
ON orders (customer_id, created_at DESC);
- Встраивание SQL в приложения
Для прикладного разработчика важно:
- Понимать, как писать SQL так, чтобы:
- он был безопасен (prepared statements, защита от SQL injection),
- предсказуем по производительности,
- читабелен для команды.
- Знать подходы:
- миграции схемы (Liquibase, Flyway, goose и т.п.),
- версионирование схемы,
- разделение ответственности: бизнес-логика в коде, а не в "магии" триггеров.
- Как это презентовать на интервью
Сильный ответ:
- Подтвердить уверенное владение SQL.
- Кратко привести реальные типы задач:
- сложные JOIN/агрегации для отчётности и аналитики;
- выборки для бизнес-отчётов, сверок, мониторинга;
- диагностика проблем в производственных данных запросами;
- оптимизация тяжёлых запросов.
- Показать понимание:
- индексов,
- транзакций,
- базовой оптимизации.
- Честно обозначить границу:
- администрирование (backup, репликация, тюнинг параметров СУБД) — отдельная роль,
- но чтение планов, проектирование схем и индексов — знакомо и практикуется.
Вопрос 29. Что ты знаешь о SQLAlchemy и как понимаешь её назначение?
Таймкод: 00:36:18
Ответ собеседника: неполный. Знает SQLAlchemy на теории, практически почти не использовал. Описывает как инструмент для динамического формирования запросов и более удобной записи SQL в стиле Python-кода, абстрагирующий сырые SQL-строки, без раскрытия архитектуры и ключевых концепций.
Правильный ответ:
SQLAlchemy — это зрелый и мощный стек для работы с реляционными базами данных в Python, который предоставляет:
- гибкий, выразительный Core-уровень (конструктор SQL),
- ORM-уровень для объектного маппинга,
- унифицированный интерфейс к разным СУБД,
- управление соединениями, транзакциями и жизненным циклом сессий.
Важно понимать, что SQLAlchemy — это не просто “обёртка над SQL”, а инструмент, который позволяет:
- писать сложные запросы декларативно, не теряя контроля над SQL;
- поддерживать крупные кодовые базы с чистой архитектурой доступа к данным;
- избежать жёсткой привязки к конкретной СУБД (PostgreSQL, MySQL, SQLite и т.д.).
Ключевые концепции SQLAlchemy:
- Engine и Connection
- Engine:
- основной объект, инкапсулирующий:
- пул соединений,
- диалект СУБД (Postgres, MySQL и т.д.),
- низкоуровневые детали подключения.
- основной объект, инкапсулирующий:
- Создание:
from sqlalchemy import create_engine
engine = create_engine(
"postgresql+psycopg2://user:password@localhost:5432/mydb",
echo=False, # логировать SQL
pool_pre_ping=True, # проверка соединений
)
- Через
engineвыполняются запросы, создаются сессии ORM.
- SQLAlchemy Core
Это слой, дающий декларативный конструктор SQL, сохраняя близость к “ручному” SQL:
- Определение таблиц:
from sqlalchemy import MetaData, Table, Column, Integer, String
metadata = MetaData()
users = Table(
"users", metadata,
Column("id", Integer, primary_key=True),
Column("name", String, nullable=False),
Column("email", String, unique=True, nullable=False),
)
- Конструирование запросов:
from sqlalchemy import select
stmt = select(users).where(users.c.email == "john@example.com")
with engine.connect() as conn:
row = conn.execute(stmt).fetchone()
Преимущества Core:
- Статически описываем структуру и запросы,
- Получаем безопасное формирование SQL без конкатенации строк,
- Легко читать и дебажить,
- Можно использовать как “тонкий слой” поверх реального SQL без тяжёлой ORM.
- ORM (Object-Relational Mapping)
ORM-слой SQLAlchemy позволяет маппить строки таблиц на Python-объекты:
- Декларативное описание модели:
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
email: Mapped[str]
# Инициализация
engine = create_engine("postgresql+psycopg2://user:pass@localhost/mydb")
SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)
- Работа через ORM:
def get_user_by_email(db, email: str) -> User | None:
return db.query(User).filter(User.email == email).first()
def create_user(db, name: str, email: str) -> User:
user = User(name=name, email=email)
db.add(user)
db.commit()
db.refresh(user)
return user
with SessionLocal() as db:
u = create_user(db, "John", "john@example.com")
Преимущества ORM:
- Менее шаблонный код при типичных CRUD-операциях.
- Единые модели, которые:
- используются в бизнес-логике,
- отражают схему БД.
- Возможность декларативно задавать связи:
- one-to-many, many-to-many, lazy/eager loading и т.д.
Важный момент: ORM SQLAlchemy не прячет SQL — при необходимости всегда можно спуститься на уровень Core или сырого SQL.
- Управление транзакциями и сессиями
- Сессия (Session):
- отвечает за:
- транзакции,
- identity map (каждая строка БД → один объект в сессии),
- отслеживание изменений и их flush/commit.
- отвечает за:
- Зрелое использование:
- явный контроль границ сессии и транзакции (per-request / unit of work),
- корректное закрытие сессий,
- отсутствие "висящих" долгоживущих сессий в веб-приложениях.
Типичный паттерн (упрощённо, в духе unit-of-work):
from contextlib import contextmanager
@contextmanager
def get_session():
session = SessionLocal()
try:
yield session
session.commit()
except:
session.rollback()
raise
finally:
session.close()
- Безопасность и переносимость
- SQLAlchemy заботится о:
- безопасной подстановке параметров (подготовленные выражения) — защита от SQL injection при корректном использовании.
- абстракции диалектов: минимизируем vendor lock-in, если не упираемся в специфичные фичи.
- При этом:
- можно воспользоваться возможностями конкретной БД (Postgres, MySQL) через диалект-специфичные расширения и сырой SQL.
- Где SQLAlchemy особенно уместна
- Средние и крупные проекты на Python:
- веб-сервисы (FastAPI, Flask),
- backend-части ML-систем (хранение featurized данных, отслеживание экспериментов),
- микросервисы поверх Postgres/MySQL.
- Когда:
- нужен контролируемый слой доступа к данным,
- много сущностей и связей,
- важно разделить доменную модель и уровень SQL,
- нужна поддерживаемость и тестируемость.
- Аналогия с подходами в Go
Хотя вопрос про Python, полезно мыслить по-архитектурному:
- В Go типичный стек:
database/sql+ драйвер + легковесный слой (sqlx, squirrel, gorm).
- SQLAlchemy ближе по идеологии к:
- gorm (ORM) + squirrel (builder) одновременно:
- есть и мощный ORM,
- и низкоуровневый конструктор запросов.
- gorm (ORM) + squirrel (builder) одновременно:
Зрелый ответ в собеседовании:
- Показать понимание, что SQLAlchemy:
- включает два уровня: Core и ORM;
- управляет соединениями и транзакциями;
- абстрагирует SQL, но не скрывает его.
- Упомянуть:
- Engine, Session, декларативные модели;
- преимущества: безопасность, выразительность, переносимость, тестируемость.
- Отметить, что при необходимости можно:
- комбинировать ORM и “сырые” запросы,
- держать контроль над перформансом и сложными запросами.
Вопрос 30. Какой у тебя опыт работы с Matplotlib и для чего его имеет смысл использовать?
Таймкод: 00:37:27
Ответ собеседника: правильный. Использовал Matplotlib для анализа датасетов и построения графиков, отмечает, что визуализация помогает лучше понимать данные, чем просто смотреть на числа в pandas.
Правильный ответ:
Matplotlib — это базовая, низкоуровневая библиотека для визуализации данных в Python. Её роль — дать гибкий и контролируемый способ строить графики, поверх которого уже строятся более высокоуровневые библиотеки (seaborn, pandas.plot, многие dashboard-фреймворки).
Ключевые сценарии, в которых Matplotlib особенно уместен:
- Разведочный анализ данных (EDA)
- Быстрый визуальный просмотр распределений и зависимостей:
- понимание масштаба значений;
- обнаружение выбросов;
- интуитивное сравнение групп, классов, временных периодов.
- Примеры:
- распределение целевой переменной;
- зависимость признаков от времени;
- сравнение классов в задаче классификации.
import matplotlib.pyplot as plt
# Гистограмма распределения признака
plt.hist(df["amount"], bins=50)
plt.xlabel("Amount")
plt.ylabel("Count")
plt.title("Distribution of Amount")
plt.show()
- Связка с pandas и NumPy
- Matplotlib хорошо интегрируется:
pandas.DataFrame.plotпод капотом использует Matplotlib.
- Типичный паттерн:
- агрегации и подготовка данных в pandas;
- визуализация через Matplotlib для тонкой настройки.
by_city = df.groupby("city")["amount"].sum()
plt.figure(figsize=(8, 4))
by_city.plot(kind="bar")
plt.ylabel("Total amount")
plt.title("Sales by city")
plt.tight_layout()
plt.show()
- Гибкость и кастомизация
Matplotlib позволяет детально контролировать:
- подписи осей, шрифт, цветовую схему;
- легенды, сетку, лимиты осей;
- несколько осей/подграфиков (subplots);
- стили и темы (plt.style.use).
Пример с несколькими графиками:
fig, ax = plt.subplots(1, 2, figsize=(10, 4))
ax[0].hist(df["feature1"], bins=30, color="steelblue")
ax[0].set_title("Feature 1")
ax[1].hist(df["feature2"], bins=30, color="orange")
ax[1].set_title("Feature 2")
plt.tight_layout()
plt.show()
- Использование в ML/DS-проектах
Практически полезные паттерны:
- Отрисовка:
- обучающих и валидационных метрик по эпохам (loss, accuracy, F1);
- ROC-кривых, PR-кривых;
- важности признаков (feature importance) для бустинга/лесов;
- предсказаний vs реальных значений.
- Диагностика:
- learning curves;
- распределение ошибок;
- визуализация результатов кластеризации.
Пример: график обучения модели:
epochs = range(len(train_loss))
plt.plot(epochs, train_loss, label="train_loss")
plt.plot(epochs, val_loss, label="val_loss")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.legend()
plt.title("Training dynamics")
plt.show()
- Почему важно уметь работать с Matplotlib, даже при наличии "удобных обёрток"
- Высокоуровневые библиотеки (seaborn, plotly, pandas.plot):
- дают быстрый результат,
- но при сложных кастомизациях всё равно приходится опускаться до Matplotlib.
- В продакшн-инструментах (отчёты, internal dashboards, генерируемая аналитика):
- Matplotlib даёт контроль и воспроизводимость.
- Важно:
- понимать модель фигур/осей (Figure/Axes),
- уметь управлять layout (subplots, tight_layout),
- работать с легендами, аннотациями, форматированием.
Краткая формулировка для интервью:
- Matplotlib — стандартный инструмент для визуализации в Python:
- используется для разведочного анализа данных,
- диагностики моделей,
- построения настраиваемых графиков.
- Его сила:
- низкоуровневый контроль и гибкость,
- тесная интеграция с NumPy/pandas,
- возможность делать как быстрые черновые графики, так и аккуратные отчётные визуализации.
