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]