Метка: telegram

Пишем Brainfuck на Python

Есть мнение, что каждый программист должен написать в жизни хотя бы один язык программирования, либо реализовать уже существующий, но своими руками. В качестве примера мы начнем с самого простого и создадим Brainfuck. Согласно Вики:

Brainfuck — один из известнейших эзотерических языков программирования, придуман Урбаном Мюллером (нем.Urban Müller) в 1993 году, известен своим минимализмом. Название языка можно перевести на русский как вынос мозга, оно напрямую образовано от английского выражения brainfuck (brain — мозг, fuck — иметь половое сношение), т. е. заниматься ерундой. Язык имеет восемь команд, каждая из которых записывается одним символом. Исходный код программы на Brainfuck представляет собой последовательность этих символов без какого-либо дополнительного синтаксиса.

Википедия.

Описание 8 команд:

Команда BrainfuckОписание команды
Начало программывыделяется память под 30 000 ячеек с нулевыми начальными значениями
>перейти к следующей ячейке
<перейти к предыдущей ячейке
+увеличить значение в текущей ячейке на 1
-уменьшить значение в текущей ячейке на 1
.напечатать значение из текущей ячейки
,ввести извне значение и сохранить в текущей ячейке
[если значение текущей ячейки ноль, перейти вперёд по тексту программы на ячейку, следующую за соответствующей ] (с учётом вложенности)
]если значение текущей ячейки не нуль, перейти назад по тексту программы на символ [ (с учётом вложенности)

Реализация интерпретатора на Python

Интерпретатор читает программу и выполняет ее сразу на лету.

Первым делом надо прочитать целиком файл с исходником на BF, имя которого мы передаем первым параметром командной строки:

if __name__ == '__main__':
    if len(sys.argv) == 2:
        # один аргумент - имя файла, который надо выполнить
        with open(sys.argv[1], 'r') as f:
            code = f.read()
        run(code)  # запустить код
    else:
        print(f'usage: python {sys.argv[0]} file.bf')

После того, как код прочитан, мы создаем память из 30 000 ячеек, заполненными нулями. Устанавливаем счетчик команд и указатель на ленте ячеек на 0. Инициализация имеет такой вид:

mem_size = 30000
memory = [0] * mem_size  # оперативная память программы
data_ptr = 0  # индекс текущей ячейки памяти (memory)
instr_ptr = 0  # индекс текущей инструкции кода (code)

Далее в цикле читаем ячейку, если это одна из наших 8 команд, то выполняем, иначе пропускаем (это могут быть пробелы и переносы строк, их просто игнорируем). После выполнения команды, указатель сдвигается вправо по коду (увеличивается на единицу). Программа выполняется до тех пор, пока счетчик инструкций не выйдет за пределы размера кода.

С командами типа «+», «-«, «<«, «>» проблем в понимании быть не должно, они делают простые действия. Разве что, вам следует обратить внимание на то, что я зацикливаю оперативную память, если команда смещения переходит границу (команда < от 0 сдвинет на 29999 ячейку, а команда > с ячейки номер 29999 сдвигает на 0 ячейку).

Фрагмент цикла:

# выполняем - пока не вылетели за пределы кода
while instr_ptr < len(code):
    command = code[instr_ptr]  # текущая команда

    if command == '+':
        memory[data_ptr] += 1
    elif command == '-':
        memory[data_ptr] -= 1
    elif command == '>':
        data_ptr = (data_ptr + 1) % mem_size  # циклическая память
    elif command == '<':
        data_ptr = (data_ptr - 1) % mem_size  # циклическая память
    ...
    instr_ptr += 1   # сдвигаеи к следующей команде

Команда точка «.» печатает на экране единственный символ из текущий ячейки памяти. Я использую функцию chr, чтобы из числа получить символ. end='' указывает, что не надо переводить строку. Команда запятая «,» вводит символ из потока ввода. Считываю строку, беру первый символ и получаю его код функцией ord. Дополним код цикла обработкой ввода и вывода:

...
elif command == '.':
    # печатаем символ с кодом = значения текущей ячейки
    print(chr(memory[data_ptr]), end='')
elif command == ',':
    # ввод - берем код первого символа или 0, если ввод пустой
    inp = input()
    memory[data_ptr] = ord(inp[0]) if inp else 0
...

Это еще не все. Осталось самое сложное – обработка циклов (они же играю роль ветвлений в BF). Открывающая квадратная скобка «[« в code – команда, говорит нам, что нужно проверить текущую ячейку памяти в memory на равенство нулю. Если нуль, то прыгаем за пределы цикла – на соответствующую закрывающую скобку «]». Если не нуль, то дальше просто выполняем тело цикла. Когда натакливаемся на «]», тут тоже нужно проверить условие цикла, и если оно даст не ноль – вернуться к началу цикла, а иначе продолжить выполнять код дальше за скобкой.

