Prosto Python | вопросы с собесов
363 subscribers
184 photos
1 video
2 files
557 links
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
Download Telegram
⚖️ This vs That: is vs ==

Путают постоянно. А разница — фундаментальная, и на ней ловят на собесах.

Что делает ==
Сравнивает значения: «равны ли эти объекты по содержимому». Вызывает метод __eq__.

a = [1, 2, 3]
b = [1, 2, 3]
a == b # True — содержимое одинаковое


Что делает
is
Сравнивает идентичность: «это один и тот же объект в памяти». Сравнивает id(), никаких методов не зовёт.

a is b      # False — два разных списка, просто с одинаковым содержимым
a ──► [1, 2, 3] id = 0xAAA
b ──► [1, 2, 3] id = 0xBBB

a == b → сравнивает содержимое → True
a is b → сравнивает адреса → False


Главное отличие в одной фразе


==
спрашивает «равны ли они?»,


is

— «это вообще один и тот же объект?».


🐍Вопросы с собесов -> ProstoPython
🔥4
📈 From O(n·m) to O(n+m)

Задача
Даны два отсортированных массива. Слей их в один отсортированный.
Пример: [1, 3, 5] и [2, 4, 6][1, 2, 3, 4, 5, 6].

Наивное решение
«Соединю и отсортирую — делов-то»

def merge(a, b):
return sorted(a + b)


Работает, коротко. Но мы выбросили главное — то, что массивы уже отсортированы. sorted об этом не знает и сортирует с нуля: O((n+m)·log(n+m)).

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

Оптимизированное решение
Два указателя. Идём по обоим массивам одновременно, на каждом шаге берём меньший из текущих элементов.

def merge(a, b):
i = j = 0
result = []
while i < len(a) and j < len(b):
if a[i] <= b[j]:
result.append(a[i])
i += 1
else:
result.append(b[j])
j += 1
# один из массивов кончился — добиваем хвост второго
result.extend(a[i:])
result.extend(b[j:])
return result


Каждый элемент трогаем ровно один раз → O(n+m) время.

Объяснение
Трассировка a = [1, 3, 5], b = [2, 4, 6]:

a: [1, 3, 5]     b: [2, 4, 6]
i j

1 ≤ 2 → берём 1, i→ result: [1]
3 > 2 → берём 2, j→ result: [1, 2]
3 ≤ 4 → берём 3, i→ result: [1, 2, 3]
5 > 4 → берём 4, j→ result: [1, 2, 3, 4]
5 ≤ 6 → берём 5, i→ result: [1, 2, 3, 4, 5]
a кончился → добиваем хвост b: [6]
result: [1, 2, 3, 4, 5, 6]


Указатели только движутся вперёд и никогда не откатываются — отсюда линейность.

🐍Вопросы с собесов -> ProstoPython
👍3
Rookie Mistakes

def add_item(item, basket=[]):
basket.append(item)
return basket


Кажется логичным: не передал корзину — получи пустую. Но запусти дважды:

add_item("apple")    # ['apple']
add_item("banana") # ['apple', 'banana'] ← откуда яблоко?!


Банан попал в ту же корзину, что и яблоко. Хотя мы её вроде не передавали.

Почему это ошибка
Значение по умолчанию вычисляется один раз — в момент определения функции, а не при каждом вызове.

def add_item(...basket=[]):   ← здесь создаётся ОДИН список
и привязывается к функции навсегда


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

вызов 1: basket → [тот самый список] → append("apple")  → ['apple']
вызов 2: basket → [тот же список!] → append("banana") → ['apple', 'banana']


Список живёт между вызовами, как глобальная переменная, о которой ты не просил.
Кусаются именно мутабельные дефолты: [], {}, set(). С неизменяемыми (None, числа, строки, кортежи) проблемы нет — их и так нельзя поменять на месте.

Исправленный вариант
Дефолтом ставь None, а реальный объект создавай внутри — он будет свежим на каждый вызов:

def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
add_item("apple") # ['apple']
add_item("banana") # ['banana'] ← теперь каждый раз новая корзина


🐍Вопросы с собесов -> ProstoPython
🏆4
🧰 Code Cleanup

Плохой код
def get_discount(user):
if user is not None:
if user.is_active:
if user.subscription is not None:
if user.subscription.is_premium:
return 0.2
else:
return 0.1
else:
return 0.0
else:
return 0.0
return 0.0


