5
\$\begingroup\$

After finding a piece of code on StackOverflow that drew the Koch snowflake fractal, I made a ton of modifications to it and used it to produce an animation divided in two parts:

  • Constant size, recursion depth increasing.
  • Constant recursion depth, size increasing.

You can see the animation here.

I am interested mainly in:

  • Have I got too many constants? I like them because they make the program easy to customize, but 12 constants looks like a lot.

  • Any idea to avoid repetition in these blocks of code?

    write_as_title(\
        "Constant size, recursion depth increasing.", font=FONT)
    time.sleep(WRITING_SLEEPING_INTERVAL)
    
    for deepness in range(MAX_DEPTH):
        costum_reset()
        snowflake(deepness, SIZE_WHEN_CONSTANT_DEPTH)
        time.sleep(DRAWING_SLEEPING_INTERVAL)
    

    costum_reset()
    write_as_title(\
        "Constant recursion depth, size increasing.", font=FONT)
    time.sleep(WRITING_SLEEPING_INTERVAL)
    for size in RANGE_OF_SIZES:
        costum_reset()
        snowflake(DEPTH_WHEN_CONSTANT_SIZE, size)
    

    They are similar but I cannot quite abstract the similarity out.


from __future__ import division

import turtle
import time

WIDTH, HEIGHT = 800, 600
FONT = ("Arial", 25, "normal")
DRAWING_SLEEPING_INTERVAL = 0.5
WRITING_SLEEPING_INTERVAL = 2.5
MAX_DEPTH = 5
SIZE_WHEN_CONSTANT_DEPTH = 300
DEPTH_WHEN_CONSTANT_SIZE = 4
RANGE_OF_SIZES = range(0, 5000, 5)
SIDES_OF_AN_EQUILATERAL_TRIANGLE = 3
EQUILATERAL_TRIANGLE_INTERNAL_ANGLE = 120
DRAWING_START_X = - 200
DRAWING_START_Y = 150

def snowflake(n, size=300):

    for _ in range(SIDES_OF_AN_EQUILATERAL_TRIANGLE):
        snowflake_edge(n, size)
        turtle.right(EQUILATERAL_TRIANGLE_INTERNAL_ANGLE)

    turtle.update()

