23 February 2018

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

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

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

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

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

Заключение


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

Не злоупотребляйте им!
Tags:pythonунарные операторыпятница
Hubs: Abnormal programming Python Programming
+49
17.2k 80
Comments 11