Логика простая, а читать невозможно: четыре уровня вложенности, «лесенка», и где какой else относится — глаза ломаются. Главное действие утонуло в самой глубине.

Чистый вариант

def get_discount(user):
if user is None:
return 0.0
if not user.is_active:
return 0.0
if user.subscription is None:
return 0.0

return 0.2 if user.subscription.is_premium else 0.1


Те же условия — но «плохие» случаи отсекаем сразу и выходим. Что осталось до конца функции — это счастливый путь.

Объяснение
Приём называется guard clauses (защитные проверки): вместо того чтобы заворачивать основную логику во вложенные if, ты в начале функции отбрасываешь всё, что мешает, через ранний return.

Что меняется:
было:  условие → углубляемся → условие → углубляемся → …
(читать надо, держа в голове весь стек вложенности)

стало: не то? → выход.
не то? → выход.
дошли сюда — значит всё ок, делаем дело.


🐍Вопросы с собесов -> ProstoPython
👍3🔥1
🧠 Что выведет код

grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)


Варианты:

A) [[1, 0, 0], [0, 0, 0], [0, 0, 0]]
B) [[1, 0, 0], [1, 0, 0], [1, 0, 0]]
C) [[1, 1, 1], [0, 0, 0], [0, 0, 0]]
D) Ошибка

Правильный ответ: B
Меняли одну ячейку — «поплыла» вся колонка. На этом теряют часы отладки.

Разбор
Тут две разные * 3, и они делают не одно и то же.

[0] * 3        →  [0, 0, 0]   три НОВЫХ нуля (числа неизменяемы, копий не надо)
[...] * 3 → три ссылки на ОДИН И ТОТ ЖЕ список


* 3
для внешнего списка не копирует внутренний. Он трижды кладёт ссылку на один объект:

grid ──► [ • , • , • ]
│ │ │
└───┴───┘

[0, 0, 0] ← одна строка на всех


Поэтому grid[0], grid[1], grid[2] — это один и тот же список. Меняешь через любой — видишь во всех.

🐍Вопросы с собесов -> ProstoPython
🔥3
🧠 Interview Thinking

Задача
Дан массив. Передвинь все нули в конец, сохранив порядок остальных элементов. Меняй на месте, без нового массива.
Пример: [0, 1, 0, 3, 12][1, 3, 12, 0, 0].

Как думает junior
«Соберу ненулевые в новый список, потом добью нулями.»

def move_zeroes(nums):
result = [x for x in nums if x != 0]
result += [0] * (len(nums) - len(result))
return result

Логика верная. Но условие сказало на месте — а мы завели новый массив, O(n) доп. памяти. И ничего не меняем в исходном (вернули новый).
На собесе тут же спросят: «А без выделения второго массива?»

Как думает сильный кандидат
Инсайт: не «двигать нули», а подтягивать ненулевые вперёд. Нули окажутся в конце сами.

Два указателя, оба в одном массиве:
insert — куда класть следующий ненулевой элемент;
i — бежит по массиву и ищет ненулевые.

def move_zeroes(nums):
insert = 0
for i in range(len(nums)):
if nums[i] != 0:
nums[insert], nums[i] = nums[i], nums[insert]
insert += 1


O(n)
время, O(1) память. Меняем исходный массив.

🐍Вопросы с собесов -> ProstoPython
👍4
⏱️ Big O Breakdown

def process_queue(tasks):
while tasks:
task = tasks.pop(0) # берём из начала
handle(task)


Обрабатываем задачи по очереди — берём из начала списка, пока не опустеет. n — число задач.

Какая общая сложность по времени?
A) O(n)
B) O(n log n)
C) O(n²)
D) O(1)

Правильный ответ: C — O(n²)
Цикл выглядит линейным, но pop(0) всё портит.

Разбор
list.pop(0) берёт первый элемент. Но список в Python — это массив: элементы лежат подряд в памяти, индексы привязаны к позиции.
Убрал нулевой элемент → дырка в начале → все остальные надо сдвинуть на одну ячейку влево:

[A, B, C, D]
↑ pop(0)

убираем A, сдвигаем хвост:
[B, C, D]
└─ B, C, D переехали ← это O(n)


🐍Вопросы с собесов -> ProstoPython
👍4
⚖️ This vs That: yield vs return
Оба «отдают» значение из функции. Но yield превращает функцию в совсем другого зверя.

Что делает return
Возвращает значение и завершает функцию. Всё, она отработала, состояние стёрто.

