Flake8 + PyCharm: в одном стиле

Flake8 — инструмент, позволяющий просканировать код проекта и обнаружить в нем стилистические ошибки и нарушения различных конвенций кода на Python. Flake8 умеет работать не только с PEP 8, но и с другими правилами, к тому же поддерживает кастомные плагины.

Я покажу, как настроить Flake8, чтобы он автоматически подсвечивал проблемы прямо в коде

Шаг 1. Установка Flake8

Flake8 – это по сути модуль Python, поэтому его можно установить в одну из виртуальных сред Python. Я обычно использую venv, который хранится в папке проекта, в него и установим. Это легко с помощью терминала прямо в PyCharm.

pip install --upgrade flake8
Терминал установки Flake8

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

pip freeze > requirements.txt

Шаг 2. Плагин File Watcher

У вас должен быть установлен плагин File Watcher, это официальный плагин, но он не всегда по умолчанию загружен – у меня вот не было его. Идем в настройки (на Маке – меню PyCharm – Preferences, в других системах посмотрите в меню File).

Preferences PyCharm на Mac

Там идем в Plugins – Marketplace – вбиваем в поиске File Watchers – Install – Restart IDE.

Как установить File watchers в PyCharm

Шаг 3. Настраиваем Watcher

Нам нужно, чтобы при редактировании файла IDE смотрела за изменениями и прогоняла по файлу Flake8, если что-то изменилось. Для этого надо создать File Watcher – смотрителя, который будет выполнять команду.

Там же в настройках идем в Tools – File Watches – жмем на плюсик.

Как найти File Watcher

Далее настраиваем смотрителя. Имя можно придумать любое. Остальные настройки:

  1. File type – Python
  2. Score – Current File, я выбрал только для текущего файла, чтобы расходовать меньше ресурсов и энергии. Вы можете выбрать на весь проект.
  3. Program – это путь до установленного Flake8 – я использую переменную, которая связана с путем к интерпретатору Python, файл flake8 лежит в ней же: $PyInterpreterDirectory$/flake8
  4. Arguments – аргументы для flake8 – путь к анализируемому файлу: $FileDir$/$FileName$
  5. Show console – Never – мы не будем никогда показывать консоль, а ошибки должны будут подчеркиваться прямо в коде!
  6. Output filters – это формат строки с описанием ошибки, которую выдает flake8, если находит стилистические проблемы. По этому формату IDE поймет, где именно возникла проблема (файл, строка, колонка, сообщение) – скопируйте это: $FILE_PATH$:$LINE$:$COLUMN$: $MESSAGE$
Иллюстрации настройки смотрителя

Это базовая настройка flake8. Конечно, вы можете настраивать инспекции индивидуально, но об этом расскажу как-нибудь потом.

Шаг 4. Отображаем стилистические проблемы в коде

Теперь нам нужно заставить PyCharm подчеркивать проблемы в нужных местах кода. Сделать это несложно, нужно включить соответствующую инспекцию.

В настройках ищем: Editor – Inspections – File Watchers – File Watcher Problems – должна стоять галочка, затем правее выбираем Severity: Error. Жмем ОК и все готово!

Иллюстрация включение инспекции

Поздравляю. Начните редактировать файл и увидите, если возникнут сообщения о стилистических проблемах:

Инспекция в действии! Код подчеркнут!

Git hook!

Создадим хук для системы контроля версий GIT. Вы же пользуетесь ей, я надеюсь. Так вот pre-commit-hook – это действие, выполняемое перед коммитом. Будем запускать flake8 перед каждым коммитом, что плохо оформленный код не попадал в репозиторий. Сделать это очень просто в пару консольных команд:

flake8 --install-hook git
git config --bool flake8.strict true

Если в коде нет проблем, то коммит будет зафиксирован. А если они есть – вам придется прежде их исправить.

🐉 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈

starmap – это не звездная карта!

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

>> list(map(lambda x: x + 1, [1, 2, 3, 4]))
[2, 3, 4, 5]

Что делать, если нужно применить функцию, которая принимает большее количество аргументов? Например, возведение в степень pow принимает основание и показатель:

>>> pow(2, 4)
16

