Как писать код, который не стыдно показать
Комментарий — признак плохого имени или избыточной логики
def calc(x, y):
# x - user, y - order
if x['premium'] and y['sum'] > 100:
return 0.2
return 0.1
Почему нужно объяснять?
def calculate_discount(user, order):
if user.is_premium and order.total > 100:
return 0.2
return 0.1
Никаких комментариев — всё ясно
Одна задача — одна функция
def process_data(data):
cleaned = []
for item in data:
if item is not None and item > 0:
cleaned.append(item * 1.1)
total = sum(cleaned)
avg = total / len(cleaned) if cleaned else 0
print(f"Total: {total}, Avg: {avg}")
return {"total": total, "avg": avg}
def clean_data(data):
return [x * 1.1 for x in data if x is not None and x > 0]
def calculate_stats(cleaned):
total = sum(cleaned)
avg = total / len(cleaned) if cleaned else 0
return {"total": total, "avg": avg}
def report(stats):
print(f"Total: {stats['total']}, Avg: {stats['avg']}")
🔹 Легко тестировать
🔹 Легко переиспользовать
🔹 Легко читать и понимать
Null — источник ошибок. Используйте объекты вместо него.
def get_user(id):
return db.find(id) # может быть None
user = get_user(123)
send_email(user.email) # AttributeError!
class User:
@staticmethod
def anonymous():
return User(name="Guest", email="no@email")
def get_user(id):
found = db.find(id)
return found or User.anonymous()
user = get_user(123)
send_email(user.email) # всегда безопасно
Ранний выход лучше, чем глубокая вложенность
if user:
if user.active:
if user.has_permission:
execute_action()
else:
log("No permission")
else:
log("Inactive")
else:
log("User not found")
if not user:
log("User not found")
return
if not user.active:
log("Inactive")
return
if not user.has_permission:
log("No permission")
return
execute_action()
Медленные тесты — это анти-паттерн
def test_train_model():
model = train_on_real_dataset() # 5 минут
assert model.accuracy > 0.8
⏱️ Никто не запустит регулярно
def test_linear_predict():
model = LinearModel(weight=2)
result = model.predict(3)
assert result == 6
⚡ Выполняется за миллисекунды
Функция либо меняет состояние, либо возвращает значение
def get_results():
self.cache.clear() # side effect!
return db.query(...)
Вызываешь "get" — а система очищается
def clear_cache():
self.cache.clear()
def get_results():
return db.query(...)
Поведение предсказуемо
Группируйте по смыслу, а не по файловому типу
models/
user.py
order.py
services/
user_service.py
order_service.py
utils/
validation.py # используется везде
user/
models.py
service.py
validators.py
order/
models.py
service.py
discount_calculator.py
Всё, что относится к юзеру — в одной папке
Один ввод → один вывод. Без side effects.
results = []
def normalize(data):
global results
results.append(sum(data)) # side effect
return [x / sum(data) for x in data]
def normalize(data):
total = sum(data)
return [x / total for x in data], total
Тестируема, параллелизуема, надёжна
Они делают поведение неочевидным
def save(model, force_update=False):
if force_update:
db.update(model)
else:
db.insert(model)
save(model, True) — что это значит?
def create(model):
db.insert(model)
def update(model):
db.update(model)
Ясно, просто, легко поддерживать
Простой код — лучший код
result = [f(x) for x in data if p(x)] or [default]
Придётся думать, чтобы понять.
filtered = []
for item in data:
if passes_check(item):
filtered.append(transform(item))
if not filtered:
filtered = [default_value]
Читается как рассказ
“Каждый отрезок кода должен быть настолько простым и очевидным, чтобы очевидных ошибок в нём не было.”
— Brian Kernighan
Улучшим код шаг за шагом
def proc_data(d, f):
r = []
for i in d:
if i != None:
if f == 'norm':
r.append(i * 0.9)
elif f == 'boost':
r.append(i * 1.2)
t = sum(r)
a = t / len(r) if r else 0
print(f"Total: {t}, Avg: {a}")
return {'total': t, 'avg': a}
data = [10, -5, None, 20, 0, 15]
result = proc_data(data, 'boost')
Что не так? Переменные без смысла, флаги, side effects...
def process_data(data, mode):
result = []
for item in data:
if item is not None:
if mode == 'norm':
result.append(item * 0.9)
elif mode == 'boost':
result.append(item * 1.2)
total = sum(result)
avg = total / len(result) if result else 0
print(f"Total: {total}, Avg: {avg}")
return {'total': total, 'avg': avg}
🔹 proc_data → process_data
🔹 d, f, r → понятные имена
def process_data(data, mode):
result = []
for item in data:
if item is None:
continue
if mode == 'norm':
result.append(item * 0.9)
elif mode == 'boost':
result.append(item * 1.2)
total = sum(result)
avg = total / len(result) if result else 0
print(f"Total: {total}, Avg: {avg}")
return {'total': total, 'avg': avg}
🔹 Убрали лишнюю вложенность через continue
def process_data(data, mode):
result = []
for item in data:
if item is None:
continue
if mode == 'norm':
result.append(item * 0.9)
elif mode == 'boost':
result.append(item * 1.2)
total = sum(result)
avg = total / len(result) if result else 0
return {'total': total, 'avg': avg}
# Отдельно — вывод
stats = process_data(data, 'boost')
print(f"Total: {stats['total']}, Avg: {stats['avg']}")
🔹 Функция больше не зависит от вывода
def normalize(value):
return value * 0.9
def boost(value):
return value * 1.2
TRANSFORMATIONS = {
'norm': normalize,
'boost': boost
}
def process_data(data, mode):
transform = TRANSFORMATIONS.get(mode)
if not transform:
raise ValueError("Unknown mode")
result = [transform(item) for item in data if item is not None]
total = sum(result)
avg = total / len(result) if result else 0
return {'total': total, 'avg': avg}
🔹 Нет if/elif
🔹 Легко добавить новую операцию
from typing import List, Dict
def clean_and_transform(data: List[float],
transform_func) -> List[float]:
return [transform_func(x) for x in data if x is not None]
def calculate_stats(values: List[float]) -> Dict[str, float]:
total = sum(values)
avg = total / len(values) if values else 0
return {'total': total, 'avg': avg}
# Использование
values = clean_and_transform(data, boost)
stats = calculate_stats(values)
🔹 Одна функция — одна задача
🔹 Типы, читаемость, тестируемость
Мы превратили неподдерживаемый код в чистый, модульный, расширяемый.