def get_numbers():
return [1, 2, 3] # посчитали ВЕСЬ список сразу, отдали, вышли


Что делает
yield
Отдаёт значение и ставит функцию на паузу, запоминая, где остановилась. При следующем запросе — продолжает с того же места.

def get_numbers():
yield 1 # отдал 1, замер
yield 2 # продолжил, отдал 2, замер
yield 3


Функция с yield — это генератор: значения выдаются по одному, лениво, по требованию.

return:  [█████████]  всё посчитано и лежит в памяти целиком
yield: █ → █ → █ по одному, считается ровно когда нужно


Главное отличие в одной фразе

return
отдаёт всё сразу и забывает функцию.

yield
отдаёт по одному и помнит, где остановился.



🐍Вопросы с собесов -> ProstoPython
🔥4
Rookie Mistakes

Сравнение float через ==

total = 0.1 + 0.2

if total == 0.3:
print("оплата прошла")
else:
print("ошибка суммы")


Логично же: 0.1 + 0.2 это 0.3. Запускаем:

ошибка суммы


Python считает, что 0.1 + 0.2 != 0.3. Проверим в лоб:

>>> 0.1 + 0.2
0.30000000000000004


Почему это ошибка

Компьютер хранит дробные числа в двоичной системе. А 0.1, 0.2, 0.3 в двоичной — это бесконечные дроби (как 1/3 = 0.333... в десятичной). Их приходится обрезать под размер float.

0.1  →  хранится как ≈ 0.1000000000000000055...
0.2 → хранится как ≈ 0.2000000000000000111...
сумма → ≈ 0.3000000000000000444...

а 0.3 само по себе → ≈ 0.2999999999999999888...

0.30000...04 ≠ 0.29999...88 → False


Это не баг Python — так устроена арифметика с плавающей точкой везде (стандарт IEEE 754). В Java, C, JS — ровно то же самое.

🐍Вопросы с собесов -> ProstoPython
👍4
📈 From O(n²) to O(n)

Задача
Дана строка. Найди длину самой длинной подстроки, в которой нет повторяющихся символов.
Пример: "abcabcbb"3 (это "abc").

Наивное решение
«Проверю все подстроки: для каждого начала тяну конец, пока символы не повторятся.»

def longest_unique(s):
best = 0
for i in range(len(s)):
seen = set()
for j in range(i, len(s)):
if s[j] in seen:
break
seen.add(s[j])
best = max(best, j - i + 1)
return best


Корректно. Но для каждого старта i мы заново бежим вправо — O(n²).

Проблема
Когда натыкаемся на повтор, мы выбрасываем всю работу и начинаем со следующего символа с нуля. А ведь часть уже проверенного окна всё ещё валидна — её незачем пересчитывать.

Оптимизированное решение
Скользящее окно (sliding window). Держим окно [left..right] без повторов. Двигаем right вперёд; если символ уже в окне — подтягиваем left, выкидывая «хвост» до дубликата.

def longest_unique(s):
seen = {} # символ → его последний индекс
left = 0
best = 0
for right, ch in enumerate(s):
if ch in seen and seen[ch] >= left:
left = seen[ch] + 1 # сдвигаем левую границу за дубликат
seen[ch] = right
best = max(best, right - left + 1)
return best


O(n)
время: каждый символ обрабатывается один раз. left и right только движутся вперёд.

🐍Вопросы с собесов -> ProstoPython
🔥3
🧠 Что выведет код

def f():
print(x)
x = 10

x = 5
f()


Варианты:

A) 5
B) 10
C) None
D) UnboundLocalError

Правильный ответ: D)

Снаружи x = 5 существует. А функция всё равно падает. Почему?

Разбор

Python определяет, локальная переменная или глобальная, не во время выполнения, а заранее — на этапе компиляции функции.
Правило: если внутри функции переменной где-то присваивают значение — она считается локальной на всю функцию целиком. С первой строки до последней.

def f():
print(x) # ← пытаемся прочитать x
x = 10 # ← из-за этой строки x ЛОКАЛЬНА во всей f


Из-за x = 10 ниже Python ещё при компиляции решает: «x здесь локальная». Глобальная x = 5 для функции больше не видна — её заслонила локальная.
А на строке print(x) локальной x ещё не присвоили значение:

f() вызвана

x объявлена локальной (из-за присваивания ниже)

print(x) → читаем локальную x, которой ещё нет → 💥 UnboundLocalError


