Чистое зло 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})
Эта задачка была сложна, но и мы не лыком шиты. Что же дальше?
Если нужно записать адрес электронной почты, то используем строку, ведь так? А вот и нет!
Для решения обычных задач существуют необычные способы. А у обычных типов данных есть злые двойники. Сейчас мы выясним, кто есть кто.
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 часа практики), Кирилла Борисова и Левона Авакяна.