Насколько чист мой снег?

Я только что написал анимацию снега в 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() немного похож на хак.

Признаюсь! При написании это был дождь, а не снег!

92 голоса | спросил J Atkin 1 FebruaryEurope/MoscowbMon, 01 Feb 2016 18:30:12 +0300000000pmMon, 01 Feb 2016 18:30:12 +030016 2016, 18:30:12

6 ответов


84

Давайте посмотрим на код.

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 не так уж плох.

ответил Sjoerd Job Postmus 1 FebruaryEurope/MoscowbMon, 01 Feb 2016 18:59:23 +0300000000pmMon, 01 Feb 2016 18:59:23 +030016 2016, 18:59:23
45

Прохладная анимация!

Пойдем немного. Согласно 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)
ответил 200_success 2 FebruaryEurope/MoscowbTue, 02 Feb 2016 13:01:00 +0300000000pmTue, 02 Feb 2016 13:01:00 +030016 2016, 13:01:00
29

Вы не получаете drops снега! Очевидно, что это должно быть

  for flake in flurry:
ответил Loofer 1 FebruaryEurope/MoscowbMon, 01 Feb 2016 20:29:39 +0300000000pmMon, 01 Feb 2016 20:29:39 +030016 2016, 20:29:39
22

Я удивлен, что никто не говорил о выборе вашего персонажа! Почему их куча хэштегов падает? Нет, я ребенок, это был хороший выбор характера, но мы можем сделать лучше! Как насчет изменения кода # в unicode ( который поддерживает Python 3! ) . Теперь это действительно похоже на снег!

Кроме того, ваш код на данный момент обратно совместим с Python 2. Мое изменение характера нарушит это. Если вы хотите исправить это, добавьте:

# -*- coding: utf8 -*-

В начало файла.

ответил Dair 3 FebruaryEurope/MoscowbWed, 03 Feb 2016 11:21:26 +0300000000amWed, 03 Feb 2016 11:21:26 +030016 2016, 11:21:26
17
def createRandomDrops():
    dropCount = 4
    for i in range(dropCount):

Магическое число 4 в середине кода

def drawDrops():

Я бы ожидал, что этот метод действительно будет рисовать капли, а не возвращать строку для печати

ответил Hurda 2 FebruaryEurope/MoscowbTue, 02 Feb 2016 01:20:50 +0300000000amTue, 02 Feb 2016 01:20:50 +030016 2016, 01:20:50
6
from random import randrange

Я бы предложил использовать

import random

Затем в вашем коде используйте random.randrange. В вашем конкретном случае, возможно, это не имеет особого значения, но я нашел для себя, что это хорошее правило для импорта модуля, когда это возможно, вместо имен из него.

https://google.github.io/styleguide/pyguide.html#Imports

  

Используйте import только для пакетов и модулей.

Импорт модулей иногда помогает предотвратить циклические ошибки импорта.

Это помогает импортировать слишком много имен, когда ваш модуль растет.

Это помогает вам решать конфликты имен. Например, несколько модулей имеют класс ValidationError (mongoengine, wtforms и т. Д.), И ваш код использует их все.

ответил warvariuc 3 FebruaryEurope/MoscowbWed, 03 Feb 2016 15:19:58 +0300000000pmWed, 03 Feb 2016 15:19:58 +030016 2016, 15:19:58

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132