Чистое зло Python

от автора

Темные силы не дремлют. Они пробираются в дивное королевство Python и используют черную магию, чтобы осквернить главную реликвию — чистый код. Однако опасны не только злые чары.

Сегодня я расскажу о страшных чудовищах, которые, возможно, уже обжились в вашем коде и готовы устанавливать свои правила. Здесь нужен герой, который защитит безмятежный мир от злобных тварей. И именно вы станете тем, кто сразится с ними!

Всем героям, однако, нужно магическое снаряжение, которое верой и правдой послужит им в грандиозных битвах. К счастью, с нами будет линтер wemake-python-styleguide. Он станет тем самым острым оружием и надежным соратником.

Все готово, выступаем в поход!

Пожиратели пространства

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

x = 1 x -=- x print(x) # => 2  o = 2 o+=+o print(o) # => 4  print(3 --0-- 5 == 8) # => True

Эти странные операторы состоят из совершенно обычных и дружественных нам -= и -. Посмотрим, сможет ли наш линтер найти их:

  5:5      E225  missing whitespace around operator   x -=- x       ^    5:5      WPS346 Found wrong operation sign   x -=- x       ^    10:2     E225  missing whitespace around operator   o+=+o    ^    14:10    E225  missing whitespace around operator   print(3 --0-- 5 == 8)            ^    14:10    WPS346 Found wrong operation sign   print(3 --0-- 5 == 8)            ^    14:11    WPS345 Found meaningless number operation   print(3 --0-- 5 == 8)             ^    14:12    E226  missing whitespace around arithmetic operator   print(3 --0-- 5 == 8)              ^    14:13    WPS346 Found wrong operation sign   print(3 --0-- 5 == 8)               ^

Настала пора обнажить меч и принять бой:

x = 1 x += x  o = 2 o += o  print(3 + 5 == 8)

Враг повержен, и сразу вернулись прежние чистота и ясность!

Загадочные точки

Теперь жители сообщают о появлении странных глифов. О, смотрите-ка, вот и они!

print(0..__eq__(0)) # => True  print(....__eq__(((...)))) # => True

Что же здесь происходит? Кажется, там замешаны типы данных float и Ellipsis, но лучше удостовериться.

  21:7     WPS609 Found direct magic attribute usage: __eq__   print(0..__eq__(0))         ^    21:7     WPS304 Found partial float: 0.   print(0..__eq__(0))         ^    24:7     WPS609 Found direct magic attribute usage: __eq__   print(....__eq__(((...))))         ^

Ага, теперь понятно. Действительно, эти точки — краткая запись значений типа float (в первом случае) и Ellipsis (во втором). И в обоих случаях происходит обращение к методу, также через точку. Давайте посмотрим, что же скрывалось за этими знаками:

print(0.0 == 0) print(... == ...)

На этот раз все обошлось, но впредь не сравнивайте константы друг с другом, дабы не накликать беду.

Кривая дорожка

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

def some_func():     try:        return 'from_try'     finally:        return 'from_finally'  some_func() # => 'from_finally'

Функция не возвращает значение 'from_try' из-за закравшейся в код ошибки. «Как ее исправить?» — изумленно спросите вы.

31:5 WPS419 Found `try`/`else`/`finally` with multiple return paths  try:  ^

Оказывается, wemake-python-styleguide знает ответ: никогда не возвращайте значение из ветки finally. Послушаемся совета.

def some_func():   try:       return 'from_try'   finally:       print('now in finally')

Мрачный СИ-луэт прошлого

Древнее существо пробуждается. Уже несколько десятилетий никто не видел его, но теперь оно вернулось.

a = [(0, 'Hello'), (1, 'world')] for ['>']['>'>'>'], x in a:     print(x)

Что тут происходит? Известно, что в циклах можно распаковывать разные значения: почти любые валидные в Python выражения.