Ключевой момент: дело не в порядке строк сверху вниз. Даже если print(x) идёт первым — наличие присваивания где угодно в функции делает переменную локальной везде.

🐍Вопросы с собесов -> ProstoPython
👍4
🧰 Code Cleanup

Плохой код
def get_handler(command):
if command == "start":
return start_game()
elif command == "pause":
return pause_game()
elif command == "resume":
return resume_game()
elif command == "stop":
return stop_game()
else:
return unknown_command()


Работает. Но это «лестница»: каждая новая команда — ещё два elif. И вся структура — однотипная: сравнили строку → вызвали функцию.

Чистый вариант
HANDLERS = {
"start": start_game,
"pause": pause_game,
"resume": resume_game,
"stop": stop_game,
}

def get_handler(command):
handler = HANDLERS.get(command, unknown_command)
return handler()


Соответствие «команда → функция» стало данными, а не кодом. Добавить команду — одна строка в словарь, а не новая ветка.

Объяснение
Длинный if/elif, который сравнивает одну и ту же переменную с разными значениями — это замаскированная таблица соответствий. А таблицу естественнее хранить в словаре.

🐍Вопросы с собесов -> ProstoPython
🔥4
🧠 Interview Thinking

Задача
Даны две строки. Определи, являются ли они анаграммами — то есть состоят из одних и тех же символов в одинаковом количестве.
Пример: "listen" и "silent"True; "rat" и "car"False.

Как думает junior
«Анаграммы — это одинаковый набор букв. Отсортирую обе строки и сравню.»

def is_anagram(s, t):
return sorted(s) == sorted(t)


Коротко и работает. Это абсолютно валидный ответ — не стыдись его. Но сортировка — это O(n log n), и на собесе спросят: «А можешь за линию?»

Как думает сильный кандидат
Инсайт: мне не нужен порядок букв — нужно только сколько раз каждая встречается. А подсчёт — это O(n), без всякой сортировки.
Считаю частоты в обеих строках и сравниваю:

from collections import Counter

def is_anagram(s, t):
return Counter(s) == Counter(t)


O(n)
время. Counter строит словарь {буква: количество}, а == сравнивает их целиком.

Хочешь показать, что понимаешь, что под капотом, — можно и руками одним словарём:
def is_anagram(s, t):
if len(s) != len(t): # разной длины — точно не анаграммы
return False
counts = {}
for ch in s:
counts[ch] = counts.get(ch, 0) + 1
for ch in t:
if ch not in counts:
return False
counts[ch] -= 1
if counts[ch] == 0:
del counts[ch]
return not counts # пусто → всё сошлось


🐍Вопросы с собесов -> ProstoPython
👍4
🎭 Red Flag

Плохой пример
def process(value):
if type(value) == list:
return [v * 2 for v in value]
if type(value) == dict:
return {k: v * 2 for k, v in value.items()}
raise TypeError("неподдерживаемый тип")


Работает на обычных списках и словарях. Но проверка типа через type(x) == — хрупкая, и однажды она тихо тебя подведёт.

Что не так
type(x) == list проверяет, что тип — ровно list, и ничего больше. Наследников он не признаёт.

from collections import OrderedDict

d = OrderedDict(a=1, b=2)
type(d) == dict # False ! — а ведь это словарь по сути
isinstance(d, dict) # True


OrderedDict
, defaultdict, Counter — все наследуют dict и ведут себя как словари. Но type(x) == dict их отвергнет, и твоя функция упадёт на совершенно валидных данных.
Это нарушает подстановку Лисков: подкласс должен работать везде, где работает базовый. type() == ломает этот принцип на ровном месте.

Как надо
isinstance проверяет «является ли x этим типом или его наследником» — то есть то, что обычно и нужно:

def process(value):
if isinstance(value, list):
return [v * 2 for v in value]
if isinstance(value, dict): # ловит и OrderedDict, и defaultdict
return {k: v * 2 for k, v in value.items()}
raise TypeError("неподдерживаемый тип")


🐍Вопросы с собесов -> ProstoPython
🏆4
⏱️ Big O Breakdown

def join_words(words):
result = ""
for word in words:
result += word + " "
return result


Склеиваем слова в одну строку. n — число слов (для простоты считаем их сопоставимой длины).

Какая сложность по времени?
A) O(n)
B) O(n log n)
C) O(n²)
D) O(1)

Правильный ответ: C

