Насколько чист мой снег?
Я только что написал анимацию снега в Python. Я думаю, что это довольно чисто, но у меня есть несколько вещей, которые мне не нравятся.
from random import randrange
import time
# Snow animation
# Snow is simply the `#` symbol here. Half of the snow moves at 1 char
# per frame, while the other half moves at 0.5 chars per frame. The
# program ends when all the snow reaches the bottom of the screen.
# The viewing area is 80x25. It puts 100 snow flakes to output, half
# fast, and half slow. Every frame it dispenses 4 flakes, 2 fast and
# 2 slow ones, at random locations at the top of the viewing area.
screen = {'x': 80, 'y': 20}
drops = []
def createRainDrop(x, y, speed):
return {'x': x, 'y': y, 'speed': speed}
def createRandomDrops():
dropCount = 4
for i in range(dropCount):
yield createRainDrop(randrange(0, screen['x']), 0, min((i % 2) + 0.5, 1))
def moveDrops():
for drop in drops:
speed = drop['speed']
drop['y'] = drop['y']+speed
def drawDrops():
out = [''] * screen['y']
for y in range(screen['y']):
for x in range(screen['x']):
out[y] += '#' if any([drop['x'] == x and int(drop['y']) == y for drop in drops]) else ' '
return '\n'.join(out)
def dropsOnScreen():
return any([drop['y'] < screen['y'] for drop in drops])
drops += createRandomDrops()
while dropsOnScreen():
if len(drops) < 100:
drops += createRandomDrops()
print(drawDrops())
moveDrops()
time.sleep(0.100)
Например, я не знаю, как удалить дублируемую строку drops += createRandomDrops()
, а drawDrops()
немного похож на хак.
Признаюсь! При написании это был дождь, а не снег!
6 ответов
Давайте посмотрим на код.
from random import randrange
import time
Ваш импорт очень минимальный! Хорошо.
# Snow animation
# Snow is simply the `#` symbol here. Half of the snow moves at 1 char
# per frame, while the other half moves at 0.5 chars per frame. The
# program ends when all the snow reaches the bottom of the screen.
# The viewing area is 80x25. It puts 100 snow flakes to output, half
# fast, and half slow. Every frame it dispenses 4 flakes, 2 fast and
# 2 slow ones, at random locations at the top of the viewing area.
Это больше похоже на docstring для меня. Было бы неплохо сделать это как таковое. Вы можете сделать это, отбросив знаки #
и окружив их в кавычки """
.
screen = {'x': 80, 'y': 20}
drops = []
Глобальные переменные не так уж хороши. Но это простой файл, так что, возможно, мы можем оставить его таким, как сейчас? Давайте.
def createRainDrop(x, y, speed):
return {'x': x, 'y': y, 'speed': speed}
Я думаю, что что-то вроде класса было бы лучше для этого. Попробуем
class RainDrop(object):
def __init__(self, x, y, speed):
self.x = x
self.y = y
self.speed = speed
Конечно, теперь нам нужно заменить createRainDrop(...)
на RainDrop(...)
и drop['...']
с drop....
.
def createRandomDrops():
dropCount = 4
for i in range(dropCount):
yield RainDrop(randrange(0, screen['x']), 0, min((i % 2) + 0.5, 1))
Это лучше.
def moveDrops():
for drop in drops:
drop.y = drop.y + drop.speed
Мы изменяем drop
здесь, вместо того, чтобы просить его модифицировать себя. Мы должны написать что-то вроде drop.moveDown()
здесь, или, может быть, drop.tick()
('tick' - это то, что обычно используется для уведомления о событии о продвижении вперед время).
def drawDrops():
out = [''] * screen['y']
for y in range(screen['y']):
for x in range(screen['x']):
out[y] += '#' if any([drop.x == x and drop.y == y for drop in drops]) else ' '
return '\n'.join(out)
Здесь, для всех позиций на экране, вы перебираете все капли. В идеале мы бы включили это:
def drawDrops():
out = [[' ' for _ in range(screen['x'])] for _ in range(screen['y'])]
for drop in drops:
if int(drop.y) < screen['y']:
out[int(drop.y)][drop.x] = '#'
Теперь это немного быстрее и чище.
def dropsOnScreen():
return any([drop.y < screen['y'] for drop in drops])
Имеет смысл. Кроме того, я предлагаю не использовать [...]
, который создает список. Лучше использовать
def dropsOnScreen():
return any(drop.y < screen['y'] for drop in drops)
Это ведет себя одинаково, но не нужно создавать промежуточный список.
drops += createRandomDrops()
while dropsOnScreen():
if len(drops) < 100:
drops += createRandomDrops()
print(drawDrops())
moveDrops()
time.sleep(0.100)
Вы хотите избавиться от дублированного вызова drops += createRandomDrops()
.
while True:
if len(drops) < 100:
drops += createRandomDrops()
if not dropsOnScreen():
break
print(drawDrops())
moveDrops()
time.sleep(0.100)
Но, на мой взгляд, дополнительный createRandomDrops
не так уж плох.
Прохладная анимация!
Пойдем немного. Согласно PEP 8 , вы должны последовательно использовать 4 пространства отступа, а имена функций должны быть snake_case
.
Масштабируемость
Основной недостаток вашего дизайна - масштабируемость. Если вы продлеваете цикл для неограниченного выполнения, то в конечном итоге вы столкнетесь с проблемами производительности.
Одна из проблем заключается в том, что drops
list растет с каждой итерацией и никогда не обрезается. Капли не исчезают после падения на землю; они продолжают падать навсегда, невидимо, вне экрана. Решение состоит в том, чтобы moveDrops()
удалять капли, когда они выходят за нижнюю. (Это более разумная стратегия, чем dropsOnScreen()
пересмотреть каждый кадр на каждом кадре анимации.)
Кроме того, чтобы поместить капли в сетку, вы выполняете сканирование O ( n ) для каждой позиции на экране с помощью '#' if any([drop['x'] == x and int(drop['y']) == y for drop in drops])
. Я бы переписал drawDrops()
, чтобы каждый кадр помещал себя, используя словарь или двухмерный массив. Я также предпочел бы использовать понимание, чем повторные операции добавления, но это в основном предпочтение стиля.
Типы данных
В вашем комментарии говорится, что размеры экрана составляют 80Ã-25, но ваш код говорит screen = {'x': 80, 'y': 20}
. В идеале размеры должны быть обнаружены во время выполнения с использованием библиотеки curses
. Поскольку screen
используется как переменная global , я хотел бы увидеть ее с именем SCREEN
и сделать неизменным. A namedtuple
сделает его неизменным, с дополнительным преимуществом, позволяющим использовать точечный аксессуар, а не неуклюжую ноту []
. Я думаю, что width
и height
будут более подходящими именами, чем x
и y
.
Аналогично, определение класса для капель дождя позволит избежать обозначения drop['x']
. Кроме того, функция createRainDrop()
кричит как конструктор.
Создание капель и циклов
Остальная часть кода - это упражнение в итерации Pythonic. Все может быть обработано с использованием итераторов с либеральным использованием.
В createRandomDrops()
вместо критической формулы min((i % 2) + 0.5, 1)
используйте itertools.cycle([0.5, 1])
. Я бы превратил createRandomDrops()
в бесконечный генератор.
В приведенном ниже решении параметры, такие как скорость, интенсивность и продолжительность, централизованно настраиваются , изменяя drop_params
и precipitation
. Например, precipitation = drop_generator(**drop_params)
приведет к бесконечному циклу с одним новым падением на кадр.
Предлагаемое решение
from collections import namedtuple
import curses
from itertools import chain, cycle, islice, repeat
from random import randrange
import sys
import time
SCREEN = namedtuple('Screen', 'height width')(*curses.initscr().getmaxyx())
curses.endwin()
class Raindrop:
def __init__(self, x, y, speed):
self.x, self.y, self.speed = x, y, speed
def drop_generator(batch_size=1, **drop_params):
while True:
yield [
Raindrop(**{key: next(gen) for key, gen in drop_params.items()})
for _ in range(batch_size)
]
def move_drops(drops):
"""Move each drop down according to its speed, and remove drops from the
set that have fallen off."""
for drop in drops:
drop.y += drop.speed
drops.difference_update([drop for drop in drops if drop.y >= SCREEN.height])
def render_drops(drops, char='#'):
"""Return a string representation of the entire screen."""
scr = {
int(drop.y) * SCREEN.width + int(drop.x): char for drop in drops
}
return '\n'.join(
''.join(scr.get(y * SCREEN.width + x, ' ') for x in range(SCREEN.width))
for y in range(SCREEN.height)
)
drop_params = {
'x': (randrange(0, SCREEN.width) for _ in repeat(True)),
'y': repeat(0),
'speed': cycle([0.5, 1]),
}
precipitation = chain.from_iterable([
islice(drop_generator(batch_size=4, **drop_params), 25),
repeat([]) # ... then generate nothing as existing drops keep falling
])
drops = set(next(precipitation))
while drops:
drops.update(next(precipitation))
print(render_drops(drops))
# Python 2.7 seems to have a curses bug that necessitates flushing
sys.stdout.flush()
move_drops(drops)
time.sleep(0.100)
Вы не получаете drops
снега! Очевидно, что это должно быть
for flake in flurry:
Я удивлен, что никто не говорил о выборе вашего персонажа! Почему их куча хэштегов падает? Нет, я ребенок, это был хороший выбор характера, но мы можем сделать лучше! Как насчет изменения кода #
в unicode ( который поддерживает Python 3! ) ❄
. Теперь это действительно похоже на снег!
Кроме того, ваш код на данный момент обратно совместим с Python 2. Мое изменение характера нарушит это. Если вы хотите исправить это, добавьте:
# -*- coding: utf8 -*-
В начало файла.
def createRandomDrops():
dropCount = 4
for i in range(dropCount):
Магическое число 4 в середине кода
def drawDrops():
Я бы ожидал, что этот метод действительно будет рисовать капли, а не возвращать строку для печати
from random import randrange
Я бы предложил использовать
import random
Затем в вашем коде используйте random.randrange
. В вашем конкретном случае, возможно, это не имеет особого значения, но я нашел для себя, что это хорошее правило для импорта модуля, когда это возможно, вместо имен из него.
https://google.github.io/styleguide/pyguide.html#Imports
Используйте
import
только для пакетов и модулей.
Импорт модулей иногда помогает предотвратить циклические ошибки импорта.
Это помогает импортировать слишком много имен, когда ваш модуль растет.
Это помогает вам решать конфликты имен. Например, несколько модулей имеют класс ValidationError
(mongoengine
, wtforms
и т. Д.), И ваш код использует их все.