Правда, многое из этого примера нам не следовало бы делать:

  44:1     WPS414 Found incorrect unpacking target   for ['>']['>'>'>'], x in a:   ^    44:5     WPS405 Found wrong `for` loop variable definition   for ['>']['>'>'>'], x in a:       ^    44:11    WPS308 Found constant compare   for ['>']['>'>'>'], x in a:             ^    44:14    E225  missing whitespace around operator   for ['>']['>'>'>'], x in a:                ^    44:21    WPS111 Found too short name: x   for ['>']['>'>'>'], x in a:                       ^

Теперь разберемся с ['>'\]['>'>'>']. Похоже, что данное выражение можно просто переписать как ['>'\][0], поскольку у выражения '>' > '>' значение False. А False и 0 — одно и тоже.

Проблема решена.

Метки Темного Колдуна

Насколько сложным может быть выражение на Python? Наверняка такие конструкции — происки злых сил. Это Темный Колдун оставляет свои замысловатые метки во всех классах, к которым прикасается:

class _:     # Видите эти четыре метки?     _: [(),...,()] = {((),...,()): {(),...,()}}[((),...,())]  print(_._) # и этот оператор выглядит знакомо  # => {(), Ellipsis}

Что же за ними скрывается? Похоже, у каждой метки свое значение:

  • Объявление и указание типа: _: [(),...,()] =.
  • Определение словаря, где значение — набор данных: = { ((),...,()): {(),...,()} }.
  • Ключ: [((),...,())].

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

  55:5     WPS122 Found all unused variables definition: _   _: [(),...,()] = {((),...,()): {(),...,()}}[((),...,())]   ^    55:5     WPS221 Found line with high Jones Complexity: 19   _: [(),...,()] = {((),...,()): {(),...,()}}[((),...,())]   ^    55:36    WPS417 Found non-unique item in hash: ()   _: [(),...,()] = {((),...,()): {(),...,()}}[((),...,())]                                  ^    57:7     WPS121 Found usage of a variable marked as unused: _   print(_._)  # и этот оператор выглядит знакомо          ^

Теперь, когда мы удалили или зарефакторили это выражение (со значением 19 по метрике сложности Jones Complexity), от метки Темного Колдуна в бедном классе не осталось и следа. Очередные ростки зла уничтожены.

Метамагия

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

Сейчас классы выдают очень странные результаты:

class Example(type((lambda: 0.)())):  ...  print(Example(1) + Example(3)) # => 4.0

Почему 1 + 3 равно 4.0, а не 4? Чтобы это выяснить, рассмотрим поближе часть с type((lambda: 0.)()):

  • (lambda: 0.)() просто равно 0., а это просто иная запись 0.0.
  • type(0.0) возвращает тип float.
  • когда мы пишем Example(1), это значение преобразуется в Example(1.0) внутри класса.
  • Example(1.0) + Example(3.0) = Example(4.0).

Давайте убедимся, что наш линтер-клинок по-прежнему остр:

  63:15    WPS606 Found incorrect base class   class Example(type((lambda: 0.)())):                 ^    63:21    WPS522 Found implicit primitive in a form of lambda   class Example(type((lambda: 0.)())):                       ^    63:29    WPS304 Found partial float: 0.   class Example(type((lambda: 0.)())):                               ^    64:5     WPS428 Found statement that has no effect   ...   ^    64:5     WPS604 Found incorrect node inside `class` body   ...   ^

Со всем разобрались, теперь наши классы в безопасности. Можем двигаться дальше.

Иллюзии

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

a = ['a', 'b'] print(set(x + '!' for x in a)) # => {'b!', 'a!'}  print(set((yield x + '!') for x in a)) # => {'b!', None, 'a!'}

Это одно из хтонических чудовищ Python — да, они все-таки существуют и тут. Учитывая, что в python3.8 такая конструкция приведет к SyntaxError, yield и yield from следует использовать только в функциях-генераторах.

А вот и отчет об инциденте:

  73:7     C401  Unnecessary generator - rewrite as a set comprehension.   print(set(x + '!' for x in a))         ^    76:7     C401  Unnecessary generator - rewrite as a set comprehension.   print(set((yield x + '!') for x in a))         ^    76:11    WPS416 Found `yield` inside comprehension   print(set((yield x + '!') for x in a))