Один цикл, простое += — а под капотом квадрат.

Разбор
Главная ловушка: строки в Python неизменяемы. result += word не дописывает в существующую строку — он создаёт новую, копируя в неё всё старое содержимое плюс новый кусок.

result += word  →  на самом деле:  result = result + word
(новая строка, копия всего)


А копирование строки длины k стоит O(k). На каждой итерации result всё длиннее:

итерация 1:  копируем строку длины ~1
итерация 2: копируем длины ~2
итерация 3: копируем длины ~3
...
итерация n: копируем длины ~n

всего: 1 + 2 + 3 + ... + n = O(n²)


Строка переписывается заново снова и снова. Каждый символ из начала копируется десятки тысяч раз.

🐍Вопросы с собесов -> ProstoPython
❤‍🔥4
💾 Memory Footprint: чтение файла целиком vs построчно

def count_errors(path):
lines = open(path).readlines()
return sum(1 for line in lines if "ERROR" in line)


Считаем строки со словом ERROR в лог-файле. n — число строк в файле.

Какая сложность по ПАМЯТИ?
A) O(1)
B) O(log n)
C) O(n)
D) Зависит от числа ошибок

Правильный ответ: C

По времени тут честный O(n).

А вот по памяти — ловушка, которую почти не замечают.

Разбор
readlines() читает весь файл сразу и возвращает список всех строк:

readlines()  →  ["строка1\n", "строка2\n", ..., "строкаN\n"]
весь файл целиком лежит в памяти


Это список из n строк → O(n) памяти. На лог-файле в 10 ГБ программа просто упадёт с MemoryError, хотя нам нужно-то всего одно число — счётчик.
Ключевое наблюдение: нам не нужны все строки одновременно. Мы смотрим на каждую по разу и забываем. Зачем держать их все в памяти?

Как починить — O(1)
Файловый объект сам по себе итерируемый: можно идти по строкам, не загружая файл целиком. Каждая строка читается, обрабатывается и выбрасывается.

def count_errors(path):
with open(path) as f:
return sum(1 for line in f if "ERROR" in line)
readlines(): [строка1][строка2]...[строкаN] ← всё в памяти, O(n)
итерация по f: строка → обработали → забыли ← одна за раз, O(1)


В памяти в любой момент — одна строка.

🐍Вопросы с собесов -> ProstoPython
👍4
💾 Memory Footprint: генератор vs списковое включение

nums = range(1_000_000)

a = [x * x for x in nums]
b = (x * x for x in nums)

print(sum(a))
print(sum(b))


Обе строки «создают квадраты». sum от обеих даст одинаковый результат. Но память они едят по-разному.

Сколько памяти держит b (в круглых скобках)?
A) O(n), как и a
B) O(1)
C) O(log n)
D) Ошибка — генератор нельзя суммировать


Правильный ответ: B

Один символ разницы — квадратные скобки против круглых — меняет память с мегабайтов на байты.

Разбор
a = [x * x for x in nums]    # список: считает и хранит ВСЕ значения сразу
b = (x * x for x in nums) # генератор: не считает ничего, пока не спросят


a
списковое включение. Оно сразу вычисляет миллион квадратов и складывает их в список. Это O(n) памяти — все элементы лежат одновременно.
bгенераторное выражение. Оно не вычисляет ничего наперёд. Это рецепт: «при запросе выдай следующий квадрат». Значения рождаются по одному во время итерации и тут же выбрасываются.

a:  [0, 1, 4, 9, ..., n²]      ← весь миллион в памяти, O(n)
b: → 0 → 1 → 4 → ... ← по одному, O(1)
(в памяти всегда один элемент)


sum(b)
идёт по генератору: берёт квадрат, прибавляет к сумме, забывает, берёт следующий. В памяти в каждый момент — одно число.

🐍Вопросы с собесов -> ProstoPython
🔥4
🧠 Interview Thinking

Задача
Дан массив чисел. Определи, есть ли в нём хотя бы один повторяющийся элемент.
Пример: [1, 2, 3, 1]True; [1, 2, 3, 4]False.

Как думает junior
«Для каждого элемента проверю, встречается ли он дальше. Сравню все пары.»

def contains_duplicate(nums):
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] == nums[j]:
return True
return False


Корректно. Но это O(n²) — два вложенных цикла. На большом массиве собес ляжет.

