Dream on Python

Posts About Python & Programming

Misc

Увлекательная симметрия с Черепашкой (Oct 17)

Как вы думаете, что нарисует следующий алгоритм?

  1. Рисуем равносторонний треугольник.
  2. Рисуем точку в любом месте внутри треугольника.
  3. Следующую точку рисуем посередине между текущей точкой и случайно выбранной вершиной треугольника.
  4. Бесконечно долго повторяем последний шаг (3).

Постепенно будет создаваться фрактал из вложенных треугольников.

Давайте попробуем отобразить это на Python. Для графики используем весёлый модуль turtle (черепашка).

Это та самая Черепашка, с которой дети учатся программированию.

Черепашка может делать следующие операции:

Главное, что если черепашка перемещается с опущенным хвостом, то она оставляет след (рисует).

Начнём писать код.

Импортируем черепашку (turtle) и модуль работы со случайными числами (random) —нужно выбирать вершину случайным образом):

import random, turtle

side = 600
vertexes = []

Устанавливаем максимальную скорость, цвет хвоста и заливки, а также прыгаем в начальную позицию:

turtle.speed(0)
turtle.color('red', 'yellow')
turtle.up()
turtle.setpos(-side / 2, side / 2)
turtle.down()

Теперь нарисуем треугольник, и запомним в списке vertexes все его вершины:

turtle.begin_fill()

for _ in range(3):
    turtle.forward(side)
    turtle.right(120)
    vertexes.append(turtle.pos())

turtle.end_fill()

Три раза делам следующее: прыгаем вперёд (вдоль стороны треугольника), поворачиваемся на 120 градусов (создаём внутренний угол: 180 - 120 = 60), и запоминаем вершину треугольника в vertexes.

До входа в бесконечный цикл создаём три переменных:

total_dots = 0
x = y = 0
while True:
    v_x, v_y = vertexes[random.randint(0, 2)]
    x = (x + v_x) / 2
    y = (y + v_y) / 2

Далее в том же цикле синим цветом рисуем точку с диаметром 3:

    turtle.up()
    turtle.setpos(x, y)
    turtle.down()
    turtle.dot(3, 'blue')

Обновляем статистику нарисованных точек и выводим в консоль каждую тысячу (это необязательно делать).

    total_dots += 1
    if total_dots % 1000 == 0:
        print(f'total dots drawn: {total_dots}')

Всё! Запускам с PyCharm и ждём, минут 5, а можно и часик…. К 10,000 точек картинка получается очень даже ничего так.

Медитация!

Code: https://onlinegdb.com/qrT12YxnT


Что нового в Python 3.10 (May 22)

Для демонстрации некоторых старых и новых возможностей, напишем функции работы с арифметическими выражениями, представленными в виде вложенных списков.

Примеры:

Первым элементом списка идёт операция (+, *, можно ещё ввести: min, max, и т.д.), далее следуют операнды, к которым применяется эта операция. Причём каждый операнд может представлять тоже арифметическое выражение, представленное списком.

Начнём с класса, который определяет допустимые операторы (+, *, min, max).

class OP:
    def __init__(self, name: str, apply: Callable[[Iterable], Any]):
        self.name = name
        self.apply = apply

    def __repr__(self) -> str:
        return self.name
class Ops:
    ADD = OP('+', sum)
    MUL = OP('*', 
             lambda values: functools.reduce(operator.mul, values, 1))
    MIN = OP('min', min)
    MAX = OP('max', max)

Можно было бы обойтись и без OP, Ops, а вместо ADD, MUL, MIN, MAX, использовать '+', '-', 'min', 'max'. Но наш подход несколько снижает вероятность сделать ошибки в описании выражений.

В качестве рабочего арифметического выражения возьмём следующее:

expr = [
    Ops.MUL,
    5,
    20,
    5,
    [Ops.ADD, 10, 20],
    [Ops.MIN, 5, 15, 25]
]

Оно представляет: (5 * 20 * 5 * (10 + 20) * min(5, 15, 25), что равно 75000.

Функцию, которая вычисляет такие выражения, написать несложно:

def calc(expr: Sequence|int|float) -> int|float:
    try:
        return expr[0].apply(
            calc(expr[i]) for i in range(1, len(expr))
        )
    except TypeError:
        return expr

Напомним, что expr[0] содержит оператор (например, Ops.ADD), если конечно expr ⏤ это Sequence (list является Sequence).

Функция производит вычисления рекурсивно ⏤ это естественный подход, поскольку выражения тоже определены рекурсивно.

Для каждого операнда, функция вызывает сама себя. Если операнд оказался не Sequence (а имеет простой тип: int, float), то генерируется ошибка, которая обрабатывается в try-except. Простой операнд возвращается как есть (return expr).

В Python 3.10 было введено обозначения для объединения типов: int|float, что означает: int или float. В предыдущих версиях Python пришлось бы писать так: Union[int, float].

Пробуем вычислить выражение:

print(f'{expr=}')
print(f'{calc(expr)=}')

Output:

expr=[*, 5, 20, 5, [+, 10, 20], [min, 5, 15, 25]]
calc(expr)=75000

На удивление написать функцию, которая переводит такие выражения в обычный формат несколько сложнее:

def to_str(expr: Sequence|int|float) -> str:
    try:
        op = expr[0]
        match op:
            case OP(name='+'|'*', apply=_):
                joiner = f' {op} '
                maybe_func = ''
            case _:
                joiner = ', '
                maybe_func = f'{op}'
        return f'{maybe_func}({joiner.join(to_str(expr[i]) for i in range(1, len(expr)))})'
    except TypeError:
        return str(expr)

Принцип тот же: рекурсия пробегается по операндам. Но тут мы используем новую конструкцию match-case (введена в Python 3.10).

Заметим, что [+, 5, 10] и [min, 5, 10] должны отобразиться совершенно по-разному: (5 + 10) против min(5, 10). Поэтому используются ветки case. В конце концов сводим к вызову joiner.join(to_str(expr[i]) for i in range(1, len(expr))).

Вызов str.join() объединяет все операнды, или через оператор (например: ' + ') или запятыми (', ').

Команда match-case ⏤ это расширенный (более читаемый?) вариант старой доброй команды: if-elif-else.

Поскольку to_str(expr) ⏤ это правильное арифметическое выражение, его можно посчитать используя стандартную функцию: eval(). Удобно этим воспользоваться, чтобы проверить, что to_str и calc ⏤ выдают согласованные результаты.

print(f'{to_str(expr)=}')
print(f'{eval(to_str(expr))=}')
print(f'{calc(expr)==eval(to_str(expr))=}')

Output:

to_str(expr)='(5 * 20 * 5 * (10 + 20) * min(5, 15, 25))'
eval(to_str(expr))=75000
calc(expr)==eval(to_str(expr))=True

Code: [https://onlinegdb.com/Q9M1jLNIo]