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)
2 Answers 2
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.
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()
There's no docstring. What does it do? What is the meaning of the arguments?
The name
n
does not give much of a clue. I would call this argumentdepth
.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?
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.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}\$.
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.)