Как думает сильный кандидат
Инсайт: вопрос «видел ли я это число раньше?» — это вопрос про принадлежность множеству. А in по set — это O(1).
Иду по массиву один раз, складываю увиденное в set. Если новый элемент уже там — нашли дубликат.

def contains_duplicate(nums):
seen = set()
for num in nums:
if num in seen:
return True
seen.add(num)
return False


O(n)
время, O(n) память. И ранний выход — на первом же повторе.

🐍Вопросы с собесов -> ProstoPython
👍4
🧠 Что выведет код

a = [1, 2, 3]
b = a
a = a + [4]
print(b)

c = [1, 2, 3]
d = c
c += [4]
print(d)


Варианты:

A) [1, 2, 3] и [1, 2, 3]
B) [1, 2, 3, 4] и [1, 2, 3, 4]
C) [1, 2, 3] и [1, 2, 3, 4]
D) [1, 2, 3, 4] и [1, 2, 3]

Правильный ответ: C

Один и тот же b = a, почти одинаковые операции — а результат разный. Вот где зарыто.

Разбор

Разница между a = a + [4] и a += [4] — фундаментальная, хоть и выглядят синонимами.
Первый случай: a = a + [4]
a + [4] создаёт новый список. Затем имя a переставляется на него. А b как смотрело на старый список — так и смотрит.

до:   a ──► [1, 2, 3] ◄── b

a + [4] создаёт НОВЫЙ список [1,2,3,4]
a переставляется на него:

a ──► [1, 2, 3, 4]
b ──► [1, 2, 3] ← b на старом, не тронут


Второй случай:
c += [4]
Для списка += — это extend на месте. Он не создаёт новый объект, а мутирует существующий. А d указывает на тот же самый объект → видит изменение.

до:   c ──► [1, 2, 3] ◄── d

c += [4] меняет ТОТ ЖЕ список на месте:

c ──► [1, 2, 3, 4]
d ──────┘ ← d на том же объекте, видит [1,2,3,4]


Суть:

a = a + [4]   →  новый объект, переставляем имя   →  чужие ссылки целы
c += [4] → мутируем старый объект на месте → чужие ссылки видят изменение


🐍Вопросы с собесов -> ProstoPython
❤‍🔥4
⚖️ This vs That: staticmethod vs classmethod

Оба «методы, которым не нужен экземпляр». Путают постоянно — а разница в одном: что метод получает первым аргументом.

Что делает classmethod
Получает первым аргументом сам класс (cls). Через него имеет доступ к классу: другим методам, атрибутам, и — главное — может создавать экземпляры.

class User:
def __init__(self, name):
self.name = name

@classmethod
def from_string(cls, data):
name = data.strip().title()
return cls(name) # создаём экземпляр через cls
User.from_string(" аня ") # User(name="Аня")


Что делает
staticmethod
Не получает ничего автоматически — ни self, ни cls. Это просто функция, которая лежит внутри класса для группировки. О классе она ничего не знает.

class User:
@staticmethod
def is_valid_name(name):
return name.isalpha() and len(name) >= 2
User.is_valid_name("Аня") # True


Главное отличие в одной фразе

classmethod
знает про свой класс (

cls
) и может им пользоваться.

staticmethod
не знает ни про класс, ни про экземпляр — это просто функция в неймспейсе класса.


🐍Вопросы с собесов -> ProstoPython
❤‍🔥4
Rookie Mistakes

def check_status(code):
if code is 200:
return "OK"
return "ошибка"

print(check_status(200)) # "OK" — вроде работает
print(check_status(1000)) # ???


Со 200 всё хорошо. А теперь то же самое с большим числом:

x = 1000
y = 1000
print(x is y) # False ?!


Два раза 1000 — а is говорит «разные». Хотя == сказал бы True. Почему?

Почему это ошибка
is проверяет не равенство значений, а идентичность — «это один и тот же объект в памяти». А Python кэширует только маленькие целые числа: от −5 до 256. Они существуют в единственном экземпляре, поэтому is для них «случайно» работает:

256 is 256    → True    (256 кэширован, объект один)
257 is 257 → False (создаются два разных объекта)


То же со строками — короткие интернируются, длинные нет:

"hi" is "hi"              # часто True  (интернирована)
"hello world!" is "hello world!" # может быть False


Самое коварное: код проходит тесты на маленьких значениях и падает в проде на больших. Баг, который невозможно объяснить, глядя только на логику.

🐍Вопросы с собесов -> ProstoPython
👍3