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-разработчиком!