Каждый раз во время выполнения кода искать нужную скобку – неудобно и медленно. Можно оптимизировать код, заранее пройдя по нему и запомнив, где какая скобка и где ее подруга – парная скобка. Для этого заведем словарь bracket_map. Для учета вложенности будем пользоваться структурой данных – стэк (из обычного списка):

# ключ и значения - индекс байта в коде
# по любой скобке находим ей пару
bracket_map = {}
stack = []
for pos, symbol in enumerate(code):
    # открыли скобку - положим на вершину стэка ее позицию
    if symbol == '[':
        stack.append(pos)
    elif symbol == ']':
        # вершина стэка - как раз наша парная скобка - была последней
        # удалим ее с вершины
        last_open_pos = stack.pop()
        # создадим парные записи
        bracket_map[pos] = last_open_pos
        bracket_map[last_open_pos] = pos

На иллюстрации ниже показано, что хранит этот словарь на примере простой программы.

Схема скобок для Hello world

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

...
elif command == '[':
    # начало цикла - проверка текущей ячейки
    if not memory[data_ptr]:  # == 0
        # значит надо перейти на ячейку за соответствующей ей закрывающей скобкой
        instr_ptr = bracket_map[instr_ptr]
elif command == ']':
    # проверяем тоже условие, если выполняется, то скачем на начало цилка
    if memory[data_ptr]:  # не ноль
        # перемещаем на конец цикла
        instr_ptr = bracket_map[instr_ptr]

Готово! Интерпретатор завершен! Выполним простую программу hello_world.bf:

 ++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++
 .>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.
 ------.--------.>+.>.
(venv) ➜  bf python bf_interpretator.py hello_world.bf
Hello World! 

Работает! А вот как вам сортировка вставками на Brainfuck:

>>+>,[
    <[
        [>>+<<-]>[<<+<[->>+[<]]>>>[>]<<-]<<<
    ]>>[<<+>>-]<[>+<-]>[>>]<,
]<<<[<+<]>[>.>]

Запускаем. Пишем по очереди символы по одному, энтер после каждого. Чтобы завершить ввод массива – просто энтер без символа:

(venv) ➜  bf python bf_interpretator.py isort.bf      
8
4
6
1
9

14689  

Код программы и примеры загрузил на gist.

Компилятор Brainfuck на Python

Компилятор берет исходный код и превращает его в машинный исполняемый код. Не бойтесь, мы не будем писать в сложные по формату исполняемые файлы ассемблерные команды в виде байт. Поступим хитро. Сначала транслируем программу на BF в код на языке С, а потом обычным компилятором С транслируем код С в исполняемый файл.

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

PRELUDE = """// brainfuck generated:
#include <stdio.h>
int main() {
    const int mem_size = 30000;
    char mem[mem_size];
    memset(mem, 0, mem_size);
    int cur = 0;
"""

ENDING = """
    return 0;
}
"""

Далее код компилятора BF в Си. Все по таблице из Википедии:

def compile_to_c(bf_code):
    instr_ptr = 0  # индекс текущей инструкции кода (code)

    indent = 4  # смещение пробелами - для красоты
    c_code = PRELUDE

    # выполняем - пока не вылетели за пределы кода
    for command in bf_code:
        content = ''
        if command == '+':
            content = 'mem[cur]++;'
        elif command == '-':
            content = 'mem[cur]--;'
        elif command == '>':
            content = 'cur++;'
        elif command == '<':
            content = 'cur--;'
        elif command == '.':
            content = 'putchar(mem[cur]);'
        elif command == ',':
            content = 'mem[cur] = getchar();'
        elif command == '[':
            content = 'while(mem[cur]) {'
        elif command == ']':
            content = '}'

        instr_ptr += 1  # сдвигаем к следующей команде

        if content:
            if command == ']':
                indent -= 4
            c_code += ' ' * indent + content + '\n'
            if command == '[':
                indent += 4

    c_code += ENDING
    return c_code