Как и требуют, мы даем в map функцию с одним аргументом, но каждый элемент t – кортеж из двух элементов, мы распаковываем его в аргументы pow звездочкой:

>>> list(map(lambda t: pow(*t), [(2, 4), (3, 2), (5, 2)]))
[16, 9, 25]

Если вы не знали: pow(*t) то же самое, что и pow(t[0], t[1]), если в t два элемента.

К счастью, не обязательно делать этот хак с лямбдой, потому что в модуле itertools есть функция starmap, которая как раз звездочкой распаковывает каждый элемент исходного итератора в аргументы функции:

>>> from itertools import starmap
>>> list(starmap(pow, [(2, 4), (3, 2), (5, 2)]))
[16, 9, 25]
Схема работы map и starmap показывает как передаются аргументы

🐉 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈

Отрезок времени в Python – timedelta

В модуле datatime содержатся классы для работы с датой и временем. В частности часто используются datetime для хранения даты и времени некоторого события и timedelta для хранения интервала времени между каким-то двумя событиями.

Удобно работать с datetime и timedelta путем математических операций. 

📎 Примеры. Добавить к дате один день, год или отнять 2:20 (функция str тут для человекочитаемого формата):

>>> str(datetime.now() + timedelta(days=1))
'2019-10-06 15:51:09.089691'
>>> str(datetime.now() + timedelta(days=365))
'2020-10-04 15:52:04.618896'
>>> str(datetime.now() - timedelta(hours=2, minutes=20))
'2019-10-05 13:41:27.617589'

Разница во времени между событиями:

>>> a = datetime.now()
>>> b = datetime.now() + timedelta(minutes=5)
>>> b - a
datetime.timedelta(0, 317, 99915)
>>> str(b - a)
'0:05:17.099915'

timedelta внутренне хранит только секунды от начала дня и сами дни, хотя в конструкторе принимает дни, секунды, микросекунды, миллисекунды, минуты, часы и недели (можно выбрать любые их сочетания для задания интервала): 

datetime.timedelta(days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0)

>>> str(timedelta(days=1, hours=2, milliseconds=333))
'1 day, 2:00:00.333000'

Причем мы не обязаны нормализовывать аргументы: он сам поймет, что 200 минут – это 3 часа 20 минут:

>>> str(timedelta(minutes=200))
'3:20:00'

Достать часы и минуты (странно, что у объекта нет свойств hours и minutes):

def hours_minutes(td):
    return td.seconds // 3600, (td.seconds // 60) % 60

>>> hours_minutes(timedelta(0, 12345))
(3, 25)

Сколько всего секунд в интервале:

>>> timedelta(minutes=200, seconds=21, hours=25).total_seconds()
102021.0

Можно даже умножать timedelta на числа или поделить два timedelta или взять остаток. Допустим рабочая смена длится 7 часов 30 минут, сколько полных смен в 3-х сутках?

>>> a = timedelta(days=3)
>>> b = timedelta(hours=7, minutes=30)
>>> a // b
9
>>> str(a % b)
'4:30:00'

Ответ 9 полных смен и еще останется 4 часа 30 минут лишних.

Бонус. Формат даты по-нашенскому (ДД.ММ.ГГГГ):

>>> datetime.strftime(datetime.now(), '%d.%m.%Y')
'05.10.2019'

🐉 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈

Деление с остатком преподнесло сюрприз

Деление с остатком – часто используемая операция в программировании. Начиная от классических заданий для начинающих на вычисление минут и секунд:

total_seconds = 119
seconds = total_seconds % 60
minutes = total_seconds // 60
print(f'{minutes}:{seconds}')  # 1:59

Заканчивая тем, что на остатках построена львиная доля криптографии. Нахождения остатка часто называют modulo (или коротко mod). 

При делении a на b неполное частное q и остаток r связаны формулой:

a = b · q + r, где b ≠ 0

В Python 3 частное и остаток вычисляются операторами:

q = a // b
r = a % b

Именно двойной слэш, одинарный слэш – деление без остатка (до конца). Иногда двойной слэш называют целочисленным делением, что не очень справедливо, потому что мы можем без проблем делить числа с запятой. Если оба числа целые (int), то частное будет тоже целым числом (int), иначе float. Посмотрите примеры:

