Улучшаем свой код на Python

Kate

Administrator
Команда форума
Время идет, IT-индустрия развивается, количество набитых шишок множится. За счет переиспользования накопленных сообществом знаний код начинающего специалиста сегодня в среднем выглядит лучше кода его предшественников.

Уже почти никто не называет свои переменные непонятными буквами и имена функциям стараются давать начинающиеся с глагола, однако из раза в раз замечаю общие для многих ошибки зоны роста.

Например, до сих пор встречается такой зверь:

admins = []

for user in get_users():
if user.is_admin:
admins.append(user)
И раз подобное живет в сердцах людей, мне показалось, не будет лишним описать пару частых кейсов из накопленного опыта ревью. Возможно, кто-то наткнется на эту статью и станет немножечко сильнее.

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

1. Использовать comprehension​

Код выше можно написать короче, используя генератор списка.

admins = [user for user in get_users() if user.is_admin]
Более того, такое решение будет работать немного быстрее, так как comprehension-ы - оптимизированы для создания объектов. Не забываем также, что подобным образом можно создавать словари и множества.

Эта же конструкция признается сообществом как более pythonic way для фильтрации

# Вместо
active_users = list(filter(lambda user: user.is_active, get_users()))

# Лучше
active_users = [user for user in get_users() if user.is_active]

2. Вложенные циклы for в comprehension​

Предположим, у нас есть задача сформировать список комбинаций элементов двух коллекций.

numbers = ["1", "2", "3"]
letters = ["A", "B", "C"]

# Хотим ['A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3']
Даже знакомые с генераторами списков могу не знать о небольшом сахаре и написать вложенный цикл.

spots = []

for letter in letters:
for number in numbers:
spots.append(letter + number)

# Хотя и можно так
spots = [letter + number for letter in letters for number in numbers]
Удобная штука, эти компрехеншены) Но не серебряная пуля. Обещаем не использовать их при наличии сложной логики во время итераций (это снизит читаемость) и идем дальше.

3. Логические операторы вместо тернарного​

Логические операторы часто можно использовать для задания значения переменной. Многие знакомы с тернарным оператором и избегают лишних if-else, однако и от первого можно уйти в некоторых случаях.

# Есть переменная, которая может быть не заполнена
user: User | None

# Задаем админа тернарником
admin = user if user else get_user()

# Но можно и короче
admin = user or get_user()

# Цепочку можно продолжать. Вернется первое приводимое к True значение
admin = get_admin() or user or get_user()

4. Использование оптимизации проверки условий​

Если python явно узнаёт значение условия, он не производит дальнейших проверок.

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

Чтобы не получить "IndexError: list index out of range",
часто вижу, что начинающие разработчики сначала проверяют список на пустоту

approvers: list[User] = []

if approvers:
if approvers[-1].is_admin:
return True
return False
Однако мы можем и совместить условия, упростив наш код:

approvers = []

if approvers and approvers[-1].is_admin:
return True
return False
Здесь, если approvers пустой, то общее условие уже никак не может быть True. Поэтому дальнейшие проверки не производятся, и за границы списка мы не выходим.

Работает также и в обратную сторону для or

approvers = []

# Если первое условие уже `True`, то часть с `or` не имеет значения
approved_by_admin = True
if approved_by_admin == True or approvers[-1].is_admin:
return True
return False

5. Использование магии bool​

Как ни странно, в python True и 1 равны. Как и False c 0

>>> True == 1
True

>>> False == 0
True
Помимо багов, это может и здорово сыграть нам на руку. Снова пример.

Представим, что мы решили бросить программирование и ударились ростовщичество. Математическое прошлое подсказывает, что нужна какая-то система принятия решения о выдаче кредита, и мы выбираем самую передовую из известных нам.
А именно: мы собираемся анализировать три показателя у заемщика.

  • наличие дома
  • наличие машины
  • наличие работы
Если какие-либо два из них положительны, то клиент, скорее всего, кредит отдаст, поэтому мы дадим ему деньги. Приступаем!

has_a_house: bool
has_as_car: bool
has_a_job: bool

if has_a_house and has_a_car:
return True
if has_a_house and has_a_job:
return True
if has_a_car and has_a_job:
return True
Вроде работает, но смущает нездоровое количество if-ов... А если факторов будет не три, а 100, и для положительного ответа нужно будет набрать минимум 60 очков? Переписываем.

factors = [has_a_house, has_as_car, has_a_job]