Сохраняем С-код на диск и вызывем компилятор cc:

c_code = compile_to_c(code)
c_file = f'{name}.c'
with open(c_file, 'w') as f:
    f.write(c_code)
os.system(f'cc {c_file} -o {name}.out')

Проверка:

(venv) ➜  bf python bf_compiler.py hello_world.bf
...тут какое-то предупреждение от компилятора...

(venv) ➜  bf ./hello_world.bf.out 
Hello World!

Код на Си получится примерно такой (я сократил его значительно, чтобы не занимать много места в статье):

// brainfuck generated:
#include <stdio.h>
int main() {
    const int mem_size = 30000;
    char mem[mem_size];
    memset(mem, 0, mem_size);
    int cur = 0;
    mem[cur]++;
    mem[cur]++;
    mem[cur]++;
    mem[cur]++;
    mem[cur]++;
    while(mem[cur]) {
        cur++;
        mem[cur]++;
        mem[cur]++;
        mem[cur]++;
        cur++;
        mem[cur]++;
        cur--;
        cur--;
        cur--;
        cur--;
        mem[cur]--;
    }
    cur++;
    mem[cur]++;
    mem[cur]++;
    putchar(mem[cur]);
    cur++;
    mem[cur]++;
    putchar(mem[cur]);
    mem[cur]++;
    mem[cur]++;
    mem[cur]++;
    mem[cur]++;
    mem[cur]++;
    mem[cur]++;
    putchar(mem[cur]);
    cur++;
    putchar(mem[cur]);

    return 0;
}

Код компилятора в том же самом gist.

Замечания. В это варианте запятая будет работать не так, как в интерпретаторе выше. Поэтому код с сортировкой будет вести себя иначе. И второе: проверял компилятор на своей системе macOS, на Windows компилятор скорее всего не будет работать, если только вы его не запускаете из подсистемы Linux или Cygwin или подобных.

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Доступ к атрибутам

Скрин из Готики 1: страж не пускает в замок героя

Атрибуты объекта в Python – это именованные поля (данные, функции), присущие данному объекту (экземпляру, классу). Самый простой доступ к атрибутам – через точку:

class Foo:
     def init(self):
         self.x = 88  # установка значения атрибута      
 f = Foo()
 print(f.x)  # доступ к атрибуту через точку

Если мы обратимся к атрибуту, которого нет, то получим ошибку AttributeError. Мы можем переопределить это поведение путем реализации магических методов __getattr__ или __getattribute__.

__getattr__ вызывается, если атрибут не найден обычным способом (не был задан ранее через точку, функцию setattr, или через __dict__). Если атрибут найден, то __getattr__ НЕ вызывается.

📎 Пример. Возвращаем -1 для любого несуществующего атрибута.

class Test:
    def __getattr__(self, item):
        print(f'__getattr__({item})')
        return -1  

t = Test()
# зададим x и y
t.x = 10
setattr(t, 'y', 33)

print(t.x)  # 10
print(t.y)  # 33  
print(t.z)  # __getattr__(z) -1

Метод __getattribute__ вызывается, когда мы пытаемся получить любой атрибут, не зависимо от того, есть он или нет. Этот метод, вызывается прежде __getattr__. Он немного хитрее. Если __getattribute__ кидает AttributeError, то будет вызвана __getattr__.

📎 Пример. Мы можем запретить чтение каких-то атрибутов:

class Test:
    def __getattr__(self, item):
        print(f'__getattr__({item})')
        return -1

    def __getattribute__(self, item):
        print(f'__getattribute__({item})')
        if item == 'y':  # запретим получать y
            raise AttributeError
        return super().__getattribute__(item)


# зададим x и y
t = Test()
t.x = 10
t.y = 20

print(t.x)  # __getattribute__(x) 10
print(t.y)  # __getattribute__(y) __getattr__(y) -1
print(t.z)  # __getattribute__(z) __getattr__(z) -1

