Чистое зло 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 часа практики), Кирилла Борисова и Левона Авакяна.

    Конференции Олега Бунина (Онтико)
    Конференции Олега Бунина

    Комментарии 26

      +2

      Использовать "О" в качестве имени переменной — это чистое зло. Помню как в бейске или паскале был ноль перечёркнут вот как от таких вот недоразумений.

        +1
        Ммм? В бейсике или тем более, паскале (который может жить просто компилятором) был свой, особенный шрифт, отличный от системного?
          +1
          Хм, а я подумал про бумажные книги по Бейсику
        +4
        print(_._)

        О это же оператор задницы, в Scala есть почти такой же: map(_,_).

          +1

          Что-то не припомню в скале такого оператора. Там конечно можно для коллекции написать что-то типа .reduce(_ + _) или .map(_._1), но ни в том, ни в другом случае это конечно никакой не оператор.

            0

            да простите, это в случае сопоставления с образцом можно забабахать, что-то вроде case MyCaseClass (_,_) => ...

          +3
          Мне, наверное, повезло и я не сталкивался с таким кодом, который написан в примерах :)
          В основном всё было pythonic way
            +14
            Как писать на Питоне, чтобы получился Перл
              +1
              аж до слёз
                0

                Это не те wtf'ы, которые обычно ожидают вас в Питоне. Просто совсем не те. Такие вещи и без линтера видно — фигня написана.


                А вот это пример wtf'а настоящего:


                >>> a={ 1: 'first', True: 'second'}
                >>> a[1]
                'second'

                Всё понятно почему, но дикий wtf.

                  +1

                  Потому, что секонд перезаписал фест?

                    –1
                    Потому что он обращается к 2-му элементу словаря.
                      +1
                      который перезаписал первый?
                      >>> a={ 1: 'first', True: 'second'}
                      >>> len(a)
                      1

                        0
                        Да, тут я сам тупанул. Чисто из-за булевой логики в python 1 и True одно и то же.
                        Если в коде изменить 1 на 0, а True на False результат будет тем же.
                    +1
                    Георгий, а в чём собственно WTF? Булин — сабкласс от инта, отсюда все хэш и арифметические операции абсолютно логичны
                    >>> hash(1)
                    1
                    >>> hash(True)
                    1
                    >>> True + True
                    2

                    Или WTF что оно так реализовано?
                      0
                      In [128]: {True: 'yes', 1: 'no', 1.0: 'maybe'}                                                
                      Out[128]: {True: 'maybe'}

                      Это уже не так логично.)

                        0
                        По-моему ничего не изменилось в этом примере
                        >>> a = {True: 'yes', 1: 'no', 1.0: 'maybe'}
                        >>> a[True]
                        'maybe'
                        >>> a[1]
                        'maybe'
                        >>> a[1.0]
                        'maybe'
                        >>> a.__str__()
                        "{True: 'maybe'}"
                        

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

                          Что такое "стринг-репрезентация"? В питоне используется object.__hash__.


                          Если покопаться, то можно понять что происходит.


                          >>> a=1
                          >>> b=True
                          >>> a.__hash__.__objclass__
                          <class 'int'>
                          >>> b.__hash__.__objclass__
                          <class 'int'>
                        0

                        Признак WTF — пока причин не знаешь, кажется ахинеей, а когда причины знаешь, они не кажутся обоснованными. Кстати, особый wtf тут доставляет то, что (1 is True) == False. Т.е. id у них разные, классы у них разные, а хеши у них почему-то одинаковые.

                        0

                        Ух ты, какая строгая типизация.

                          +1

                          Я специально самый wtf случай приводил. В целом у питона умеренно строгая типизация, которую делают строже в новых версиях. О преимуществах и недостатках динамической типизации можно долго дискутировать. Недостатки — вот такие wtf'ы, преимущества — обычно в районе произвольного числа аргументов, интраспеции и 'everything is an object, even 'type' is an object'.

                        +3
                        Такой код можно написать если только нарочно
                          +1
                          А вы, вольные жители Python королевства, встречались с подобной черной магией в вашем коде?
                          Почти все эти примеры не то, что встретить в коде, их даже выдумать сложно.
                            +1
                            видимо, как раз в этом и прикол
                            –1
                            Препод в универе оценит
                              0
                              :) Зачем Владимир Филонов создаёт плохой и старый код? «У нас будут проверенные рецепты по борьбе с плохим и старым кодом от Владимира Филонова»

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

                              Самое читаемое