И давайте перепишем обработку, как нам предлагают.

print({x + '!' for x in a})

Эта задачка была сложна, но и мы не лыком шиты. Что же дальше?

Злобный двойник email

Если нужно записать адрес электронной почты, то используем строку, ведь так? А вот и нет!

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

class G:     def __init__(self, s):         self.s = s     def __getattr__(self, t):         return G(self.s + '.' + str(t))     def __rmatmul__(self, other):         return other + '@' + self.s  username, example = 'username', G('example') print(username@example.com) # => username@example.com

Разберемся, как это работает.

  • в Python @ — это оператор, который можно переопределить с помощью магических методов __matmul__ и __rmatmul__.
  • выражение .com означает обращение к свойству com; переопределяется методом __getattr__.

Этот пример значительно отличается от остальных тем, что он-то на самом деле корректный. Просто вот такой необычный. Вероятно, пользоваться им мы не будем, но в бестиарий запишем.

Сила заблуждений

В нашем королевстве настали смутные времена. Тьма наделила жителей сомнительными способностями. Это раскололо содружество разработчиков и привело к серьезным разногласиям.

Способности эти воистину страшные, ибо теперь вам дано программировать в строках:

from math import radians for angle in range(360):     print(f'{angle=} {(th:=radians(angle))=:.3f}')     print(th)  # => angle=0 (th:=radians(angle))=0.000 # => 0.0 # => angle=1 (th:=radians(angle))=0.017 # => 0.017453292519943295 # => angle=2 (th:=radians(angle))=0.035 # => 0.03490658503988659

Что происходит в этом примере?

  • f'{angle=} — это способ записи f'angle={angle} в новых версиях (python3.8+).
  • (th:=radians(angle)) — это операция присваивания значения; да, теперь можно так делать и внутри строки.
  • =:.3f указывает на формат вывода: возвращается значение, округленное до третьего знака
  • метод print(th) отрабатывает, так как (th:=radians(angle)) имеет локальную область видимости в части кода, где находится вся строка.

Стоит ли использовать f-строки? Как хотите.

Стоит ли определять переменные в f-строках? Ни в коем случае.

А вот дружеское напоминание о том, что еще можно (но, наверное, не нужно) написать с помощью f-строк:

print(f"{getattr(__import__('os'), 'eman'[None:None:-1])}") # => posix

Всего лишь импортируем модуль внутри строки, ничего такого, идем дальше.

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

  105:1    WPS221 Found line with high Jones Complexity: 16   print(f"{getattr(__import__('os'), 'eman'[None:None:-1])}")   ^    105:7    WPS305 Found `f` string   print(f"{getattr(__import__('os'), 'eman'[None:None:-1])}")         ^    105:18   WPS421 Found wrong function call: __import__   print(f"{getattr(__import__('os'), 'eman'[None:None:-1])}")                    ^    105:36   WPS349 Found redundant subscript slice   print(f"{getattr(__import__('os'), 'eman'[None:None:-1])}")                                      ^

И еще кое-что: f-строки нельзя использовать как переменные docstrings:

def main():     f"""My name is {__file__}/{__name__}!"""  print(main().__doc__) # => None

Заключение

Мы сразились со многими жуткими монстрами, расплодившимися в коде, и сделали королевство Python прекраснее. Вы герой, гордитесь собой!

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

На сегодня все. Удачи на тракте, пусть звезды ярко освещают ваш путь!

Полезные ссылки

А вы, вольные жители Python королевства, встречались с подобной черной магией в вашем коде? Удалось ли справиться с ней? Или битва еще не завершена (или вовсе проиграна)? Если вам нужна помощь бывалых магов и чародеев Python, то приходите к нам на Moscow Python Conf++ 27 марта 2020 года. У нас будут проверенные рецепты по борьбе с плохим и старым кодом от Владимира Филонова (доклад + 2 часа практики), Кирилла Борисова и Левона Авакяна.

ссылка на оригинал статьи https://habr.com/ru/company/oleg-bunin/blog/485960/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *