5 Хитростей Python, которые отличают Senior-разработчика от Junior
Каждый год, начиная с 2015 года, первого декабря стартует Advent of Code. Как описано на их веб-сайте, Advent of Code (далее AoC) – это
адвент-календарь небольших головоломок по программированию для различных наборов навыков и уровней квалификации, которые могут быть решены на любом языке программирования, который вам нравится. Люди используют их для подготовки к собеседованию, корпоративному обучению, университетским курсовым работам, практическим задачам, соревнованиям на скорость или для того, чтобы бросить вызов друг другу.
В этой статье мы рассмотрим пять подходов к решению распространённых задач кодинга Senior-способами, а не Junior. Каждая задача является производной от головоломки AoC, причём многие из них многократно повторяются на протяжении AoC и других задач кодинга и задач, с которыми вы можете столкнуться, например, на собеседованиях при приёме на работу.
Чтобы проиллюстрировать концепции, я не буду вдаваться вполные решение головоломок AoC, а, скорее, сосредоточусь только на небольшой части конкретной головоломки, в которой Сениоров легко отличить от Джуниоров.
https://t.me/python_job_interview – разбор практических задач на Python.
1. Эффективное считывание в файле со списковым включением и split
На первый день AoC требуется прочитать некоторое количество блоков чисел. Каждый блок разделён пустой строкой (таким образом, фактически ‘\n
’).
Входные данные и желаемый результат
# INPUT
10
20
30
50
60
70
# DESIRED OUTPUT
[[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()))
# append the last group because if line == "\n" will not be True for
# the 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
сопоставляет функцию (первый аргумент) с итерацией во втором аргументе. В этой конкретной ситуации он применяет int()
к каждому значению в списке, превращая каждый элемент в целое число. Для получения дополнительной информации о карте
нажмите здесь.
2. Использование Enum вместо if-elif-else
На второй день задача вращается вокруг игры в камень-ножницы-бумага. Любая выбранная форма (камень, бумага или ножницы) приводит к различному количеству очков: 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
Конечно, в этом примере if else не так уж и ужасен, но использование Enum
приводит к более короткому и удобочитаемому коду. Особенно, когда есть много вариантов, алгоритм if-elif-else будет становиться всё хуже и хуже, в то время как с Enum
остаётся относительно легко. Для получения дополнительной информации о Enum
читайте здесь.
3. Использование таблиц поиска вместо словарей
На третий день задача с буквами, которые имеют разные значения. Строчные буквы a-z имели значения от 1 до 26, а прописные буквы a-z от 27 до 52. Из-за множества различных возможных значений использование Enum
, подобного приведённому выше, привело бы к большому количеству строк кода. Более практичным подходом здесь является использование метода .index()
:
# INPUT
c
Z
a
...
# DESIRED OUPUT
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.
# INPUT
[D]
[N] [C]
[Z] [M] [P]
# DESIRED OUTPUT
[' 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. Использование атрибутов класса для хранения экземпляров класса
На одиннадцатый день ваш ждет задача, в которой обезьяны передают предметы друг другу. Для упрощения мы представим, что они просто передают бананы друг другу. Каждая обезьяна может быть представлена как экземпляр Python с 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
Самодокументируемые выражения (БОНУС)
Этот трюк применим практически каждый раз, когда вы пишете программу на Python. Вместо определения в f-строке того, что вы печатаете (например
, print(f"x = {x}")
, вы можете использовать print(f"{x = }”)
для печати значения со спецификацией того, что вы печатаете.
# INPUT
x = 10 * 2
y = 3 * 7
max(x,y)
# DESIRED OUTPUT
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) = }")
Заключение
Мы рассмотрели 5 приёмов Python, которые отличают Сениоров от Джуниоров. Конечно, только применение этих приёмов не сделает вас Сениором . Однако, проанализировав разницу в стиле и шаблонах между ними, вы можете узнать разницу в том, как оптытный разработчик подходит к проблемам кодинга по сравнению с Джуниором, и вы можете начать изучать эти подходы, чтобы в конечном итоге самому стать Senior-разработчиком!