Балуемся с унарными операторами в Python

    >>> +--+_+-+_++_+--_+_-_+-+-+-___++++_+-_-+++_+-+_--++--_
    'ПРИВЕТ, ХАБР!'

    Что это было? Да, вы не ошиблись — это азбука Морзе с плюсиками вместо точек прямо в синтаксисе Питона!

    Если вы не понимаете, как это работает, или просто не прочь освежить свои знания в День Советской армии (и Военно-морского флота!), добро пожаловать под кат.

    Унарные операторы в Python


    В Питоне есть три унарных оператора: +, - и ~ (побитовое отрицание). (Есть ещё not, но это отдельная история.) Интересно то, что их можно комбинировать в неограниченных количествах:

    >>> ++-++-+---+--++-++-+1
    -1
    >>> -~-~-~-~-~-~-~-~-~-~1
    11

    И все три из них можно переопределить для своих объектов.

    Но только у двух из них — плюса и минуса — есть омонимические бинарные варианты. Именно это позволит нам скомбинировать несколько последовательностей плюсов и минусов, каждая из которых будет одной буквой в азбуке Морзе, в единое валидное выражение: приведённая в начале строка +--+_+-+_++_+--_+_-_+-+-+-___++++_+-_-+++_+-+_--++--_ распарсится как

    
    (+--+_) + (-+_) + (+_) + (--_) + _ - _ + (-+-+-___) + (+++_) + (-_) - (+++_) + (-+_) - (-++--_)
    

    Осталось определить объекты _ (конец последовательности) и ___ (конец последовательности и пробел).

    Переопределение операторов в Python


    Для переопределения операторов в Python нужно объявлять в классе методы со специальными названиями. Так, для унарных плюса и минуса это __pos__ и __neg__, а для бинарных — это сразу четыре метода: __add__, __radd__, __sub__ и __rsub__.

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

    class Morse(object):
    
        def __init__(self, buffer=""):
            self.buffer = buffer
    
        def __neg__(self):
            return Morse("-" + self.buffer)
    
        def __pos__(self):
            return Morse("." + self.buffer)

    Также наш объект должен уметь конвертироваться в строчку. Давайте заведём словарь с расшифровкой азбуки Морзе и добавим метод __str__.

    Азбука Морзе
    morse_alphabet = {
        "А" : ".-",
        "Б" : "-...",
        "В" : ".--",
        "Г" : "--.",
        "Д" : "-..",
        "Е" : ".",
        "Ж" : "...-",
        "З" : "--..",
        "И" : "..",
        "Й" : ".---",
        "К" : "-.-",
        "Л" : ".-..",
        "М" : "--",
        "Н" : "-.",
        "О" : "---",
        "П" : ".--.",
        "Р" : ".-.",
        "С" : "...",
        "Т" : "-",
        "У" : "..-",
        "Ф" : "..-.",
        "Х" : "....",
        "Ц" : "-.-.",
        "Ч" : "---.",
        "Ш" : "----",
        "Щ" : "--.-",
        "Ъ" : "--.--",
        "Ы" : "-.--",
        "Ь" : "-..-",
        "Э" : "..-..",
        "Ю" : "..--",
        "Я" : ".-.-",
        "1" : ".----",
        "2" : "..---",
        "3" : "...--",
        "4" : "....-",
        "5" : ".....",
        "6" : "-....",
        "7" : "--...",
        "8" : "---..",
        "9" : "----.",
        "0" : "-----",
        "." : "......",
        "," : ".-.-.-",
        ":" : "---...",
        ";" : "-.-.-.",
        "(" : "-.--.-",
        ")" : "-.--.-",
        "'" : ".----.",
        "\"": ".-..-.",
        "-" : "-....-",
        "/" : "-..-.",
        "?" : "..--..",
        "!" : "--..--",
        "@" : ".--.-.",
        "=" : "-...-",
    }
    
    inverse_morse_alphabet = {v: k for k, v in morse_alphabet.items()}

    Метод:

        def __str__(self):
            return inverse_morse_alphabet[self.buffer]
            # Если в словаре нет текущей последовательности,
            # то это KeyError. Ну и отлично.

    Далее, бинарное сложение и вычитание. Они в Питоне левоассоциативны, то бишь будут выполняться слева направо. Начнём с простого:

        def __add__(self, other):
            return str(self) + str(+other)
            # Обратите внимание на унарный + перед other.

    Итак, после сложения первых двух последовательностей у нас получится строка. Сможет ли она сложиться со следующим за ней объектом типа Morse? Нет, сложение с этим типом в str.__add__ не предусмотрено. Поэтому Питон попытается вызвать у правого объекта метод __radd__. Реализуем его:

        def __radd__(self, s):
            return s + str(+self)

    Осталось сделать аналогично для вычитания:

        def __sub__(self, other):
            return str(self) + str(-other)
    
        def __rsub__(self, s):
            return s + str(-self)

    Весь класс вместе
    class Morse(object):
    
        def __init__(self, buffer=""):
            self.buffer = buffer
    
        def __neg__(self):
            return Morse("-" + self.buffer)
    
        def __pos__(self):
            return Morse("." + self.buffer)
    
        def __str__(self):
            return inverse_morse_alphabet[self.buffer]
    
        def __add__(self, other):
            return str(self) + str(+other)
    
        def __radd__(self, s):
            return s + str(+self)
    
        def __sub__(self, other):
            return str(self) + str(-other)
    
        def __rsub__(self, s):
            return s + str(-self)


    Давайте напишем простенькую функцию, которая будет конвертировать нам строки в код на Питоне:

    def morsify(s):
        s = "_".join(map(morse_alphabet.get, s.upper()))
        s = s.replace(".", "+") + ("_" if s else "")
        return s

    Теперь мы можем забить всю эту красоту в консоль и увидеть, что код работает:
    >>> morsify("ПРИВЕТ,ХАБР!")
    '+--+_+-+_++_+--_+_-_+-+-+-_++++_+-_-+++_+-+_--++--_'
    >>> _ = Morse()
    >>> +--+_+-+_++_+--_+_-_+-+-+-_++++_+-_-+++_+-+_--++--_
    'ПРИВЕТ,ХАБР!'


    Добавляем поддержку пробелов


    Давайте сделаем объект, который будет вести себя как Morse, только ещё добавлять пробел в конце.

    class MorseWithSpace(Morse):
        def __str__(self):
            return super().__str__() + " "
    
    ___ = MorseWithSpace()

    Просто? Да! Работает? Нет :-(

    Чтобы в процессе работы объекты типа MorseWithSpace не подменялись объектами типа Morse, надо ещё поменять __pos__ и __neg__:

        def __neg__(self):
            return MorseWithSpace(super().__neg__().buffer)
    
        def __pos__(self):
            return MorseWithSpace(super().__pos__().buffer)

    Также стоит добавить запись " " : " " в словарь азбуки Морзе и поменять чуть-чуть функцию morsify:

    def morsify(s):
        s = "_".join(map(morse_alphabet.get, s.upper()))
        s = s.replace(".", "+") + ("_" if s else "")
        s = s.replace("_ ", "__").replace(" _", "__")
        return s

    Работает!

    >>> morsify("ПРИВЕТ, ХАБР!")
    '+--+_+-+_++_+--_+_-_+-+-+-___++++_+-_-+++_+-+_--++--_'
    >>> ___ = MorseWithSpace()
    >>> +--+_+-+_++_+--_+_-_+-+-+-___++++_+-_-+++_+-+_--++--_
    'ПРИВЕТ, ХАБР!'

    Весь код в Gist.

    Заключение


    Переопределение операторов может завести вас далеко и надолго.

    Не злоупотребляйте им!
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 11
    • 0
      Убил бы за эту «фичу». Как то, видимо" передержал нажатую клавишу "-" и получил вместо минуса "--". в длинном выражении. Еле нашел потом ошибку.
      • +1

        Вы про возможность комбинировать бинарные операторы? Да, она, конечно, не самая востребованная, но с другой стороны, вреда от неё не так много. Два минуса должны были поймать тесты. Точно так же можно передержать минус и получить вместо a-b выражение a--b. Или случайно поставить запятую в присваивании вместо точки: a = 5,3 и получить кортеж. Надо, конечно, при разработке стремится сделать такие ошибки маловероятными, но от опечаток никак не застраховаться. (Мораль: тесты — наше всё.)

        • 0
          Да наличие-то ошибки сразу всплыло. Но я ее локализовать очень долго в длинном выражении не мог. Всякие синусы, косинусы, экспоненты и пр. А насчет востребованности — вот я только в статье единственный пример и увидел. Еще есть?
          • 0

            Вообще это как бы не «фича». Возможность написания ++i вытекает из грамматики языка, чтобы «убрать» эту возможность нужно усложнить грамматику. Запрет на последовательные унарные операторы я что‐то нигде не видел, хотя их и нужно писать с пробелом во многих языках из‐за существования инкремента/декремента (или из‐за того, что унарного плюса нет, а -- начинает комментарий — это я про lua).


            Комбинирование унарных/бинарных операторов проходит по тому же разряду.

            • 0
              Попробуйте линтеры.
              Не ручаюсь за линтеры в питоне, но они любят подсвечивать «странные легальные конструкции» которые больше похожи на опечатки чем умышленное использование. Мне помогали:
              if (foo);
              if (a=b)
              Правда при первом применении все горит красным как в аду.
        • 0

          Да, интересный вышел DSL с морзянкой. Вроде бы идея на поверхности, но попробуй додумайся. :)

          • +4
            Весело получилось.
            Если убрать заголовок, то можно подумать, что написано на обновленном Brainfuck.
            • +1
              Прикольно! Весь секрет в логическом значении.
              • 0
                Python 3.6.4 не работает:
                Заголовок спойлера
                >>> +--+_+-+_++_+--_+_-_+-+-+-___++++_+-_-+++_+-+_--++--_
                Traceback (most recent call last):
                  File "<pyshell#0>", line 1, in <module>
                    +--+_+-+_++_+--_+_-_+-+-+-___++++_+-_-+++_+-+_--++--_
                NameError: name '_' is not defined

                • 0
                  _, ___ = Morse(), MorseWithSpace()

                  Без дополнительных объектов не обойтись.
                • +1
                  Если кому интересно, так это выглядит на языке R: github.com/bolknote/r-playground/blob/master/morse.R

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

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