⚠️ Внимание! В __getattribute__ мы можем вызвать super().__getattribute__(item) или object.__getattribute__(self, item), что посути тоже самое, но не следует делать return self.__dict__[item] или return self.__getattribute__(item) или return getattr(self, item), так как это приведет к бесконечной рекурсии.

💡 Также есть магический метод __setattr__(self, key, value), вызываемый при obj.key = value или setattr(obj, 'key', value). У него нет более длинно-названного брата-близнеца.

Для полноты картины еще есть встроенная функция getattr(object, name[, default]). Вызов getattr(x, 'y') аналогичен обращению через точку: x.y В первом случае ‘y’ – это строка, что позволяет нам динамически получать атрибуты объектов, в отличие от точки, которая требует фиксированного имени на этапе написания кода. В случае, если атрибут недоступен мы получим AttributeError при незаданном default или получим default (без возникновения ошибки), если default был задан третьим аргументом.

Специально для канала @pyway.

​​🗓 Календарь

Когда под рукой нет календаря, но есть Python:

import calendar; calendar.TextCalendar().pryear(2019)

Или из командной строки:

python -c 'import calendar; calendar.TextCalendar().pryear(2019)'
Календарь

Хотите по-русски (если вдруг еще не)?

import locale
locale.setlocale(locale.LC_ALL, 'ru_RU')
import calendar 
calendar.TextCalendar().pryear(2019)

А еще можно узнать, високосный ли год:

>>> calendar.isleap(2019)
False
>>> calendar.isleap(2020)
True

Или какой сегодня день недели?

>>> calendar.day_name[calendar.weekday(2019, 2, 19)]
'вторник'

Больше функций календаря ищите в документации к модулю calendar.

Специально для канала @pyway.

Цепочки сравнений

Распространенная ситуация: проверка того, что переменная находится в заданных пределах. Можно было бы использовать логический оператор and:

if x <= 5 and x > 20:

Однако Python предоставляет нам синтаксическое удобство, которое выглядит более «математичным». Такая запись и короче, и понятнее:

if 5 <= x < 20:

В качестве операторов сравнения могут быть любые из списка в любых сочетаниях:

">", "<", "==", ">=", "<=", "!=", "is" ["not"], ["not"] "in"

Т.е. запись вида a < b > c вполне законна, хоть и трудна для понимания.

Формально, если мы имеем N операций OP1…OPN и N + 1 выражений (a, b … y, z), то запись вида:

a OP1 b OP2 c … y OPN z 

Это эквивалентно записи:

a OP1 b and b OP2 c and … and y OPN z

📎 Примеры:

x = 5
print(1 < x < 10)  
print(x < 10 < x*10 < 100)  
print(10 > x <= 9)  
print(5 == x > 4)
a, b, c, d, e, f = 0, 5, 12, 0, 15, 15
print(a <= b < c > d is not e is f)

Специально для канала @pyway.

Python: is. Равенство и эквивалентность

is or == picture

Новички часто путаются в конструкциях is и ==. Давайте разберемся, что к чему.

Сразу к сути: == (и его антагонист !=) применяются для проверки равенства (неравенства) значения двух объектов. Значение, это непосредственно то, что лежит в переменной. Значение числа 323235 – собственно число 323235. Тавтология. Но на примерах станет яснее.

Оператор is (и его антагонист is not) применяются проверки равенства (неравенства) ссылок на объект. Сразу отметим то, что на значение (допустим 323235) может быть копировано и храниться в разных местах (в разных объектах в памяти).

>> x = 323235
>> y = 323235
>> x == y
True
>> x is y
False

Видите, значение переменных равны по значению, но они ссылаются на разные объекты. Я не случайно взял большое число 323235. Дело в том, что в целях оптимизации интерпретатор Python при старте создает некоторые количество часто-используемых констант (от -5 до 256 включительно).

Следите внимательно за ловкостью рук:

>>> x = 256
>>> y = 256
>>> x is y
True
>>> x = 257
>>> y = 257
>>> x is y
False
>>> x = -5
>>> y = -5
>>> x is y
True
>>> x = -6
>>> y = -6
>>> x is y
False 

Поэтому новички часто совершают ошибку, считая, что писать == – это как-то не Python-way, а is – Python-way. Это ошибочное предположение может быть раскрыто не сразу.

