>>> +--+_+-+_++_+--_+_-_+-+-+-___++++_+-_-+++_+-+_--++--_
'ПРИВЕТ, ХАБР!'
Что это было? Да, вы не ошиблись — это азбука Морзе с плюсиками вместо точек прямо в синтаксисе Питона!
Если вы не понимаете, как это работает, или просто не прочь освежить свои знания в День Советской армии (и Военно-морского флота!), добро пожаловать под кат.
Унарные операторы в 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)
masai
23.02.2018 15:35Да, интересный вышел DSL с морзянкой. Вроде бы идея на поверхности, но попробуй додумайся. :)
AlePil
23.02.2018 17:01Весело получилось.
Если убрать заголовок, то можно подумать, что написано на обновленном Brainfuck.
mcblack
24.02.2018 08:08Python 3.6.4 не работает:
Заголовок спойлера>>> +--+_+-+_++_+--_+_-_+-+-+-___++++_+-_-+++_+-+_--++--_ Traceback (most recent call last): File "<pyshell#0>", line 1, in <module> +--+_+-+_++_+--_+_-_+-+-+-___++++_+-_-+++_+-+_--++--_ NameError: name '_' is not defined
saluev Автор
24.02.2018 11:29_, ___ = Morse(), MorseWithSpace()
Без дополнительных объектов не обойтись.
Andy_U
Убил бы за эту «фичу». Как то, видимо" передержал нажатую клавишу "-" и получил вместо минуса "--". в длинном выражении. Еле нашел потом ошибку.
masai
Вы про возможность комбинировать бинарные операторы? Да, она, конечно, не самая востребованная, но с другой стороны, вреда от неё не так много. Два минуса должны были поймать тесты. Точно так же можно передержать минус и получить вместо
a-b
выражениеa--b
. Или случайно поставить запятую в присваивании вместо точки:a = 5,3
и получить кортеж. Надо, конечно, при разработке стремится сделать такие ошибки маловероятными, но от опечаток никак не застраховаться. (Мораль: тесты — наше всё.)Andy_U
Да наличие-то ошибки сразу всплыло. Но я ее локализовать очень долго в длинном выражении не мог. Всякие синусы, косинусы, экспоненты и пр. А насчет востребованности — вот я только в статье единственный пример и увидел. Еще есть?
ZyXI
Вообще это как бы не «фича». Возможность написания
++i
вытекает из грамматики языка, чтобы «убрать» эту возможность нужно усложнить грамматику. Запрет на последовательные унарные операторы я что?то нигде не видел, хотя их и нужно писать с пробелом во многих языках из?за существования инкремента/декремента (или из?за того, что унарного плюса нет, а--
начинает комментарий — это я про lua).Комбинирование унарных/бинарных операторов проходит по тому же разряду.