Лучший опыт

5 приемов Python, которые отличают профессионалов от новичков.

Ежегодно с 2015 года 1 декабря стартует Advent of Code (AoC). Вот как описывается этот инструмент на сайте Advent of Code: Адвент-календарь, который ежедневно предлагает программистам небольшие задачи для проверки навыков и уровня мастерства. Задачи можно решать на любом языке программирования. Пользователи сервиса используют их для подготовки к собеседованию, профобучения, овладения университетским курсом, решения практических задач, соревнован
5 приемов Python, которые отличают профессионалов от новичков...

Ежегодно с 2015 года 1 декабря стартует Advent of Code (AoC). Вот как описывается этот инструмент на сайте Advent of Code:

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

В этой статье мы рассмотрим пять подходов senior- и junior-программистов к решению распространенных задач. Все задачи взяты из адвент-календаря. Многие из них повторяются многократно в AoC и в других задачниках по программированию и тестах, с которыми вы можете столкнуться, например, на собеседовании при приеме на работу.

Для понимания разницы между профессионалом и новичком не стоит углубляться в решение всех задач AoC. Остановимся лишь на небольшой их части, позволяющей проиллюстрировать принципиальные отличия senior- от junior-разработчиков.

1. Эффективный ввод файла с помощью списковых выражений (comprehensions) и разделений (splits)