Python старается кэшировать и переиспользовать строковые значения. Поэтому весьма вероятно, что переменные, содержащие одинаковые строки, будут содержать ссылки на одинаковые объекты. Но это не факт! Смотрите последний пример:

>>> x = "hello"
>>> y = "hello"
>>> x is y
True
>>> x = "hel" + "lo"
>>> y = "hello"
>>> x is y
True
>>> a = "hel"
>>> b = "lo"
>>> x = a + b
>>> y = "hello"
>>> x == y
True
>>> x is y
False

Мы составили строку из двух частей и она попала в другой объект. Python не догадался (и правильно) поискать ее в существующих строках.

Суть is (id)

В Python есть встроенная функция id. Она возвращает идентификатор объекта – некоторое число. Гарантируется, что оно будет различно для различных объектах в пределах одного интерпретатора. В реализации CPython – это просто адрес объекта в памяти интерпретатора.

Так вот:

a is b

Это тоже самое, что:

id(a) == id(b)

И все! Пример для проверки:

>>> x = 10.40
>>> y = 10.40
>>> x is y
False
>>> x == y
True

>>> id(x)
4453475504
>>> id(y)
4453475600
>>> id(x) == id(y)
False

>>> x = y
>>> x is y
True
>>> id(x)
4453475600
>>> id(y)
4453475600

Значения переменных равны, но их id – разные, и is выдает False. Как только мы к x привязали y, то ссылки стали совпадать.

Для чего можно применять is?

Если мы точно знаем уверены, что хотим проверять именно равенство ссылок на объекты (один ли это объект в памяти или разные).

Еще можно применять is для сравнения с None. None – это встроенная константа и двух None быть не может.

>>> x is None
False
>>> x = None
>>> x is None
True

Также для Ellipsis:

>>> ... is Ellipsis
True
>>> x = ...
>>> y = ...
>>> x is y
True

Я не рекомендую применять is для True и False.

Потому что короче писать if x:, чем if x is True:.

Можно применять is для сравнения типов с осторожностью (без учета наследования, т. е. проверка на точное совпадение типов):

>>> x = 10.5
>>> type(x) is float
True

С наследованием может быть конфуз:

>>> class Foo: ...
...
>>> class Bar(Foo): ...
...
>>> f = Foo()
>>> b = Bar()
>>> type(f) is Foo
True
>>> type(b) is Bar
True
>>> type(b) is Foo
False
>>> isinstance(b, Foo)
True

Не смотря на то, что Bar – наследник Foo, типы переменных foo и bar не совпадают. Если нам важно учесть наcледование, то пишите isinstance.

Нюанс: is not против is (not)

Важно знать, что is not – это один целый оператор, аналогичный id(x) != id(y). А в конструкции x is (not y) – у нас сначала будет логическое отрицание y, а потом просто оператор is.

Пример уловки:

>>> x = 10
>>> x is not None
True
>>> x is (not None)
False

Сравнение пользовательских классов

Далее речь пойдет об обычных == и !=. Можно определить магический метод __eq__, который обеспечит поведение при сравнении классов. Если он не реализован, то объекты будет сравниваться по ссылкам (как при is).

>>> class Baz: ...
...
>>> x = Baz()
>>> y = Baz()
>>> x == y
False
>>> x = y
>>> x == y
True

Если он реализован, то будет вызван метод __eq__ для левого операнда.

class Foo:
 def __init__(self, x):
  self.x = x
 def __eq__(self, other):
  print('Foo __eq__ {} and {}'.format(self, other))
  return self.x == other.x

>>> x = Foo(5)
>>> y = Foo(5)
>>> x == y
Foo __eq__ <__main__.Foo object at 0x109e9c048> and <__main__.Foo object at 0x109e8a5c0>
True

Метод __ne__ отвечает за реализацию !=. По умолчанию он вызывает not x.__eq__(y). Но рекомендуется реализовывать их оба вручную, чтобы поведение сравнения было согласовано и явно.

Вопрос к размышлению: что будет если мы сравним объекты разных классов, причем оба класса реализуют __eq__?

Что будет, если мы реализуем __ne__, но не реализуем __eq__?

А еще есть метод __cmp__. Это уже выходит за рамки статьи про is. Почитайте самостоятельно…

Специально для канала @pyway.