Misc
Увлекательная симметрия с Черепашкой (Oct 17)
Как вы думаете, что нарисует следующий алгоритм?
- Рисуем равносторонний треугольник.
- Рисуем точку в любом месте внутри треугольника.
- Следующую точку рисуем посередине между текущей точкой и случайно выбранной вершиной треугольника.
- Бесконечно долго повторяем последний шаг (3).
Постепенно будет создаваться фрактал из вложенных треугольников.
Давайте попробуем отобразить это на Python. Для графики используем весёлый модуль turtle (черепашка).
Это та самая Черепашка, с которой дети учатся программированию.
Черепашка может делать следующие операции:
down
/up
: опустить или поднять хвостleft
/right
: повернуться налево или направоforward
: прыгнуть вперёдsetpos
: прыгнуть в определенную позицию
Главное, что если черепашка перемещается с опущенным хвостом, то она оставляет след (рисует).
Начнём писать код.
Импортируем черепашку (turtle) и модуль работы со случайными числами (random) —нужно выбирать вершину случайным образом):
import random, turtle
side = 600
vertexes = []
side
— длинна стороны треугольника, а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.
begin_fill
/end_fill
— позволяют залить треугольник неким цветом — это необязательно, можно эти вызовы убрать.
До входа в бесконечный цикл создаём три переменных:
total_dots
— для подсчёта нарисованных точек (это необязательно).x
,y
— положение текущей точки (начнём с 0,0)
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
randint(0, 2)
генерирует случайное целое число от 0 до 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)
Для демонстрации некоторых старых и новых возможностей, напишем функции работы с арифметическими выражениями, представленными в виде вложенных списков.
Примеры:
[+, 10, 20, 30]
представляет(10 + 20 + 30)
, что равно: 60.[*, 5, 10, [+, 10, 20]]
, что означает:(5 * 10 * (10 + 20))
, и это равно: 1500.
Первым элементом списка идёт операция (+
, *
, можно ещё ввести: 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]