Задача 1-го дня на AoC требовала ввести несколько блоков чисел. Каждый блок отделен пустой строкой (фактически '\n’).

Ввод и желаемый вывод:

# ВВОД
10
20
30

50
60
70

# ЖЕЛАЕМЫЙ ВЫВОД
[[10, 20, 30], [50, 60 70]]

Подход junior-разработчика: цикл с операторами if-else.

numbers = []
with open("file.txt") as f:
group = []
for line in f:
if line == "\n":
numbers.append(group)
group = []
else:
group.append(int(line.rstrip()))
# добавить last group, так как if line == "\n" не будет True для
# last group
numbers.append(group)

Подход senior-разработчика: основанный на использовании списковых выражений и .split().

with open("file.txt") as f:
nums = [list(map(int, (line.split()))) for line in f.read().rstrip().split("\n\n")]

С помощью списковых выражений можно вместить девять предыдущих строк в одну, без существенной потери понятности и удобочитаемости, и с выигрышем в производительности (списковые выражения быстрее, чем обычные циклы). Тем, кто еще не знаком с map, поясним: map сопоставляет функцию (первый аргумент) с итерируемым объектом во втором аргументе. В данной ситуации map применяет функцию int() к каждому значению в списке, превращая каждый элемент в целое число.

2. Enum вместо if-elif-else

Задача 2-го дня составлена по принципу игры “камень-ножницы-бумага”. За выбранную форму (камень, бумага или ножницы) начисляется определенное количество очков: 1 (X), 2 (Y) и 3 (Z) соответственно. Ниже приведены два подхода к решению этой задачи.

Ввод и желаемый вывод:

# ВВОД
X
Y
Z

# ЖЕЛАЕМЫЙ ВЫВОД
1
2
3

Подход junior-разработчика: if-elif-else.

def points_per_shape(shape: str) -> int:
if shape == 'X':
return 1
elif shape == 'Y':
return 2
elif shape == 'Z':
return 3
else:
raise ValueError('Invalid shape')

Подход senior-разработчика: Enum.

from enum import Enum

class ShapePoints(Enum):
X = 1
Y = 2
Z = 3

def points_per_shape(shape: str) -> int:
return ShapePoints[shape].value

Конечно, в данном примере подход junior-разработчика не так уж и плох, но Enum делает код более лаконичным и читаемым. Если же появляется больше вариантов, наивный подход if-elif-else будет становиться все неэффективнее, в то время как с Enum можно сохранить относительно легкий контроль над кодом. Подробнее о Enum читайте здесь.

3. Таблицы поиска вместо словарей

На 3-й день предложена задача с буквами, имеющими разные значения. Строчные буквы a-z имеют значения от 1 до 26, а прописные a-z  —  от 27 до 52. Из-за большого количества возможных значений использование Enum, как в примере выше, привело бы к большому количеству строк кода. Более практичным подходом здесь является таблица поиска:

# ВВОД
c
Z
a
...

# ЖЕЛАЕМЫЙ ВЫВОД
3
52
1
...

Подход junior-разработчика: создание глобального словаря.

letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
letter_dict = dict()
for value, letter in enumerate(letters, start=1):
letter_dict[letter] = value

def letter_value(ltr: str) -> int:
return letter_dict[ltr]

Подход senior-разработчика: использование строки в качестве таблицы поиска.

def letter_value(ltr: str) -> int
return 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.index(ltr) + 1

Применяя метод .index() для строки, получаем индекс. Следовательно, letters.index('c')+1 приведет к ожидаемому значению 3. Нет необходимости хранить значения в словаре, поскольку индекс и есть значение. Чтобы убрать +1, можно просто добавить пробельный символ в начало строки, чтобы индекс a начинался с 1. Однако это зависит от того, хотите ли вы для пробельного символа вернуть значение 0 или ошибку.

Как вы наверняка догадались, задачу “камень-ножницы-бумага” также можно решить с помощью таблицы поиска:

def points_per_shape(shape: str) -> int:
return 'XYZ'.index(shape) + 1

4. Продвинутые методы получения срезов

Задача 5-го дня  —  прочитать буквы из строк (см. вводные данные ниже). Каждая буква находится на четвертом индексе, начиная с индекса 1. Сейчас практически каждый Python-программист знаком с получением срезов строк и списков с помощью, например, list_[10:20]. Но многие не знают, что можно определить размер шага, используя, например, list_[10:20:2] для определения размера шага 2. В решении задачи 5-го дня (и во многих других ситуациях в программировании) это может сэкономить время, избавив от написания объемного и неоправданно сложного кода:

# ВВОД
[D]
[N] [C]
[Z] [M] [P]

# ЖЕЛАЕМЫЙ ВЫВОД
[' D ', 'NC', 'ZMP']

Подход junior-разработчика: двойной цикл for с range и индексами.

letters = []
with open('input.txt') as f:
for line in f:
row = ''
for index in range(1, len(line), 4):
row += line[index]
letters.append(row)

Подход senior-разработчика: использование продвинутых методов получения среза.

with open('input.txt') as f:
letters = [line[1::4] for line in f]

5. Атрибут класса для хранения экземпляров класса

В 11-й день описывается ситуация, в которой обезьяны (monkeys) передают друг другу какие-то объекты. Для упрощения представим, что они передают друг другу бананы. Каждая обезьяна (monkey) может быть представлена как экземпляр Python-класса class с id и количеством бананов в качестве атрибутов экземпляра.

Однако обезьян много, и они должны взаимодействовать друг с другом. Чтобы хранить всех обезьян и чтобы они могли взаимодействовать друг с другом, определим словарь со всеми экземплярами Monkey как атрибут класса Monkey. С помощью Monkey.monkeys[id] получаем доступ ко всем имеющимся обезьянам без использования класса Monkeys или внешнего словаря:

class Monkey:
monkeys: dict = dict()

def __init__(self, id: int):
self.id = id
self.bananas = 3
Monkey.monkeys[id] = self

def pass_banana(self, to_id: int):
Monkey.monkeys[to_id].bananas += 1
self.bananas -= 1

Monkey(1)
Monkey(2)
Monkey.monkeys[1].pass_banana(to_id=2)

print(Monkey.monkeys[1].bananas)
2

print(Monkey.monkeys[2].bananas)
4

6. Самодокументируемые выражения (бонус)

Этот прием можно применять практически каждый раз, когда вы пишете программу на Python вместо того, чтобы определять в f-строке то, что вы выводите (например, print(f"x = {x}"), используйте print(f"{x = }”)) для вывода значения с уточнением того, что выводится.

# ВВОД
x = 10 * 2
y = 3 * 7

max(x,y)

# ЖЕЛАЕМЫЙ ВЫВОД
x = 20
y = 21

max(x,y) = 21

Подход junior-разработчика:

print(f"x = {x}")
print(f"y = {y}")

print(f"max(x,y) = {max(x,y)}")

Подход senior-разработчика:

print(f"{x = }")
print(f"{y = }")

print(f"{max(x,y) = }")

Заключение

Мы рассмотрели 6 приемов Python, которые отличают senior- от junior-разработчиков. Конечно, применение только этих хитростей вряд ли поможет сразу же пробиться из junior- в senior-разработчики. Анализируйте стили и паттерны, используемые программистами разного уровня. Старайтесь понять и освоить продвинутые подходы к решению задач. Следуя этим советам, вы постепенно овладеете лучшими практиками и в конце концов сами станете senior-разработчиком!