10 / 3 == 3.3333333333333335
10 // 3 == 3
10.0 / 3.0 == 3.3333333333333335
10.0 // 3.0 == 3.0 
10.0 % 3.0 == 1.0
10 % 3 == 1

2.4 // 0.4 == 5.0
2.4 / 0.4 == 5.999999999999999
2.4 % 0.4 == 0.3999999999999998

Последние три примера немного обескураживают из-за особенностей вычислений с плавающей точкой на компьютере, но формула a = b · q + r всегда остается справедлива.

Поговорим об отрицательных числах. Математически остаток не должен быть меньше нуля и больше или равен модулю делителя b: 0 ≤ r < |b|. Однако, Intel в своих процессорах случайно либо намеренно ввела отрицательные остатки в реализации ассемблерных команд деления. Компиляторы языков C и С++, являясь платформо-зависимыми, обычно полагаются на процессорное поведение. Пример на С++. И вообще посмотрите на эту огромную таблицу, каждый язык программирования пляшет, как хочет. Не будем спорить, кто из них прав. Просто узнаем, как у нас в Python:

a, b = [10, -10], [3, -3]
for x in a:
  for y in b:
    print(f'{x} // {y} = {x // y}')
    print(f'{x} % {y} = {x % y}')
    print()

10 // 3 = 3
10 % 3 = 1

10 // -3 = -4
10 % -3 = -2

-10 // 3 = -4
-10 % 3 = 2

-10 // -3 = 3
-10 % -3 = -1

Формула выполняется всегда, но результаты отличаются для С++ и Python, где при делении на положительное число – остаток всегда положителен, а на отрицательное число – отрицателен. Если бы мы сами реализовали взятие остатка, то получилось бы так:

def mod_python(a, b):
  return int(a - math.floor(a / b) * b)

# на С++ работает так:
def mod_cpp(a, b):
  return int(a - math.trunc(a / b) * b)

Где floor – ближайшее целое число не превышающее аргумент: floor(-3.3) = -4, а trunc – функция отбрасывания целой части: trunc(-3.3) = -3. Разница проявляется между ними только для отрицательных чисел. Отсюда и разные остатки и частные – все зависит от того, с какой стороны числовой оси мы приближаемся к частному.

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

🐉 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈

Умножение списка на число

Студент Макс узнал, что в Python умножать можно не только числа, но и другие объекты, например, строку на число:

>>> "Max" * 3
'MaxMaxMax'

«Вау!» — подумал Макс — «А что если умножить список на число?»:

>>> [42, 26] * 3
[42, 26, 42, 26, 42, 26]

Значит можно создать двумерный массив очень кратко и элегантно?

>>> [[]] * 3
[[], [], []]

Заполнить его:

arr = [[]] * 3
arr[0].append(10)
arr[1].append(20)
arr[2].append(30)

Макс ожидал получить:

[[10], [20], [30]]

А вышло:

[[10, 20, 30], [10, 20, 30], [10, 20, 30]]

😯 Как же так?! Дело в том, что умножение списка на число не копирует сам объект, а лишь ссылку на него. Все три элемента arr ссылаются на один и тот же список. Легко проверить, сравнив адреса объектов:

>>> arr[0] is arr[1]
True
>>> id(arr[0]), id(arr[1])
(4400840776, 4400840776)
Диаграмма: все элементы arr указывают на один и тот же список.

Аналогично в случае классов:

class Dummy: ...
arr = [Dummy()] * 2
arr[0].x = 10
arr[1].x = 20
print(arr[0].x, arr[0] is arr[1])  # 20 True

А вот с числами, строками и кортежами умножение списка будет работать как ожидал Макс, потому что это неизменяемые типы. Вот такая тонкость, которую нужно знать. Максу следовало бы написать так:

arr = [[] for _ in range(3)]  
arr[0].append(10)
arr[1].append(20)
arr[2].append(30)
>>> arr
[[10], [20], [30]]

Менее кратко, но зато работает без сюрпризов: каждую итерацию создается новый пустой список.

🐉 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