def snowflake_edge(n, size=300):
    if n == 0:
        turtle.forward(size)
        return

    for movement in (lambda: turtle.left(EQUILATERAL_TRIANGLE_INTERNAL_ANGLE // 2),
                   lambda: turtle.right(EQUILATERAL_TRIANGLE_INTERNAL_ANGLE),
                   lambda: turtle.left(EQUILATERAL_TRIANGLE_INTERNAL_ANGLE // 2)):
        snowflake_edge(n - 1, size / SIDES_OF_AN_EQUILATERAL_TRIANGLE)
        movement()

    snowflake_edge(n - 1, size / SIDES_OF_AN_EQUILATERAL_TRIANGLE)

def costum_reset():
    turtle.reset()
    turtle.penup()
    turtle.setx( DRAWING_START_X )
    turtle.sety( DRAWING_START_Y )
    turtle.pendown()

def write_as_title(title, font=FONT):
    turtle.setx(- (WIDTH // 2.5) )
    turtle.sety(HEIGHT // 3)
    turtle.write(title, font=font)

if __name__ == "__main__":
    turtle.setup(width=WIDTH, height=HEIGHT)
    turtle.hideturtle()
    turtle.tracer(0, 0)

    write_as_title(\
        "Constant size, recursion depth increasing.", font=FONT)
    time.sleep(WRITING_SLEEPING_INTERVAL)

    for deepness in range(MAX_DEPTH):
        costum_reset()
        snowflake(deepness, SIZE_WHEN_CONSTANT_DEPTH)
        time.sleep(DRAWING_SLEEPING_INTERVAL)

    costum_reset()
    write_as_title(\
        "Constant recursion depth, size increasing.", font=FONT)
    time.sleep(WRITING_SLEEPING_INTERVAL)
    for size in RANGE_OF_SIZES:
        costum_reset()
        snowflake(DEPTH_WHEN_CONSTANT_SIZE, size)
\$\endgroup\$
5
\$\begingroup\$

Your code reads well and is easily understandable. On top of also finding that you might have too much constant (more on that in a little bit), I just have a few nitpicks:

  • whitespace in expressions is weird sometimes;
  • a docstring would help understanding the recursion involved in snowflake_edge;
  • you don't need the newline continuation (\) inside parenthesis, they already act as an implicit one;
  • you allow to parametrize write_as_title with a custom font but nothing similar is done with other kind of parameters.

On reducing the similarities between the two drawings

I would have put a call to costum_reset (costum? really?) at the beginning of both write_as_title and snowflake since both drawings need a clear state to begin with.

I would also put time.sleep(WRITING_SLEEPING_INTERVAL) at the end of write_as_title. Considering its value, it won't hurt anyone in an interactive session and is still needed in a program for people to be able to read the text.

This way, the main part would look like:

if __name__ == "__main__":
    turtle.setup(width=WIDTH, height=HEIGHT)
    turtle.hideturtle()
    turtle.tracer(0, 0)

    write_as_title(
        "Constant size, recursion depth increasing.", font=FONT)    
    for deepness in range(MAX_DEPTH):
        snowflake(deepness, SIZE_WHEN_CONSTANT_DEPTH)
        time.sleep(DRAWING_SLEEPING_INTERVAL)

    write_as_title(
        "Constant recursion depth, size increasing.", font=FONT)
    for size in RANGE_OF_SIZES:
        snowflake(DEPTH_WHEN_CONSTANT_SIZE, size)

You can even make the two sensible parts two functions if you want to separate the concerns better.

On reducing the amount of constants

Of your 12 constants, a few are really constant, some are arbitrary fixed for the purpose of your application, and some could be tweaked to achieve a better (prettier?) result.

Maybe that allowing to change the latter group using the command line could be more intuitive to tweak the parameters of the display. I personally would keep the following constants:

  • WIDTH, HEIGHT = 800, 600
  • FONT = ("Arial", 25, "normal")
  • WRITING_SLEEPING_INTERVAL = 2.5
  • SIDES_OF_AN_EQUILATERAL_TRIANGLE = 3
  • EQUILATERAL_TRIANGLE_INTERNAL_ANGLE = 120
  • DRAWING_START_X = - 200
  • DRAWING_START_Y = 150

And use argparse to provide default values or custom ones from the command line for:

  • DRAWING_SLEEPING_INTERVAL
  • MAX_DEPTH
  • SIZE_WHEN_CONSTANT_DEPTH
  • DEPTH_WHEN_CONSTANT_SIZE
  • SIZES_LIMIT

Note the use of SIZES_LIMIT which will then create the range object RANGE_OF_SIZES.

All of these last "constants" are only used within the if __name__ == '__main__' so it's pretty easy to adapt.

\$\endgroup\$
2
\$\begingroup\$

Just looking at this function:

def snowflake(n, size=300):
    for _ in range(SIDES_OF_AN_EQUILATERAL_TRIANGLE):
        snowflake_edge(n, size)
        turtle.right(EQUILATERAL_TRIANGLE_INTERNAL_ANGLE)
    turtle.update()
  1. There's no docstring. What does it do? What is the meaning of the arguments?

  2. The name n does not give much of a clue. I would call this argument depth.

  3. The function does two things: it draws the snowflake, and it updates the screen. It is usually best if a function does just one thing: then you have more flexibility about how you use it. For example, what if you wanted to draw several snowflakes before updating the screen?

  4. The constant EQUILATERAL_TRIANGLE_INTERNAL_ANGLE is wrongly named. It has the value 120, but the interior angle of an equilateral triangle is actually 60°. The term you want here is exterior angle.

  5. The function uses two constants, but these are not independent. The exterior angle \$θ\$ of a regular polygon can be computed from the number of sides \$k\$, like this: \$θ = {360°\over k}\$.

  6. One way to reduce the number of constants is to turn them into function parameters. I would write:

    # Number of degrees in a full circle.
    FULLCIRCLE = 360
    
    def snowflake(depth=4, size=300, sides=3):
        """Draw a Koch snowflake fractal.
    
        Arguments:
        depth -- depth of recursion.
        size -- length of each side, in pixels.
        sides -- number of sides of the snowflake.
    
        """
        theta = FULLCIRCLE / sides
        for _ in range(sides):
            snowflake_edge(depth, size)
            turtle.right(theta)
    

    (A global constant for the number of degrees in a full circle is necessary, unfortunately, because Turtle allows you to change this using the degrees function, but doesn't have a documented way of getting the current value.)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.