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

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

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

Унарные операторы в 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.

Заключение


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

Не злоупотребляйте им!

Комментарии (9)


  1. Andy_U
    23.02.2018 15:11

    Убил бы за эту «фичу». Как то, видимо" передержал нажатую клавишу "-" и получил вместо минуса "--". в длинном выражении. Еле нашел потом ошибку.


    1. masai
      23.02.2018 15:34

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


      1. Andy_U
        23.02.2018 15:50

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


        1. ZyXI
          23.02.2018 16:51

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


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


  1. masai
    23.02.2018 15:35

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


  1. AlePil
    23.02.2018 17:01

    Весело получилось.
    Если убрать заголовок, то можно подумать, что написано на обновленном Brainfuck.


  1. Code_God
    23.02.2018 19:31

    Прикольно! Весь секрет в логическом значении.


  1. mcblack
    24.02.2018 08:08

    Python 3.6.4 не работает:

    Заголовок спойлера
    >>> +--+_+-+_++_+--_+_-_+-+-+-___++++_+-_-+++_+-+_--++--_
    Traceback (most recent call last):
      File "<pyshell#0>", line 1, in <module>
        +--+_+-+_++_+--_+_-_+-+-+-___++++_+-_-+++_+-+_--++--_
    NameError: name '_' is not defined


    1. saluev Автор
      24.02.2018 11:29

      _, ___ = Morse(), MorseWithSpace()

      Без дополнительных объектов не обойтись.