total = 0
for factor in factors:
if factor:
total += 1
return total >= 2
Уже лучше, но если мы воспользуемся тем, что булево True равняется единице, то получим изящный однострочник

return sum(factors) >= 2
# здесь sum сложит все единицы. По сути, посчитает количество True.

6. Использование any и all​

Если бы в предыдущем примере мы захотели проверить условия на

  • хотя бы одно
  • все
Мы бы просто могли воспользоваться конструкциями any и all соответственно

any([True, False, False])
# True

any([False, False, False])
# False

all([True, True, True])
# True

all([True, True, False])
# False

7. Сниппет для проверки пути в JSON-е​

Когда мы работаем с JSON-ами внешних сервисов, элементы в них могут быть опциональны. Причем бывает так, что дерево пути может оборваться в любой момент.

В данном примере хотим проверить наличие прав у пользователя на создание других пользователей. Для таких случаев сталкивался с таким вот кодом:

user_dict = {
"user": {
"username": "PetyaNagibator228",
"permissions": {
"create_troubles": True,
"create_users": True
}
}
}

if user := user_dict.get("user"):
if permissions := user.get("permissions"):
if permissions.get("create_users"):
return True
return False
Громоздко. Еще моржовые операторы эти. Было бы здорово обращаться к элементу прям по пути

path = "user.permissions.create_users"

Такое можно реализовать при помощи однострочника

path = "user.permissions.create_users"
reduce(dict.get, path.split('.'), user_dict)
# True
Однако у него есть несколько недостатков.

  • reduce у новичков вызывает недопонимания
  • решение не гибкое, если мы хотим иметь больше возможностей по обработке элементов
Возможно, более адаптируемым под Ваши нужды окажется прямолинейное решение:

def find(path: str, dict_: dict):
keys = path.split(".")
data = dict_
for key in keys:
try:
data = data.get(key)
except AttributeError:
return None
return data

find("user.permissions.create_users", user_dict)
# True

find("user.permissions.no_key.create_users", user_dict)
# None

8. Генераторы для экономии памяти и кода​

Уверен, что в любом уважающем себя курсе рассказывают про генераторы и про то, зачем они нужны. Однако это не мешает многим начинающим ребятам обходить пагинацию следующим образом:

j = {
"pagination": {
"page": 0, # текущая страница
"limit": 3 # кол-во элементов на странице
},
"user_ids": [1, 2, 3]
}

users = []
limit = 3
page = 0
while True:
rsp = client.get_all_users(page=page, limit=limit)
if not rsp.get("user_ids"):
break
users.extend(rsp["user_ids"])
page += 1

return users
Здесь создается список пользователей и наполняется до тех пор, пока внешний сервис отдает данные. Как только пагинация будет исчерпана, возвращается созданный список.

Используя подобную конструкцию, мы должны понимать, что весь итоговый список пользователей будет храниться в памяти. Что как минимум не очень эффективно. К тому же, существуют расходы на постоянное расширение данного списка на каждой итерации.

Здесь как нельзя лучше находят свое применение те самые генераторы.

limit = 3
page = 0
while True:
rsp = client.get_all_users(page=page, limit=limit)
if not rsp.get("user_ids"):
break
yield rsp["user_ids"]
page += 1
Так мы возвращаем пользователей пачками, сэкономив ресурсы машины.

9. Ограничение пагинации​

Существует такая вещь, как ограничение глубины рекурсии. Она нужна для избегания бесконечной вложенности. К сожалению, работая с внешними API, я не один раз сталкивался с багами, приводящими к бесконечным обходам пагинации. В примере выше цикл так и будет крутиться, если по какой-то причине не выполнится условие break. Например, если API будет всегда возвращать данные, не зависимо от передаваемого page.

Для таких случаев не лишним было бы иметь некий предохранитель, которым часто пренебрегают.

max_pages = 5
for i in range(max_pages + 1):
rsp = client.get_all_users(page=page, limit=limit)
if not rsp.get("user_ids"):
break
if i >= max_pages:
raise RuntimeError("Too many pagination elements")
yield rsp["user_ids"]

10. А так же...​

Последним пунктом хотел бы привести похожую статью, обозревающую частые ошибочные решения разработчиков.

Итог​

Конечно же, данный список не является исчерпывающим. Он также ни в коем разе не является никаким «топом самых страшных вещей в мире программирования» и весьма субъективен. Даже наоборот, я старался собрать больше «мелочи», незаслуженно обделяемые вниманием начинающих разработчиков.

 
Сверху