Перед собесом
Ну что ... настал день Х, сегодня у меня будет собес. Особо не расчитываю на успех, главное потренироваться на компаниях в которых я не особо хочу работать.
Перечитал свое резюме, пересмотрел требования к вакансии:
- "Уверенное владение Python" - ага, конечно.
- "Уверенное знание SQL" - ну если считать одну таблицу на 50 строк опытом, то ок.
- "Знание FastAPI" - ну базу то знаем, рассказать смогу, но если надо написать в моменте что-либо наверняка поплыву.
До созвона 2 часа. Решил не зубрить ничего, а пробежаться по своим проектам. Открыл репозиторий, полистал файлы, чекнул что где лежит, что за что отвечает. За 15 минут до созвона просто не знал куда деть свои руки от нервозности. Сидел как на приеме у стоматолога.
В комнату заглянула жена, спросила: "чего такой бледный, это ж просто разговор?" Ну да подумал я про себя, тебе ж не надо как школьник у доски отчитываться перед незнакомым человеком что ты знаешь и почему в 38 годиков решил пойти в IT.
Начался созвон, прозвучали банальные вопросы: типы данных, декораторы, *args и **kwargs, что такое gil и т.д. Вроде бы получалось без проблем на все ответить. Потом спросили про генераторы и итераторы и тут я поплыл. Интервьювер открыл редактор и спросил что выведет этот код:
def get_numbers():
for i in range(3):
yield i
gen = get_numbers()
print(list(gen))
print(list(gen))Подумав я выпалил раз генератор выдает 0, 1, 2 значит оба принта выведут [0, 1, 2] , так как генератор создается заново с каждым вызовом.
Интервьювер молчит, я чувствую, что что-то не так
Тут до меня доходит, gen = get_numbers() создано 1 раз, а list(gen) исчерпает генератор и второй list(gen) получит пустой список.
Далее видимо интервьювер понял где хромаю и начал спрашивать дальше:
def f():
print("start")
yield 1
print("middle")
yield 2
print("end")
g = f()
print("создал генератор")
print(next(g))
print(next(g))Вопрос: в каком порядке будет вывод?
Я вновь подумал, раз есть вызов функции начнем со start. Ошибка была в том, что f() не запускает тело функции, а стартует с первым next
Потом был live coding:
Задача: Написать небольшой сервис. Есть склад, есть товары. Товары лежат на складе в каком-то количестве. Нужно написать эндпоинты:
- Эндпоинт, который принимает список товаров и сохраняет их на склад
- Эндпоинт, который возвращает остатки по складу
- Эндпоинт, который списывает товар со склада (если товара не хватает вернуть ошибку)
Шаг 1: модели
Я написал:
class Product(BaseModel):
id: int
name: str
qty: intИнтервьювер спрашивает, что за модель SQLAlchemy или Pydantic?
- " Pydantic", сказал я
- "А где модель для базы?"
Тут я понимаю, что не помню как писать SQLAlchemy модель с нуля. Знаю, что там Base, Column, Integer а точный синтаксис забыл и откуда Base берется тоже не помню. Открыл на телефоне свой github подсмотрел проекты где писал подобное и списал оттуда:
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True)
name = Column(String)
qty = Column(Integer)Шаг 2: схемы
Интервьювер: Теперь пиши схему для запроса. Клиент присылает список товаров
Я быстро написал:
class ProductCreate(BaseModel):
name: str
qty: intИнтервьювер: А для ответа?
Я:
class ProductResponse(BaseModel):
id: int
name: str
qty: int- "А зачем две модели?" спросил он
- "Эм, Ну... чтобы id не приходил от клиента." уже робко ответил я
- "А как ProductResponse узнает, что нужно взять данные из ORM-объекта? У вас же не словарь.?"
Тут я растерялся и понял, что плыву, а точнее сказать тону, и не просто тону, а феерично иду ко дну.
Не помню про orm_mode (или from_attributes в Pydantic v2). Помню, что что-то такое было. Но не помню, что именно. И не помню, зачем.
Шаг 3: База
Тут меня попросили подключить БД.
Я написал :
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()Опять думаю откуда брался SessionLocal...Хз как создать движок и где хранить строку подключеия.
Пишу что-то по памяти
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine("sqlite:///./test.db")
SessionLocal = sessionmaker(bind=engine)Тут понеслись еще вопросы, такое ощущение будто интервьювер специально валить начал, либо хотел выпендриться насколько он крут.
Интервьювер: А почему SQLite? А если PostgreSQL?
Я: ну тут строку заменить надо engine = create_engine("sqlite:///./test.db")
А что еще поменяется спросил он ?... Не дождавшись ответа с моей стороны продолжал: А check_same_thread? А пул соединений? А миграции?
На нервах я хотел уже как можно быстрее чтобы это закончилось, вроде на позицую джуна метил а тут вопросы немного другой сложности мне показалось.
Шаг 4: первый эндпоинт
Интервьювер (Алексей): Напиши эндпоинт, который сохраняет товар.
Я:
python
@app.post("/products")
def create_products(products: list[ProductCreate], db: Session = Depends(get_db)):
for product in products:
db_product = Product(**product.dict())
db.add(db_product)
db.commit()
return {"status": "ok"}Алексей: А если на середине упадёт ошибка? Что будет с теми товарами, которые уже добавились?
Я: Ну... они останутся?
Интервьюер: А должно так быть?
Я: Наверное, нет.
Интервьюер: А как сделать, чтобы не остались?
И я плыву. Потому что не знаю про транзакции. Я знаю слово «транзакция». Но не знаю, как её контролировать в SQLAlchemy. Не знаю про rollback. Не знаю, что commit — это финал транзакции. Не знаю, что если упало до commit — ничего не сохранится. Но если упало после — уже поздно.
Итог:
В общем он задал еще пару вопросов и остановился на этом.
Так хреново я себя давно не чувствовал, как в этот день. На оффер я даже не расчитывал, и собесы как то проходить желание подугасло.
Что подучить:
- Генератор - это итератор с состоянием. Когда он дошел до конца функции и больше нет
yield, он исчерпан и последующие попытки получить из него элементы дадутStopIteration - Вызов
f()сyieldне выполняет тело функции, потому что наличиеyieldпревращает функцию в генераторную функцию и Python меняет семантику ее вызова: тело выполняется после наличияnext(например g = f(), next(g)) - Next запускает выполнение до следующего yield
- Pydantic схемы и SQLAlchemy модель - разные вещи и необходимо понимать, что Pydantic описывает какие данные API принимает и отдает клиенту, а SQLALchemy описывает как данные хранятся в базе. Почему не использовать одну модель - потому что API и БД имеют разные интересы: допустим в базе у нас класс Product (id, name, qty, created_at, updated_at, internal_cost, supplier_id). Клиенту необязательно отдавать internal_cost, supplier_id )
from_attributes=Trueнужен для создания Pydantic модели из ORM-объекта. Представим, что SQLAlchemy вернул не словарь, а объект Python:product = Product( id=42, name='iPhone' qty=10, )У него данные лежат как атрибуты (product.id, product.name, product.qty) Pydantic ожидает что-то вроде словаря:
{ "id": 42, "name": "iPhone", "qty": 10, }И вот тут нужен
from_atributes=True:from pydantic import BaseModel, ConfigDict class ProductResponse(BaseModel): id: int name: str qty: int model_config = ConfigDict(from_attributes=True)product = Product( id=42, name='iPhone', qty=10, ) response = ProductResponse.model_validate(product)Pydantic посмотрит не на словарь, а на атрибуты объекта:
Product ORM object │ ├── .id → 42 ├── .name → "iPhone" └── .qty → 10 ↓ ProductResponseSessionуправляет работой с БД, аcommit()фиксирует транзакциюПри ошибке до
commit()изменения не зафиксированы и транзакцию можно откатить черезrollback(). Еслиcommit()уже выполнен, обычнымrollback()эту транзакцию назад не вернуть:try: for product in products: db.add(Product(**product.model_dump())) db.commit() except Exception: db.rollback() raiseДля PostgreSQL меняется не только URL подключения: необходим также драйвер для работы
psycopg. Сам URL будет выглядить примерно так:DATABASE_URL = "postgresql+psycopg://user:password@localhost:5432/mydb"Кроме того, появляются различия в типах данных, поведении БД, настройках пула соединений, миграциях и т.д. В целом тема довольная обширная, буду разбирать ее позже