母亲节快到了,送花太普通,那就用代码画一颗爱心树,深刻又有意义。
今天教大家用 Python 画一棵树,树枝末端全是粉色爱心,每颗心上写着"妈妈我爱你"。程序跑起来大概长这样:棕色的树干往上长,枝桠随机分叉,越长越细,最后每根细枝顶端冒出一颗爱心。
代码不长,加上空行也就60多行出头。但里面有两个值得好好说的东西:递归和turtle 画图。
【点击视频预览】
点击预览课件:python节日课《母亲节祝福》代码脚本简单。
整个程序就两个函数加一段主程序。
love(x, y) 负责在指定坐标画一颗爱心。
tree(branchLen, t) 负责递归地画树,树枝末端调用 love() 挂上爱心。
主程序初始化画布和画笔,然后调用 tree(100, t) 启动整棵树的绘制。
deflove(x, y):lv = turtle.Turtle()lv.hideturtle()lv.up()lv.goto(x, y)defcurvemove():foriinrange(20):lv.right(10)lv.forward(2)lv.color('red', 'pink')lv.speed(10000000)lv.pensize(1)lv.down()lv.begin_fill()lv.left(140)lv.forward(22)curvemove()lv.left(120)curvemove()lv.forward(22)lv.write("妈妈\n我爱你", font=("Arial", 10, "normal"), align="center")lv.left(140)lv.end_fill()
爱心的画法是个经典技巧:两段直线 + 两段圆弧拼出来的。
curvemove() 是内部函数,做的事情很简单——右转10度、前进2步,重复20次。这样走出来的路径是一段弧,总共转了200度。两段弧加两段直线,就是爱心的轮廓。
begin_fill() 和 end_fill() 之间画的所有路径,turtle 会自动填充颜色。lv.color('red', 'pink') 里第一个参数是轮廓色,第二个是填充色,所以爱心边框红色、内部粉色。
lv.speed(10000000) 这个数字写得很夸张,实际上 turtle 的速度超过10就是最快,这里只是懒得查最大值,直接写了个大数。
每颗爱心是一个独立的 Turtle 对象,画完就留在那里,不影响主画笔 t 的状态。
deftree(branchLen, t):ifbranchLen>5:ifbranchLen<20:t.color("green")t.pensize(random.uniform((branchLen+5) /4-2, (branchLen+6) /4+5))t.down()t.forward(branchLen)love(t.xcor(), t.ycor())t.up()t.backward(branchLen)t.color("brown")returnt.pensize(random.uniform((branchLen+5) /4-2, (branchLen+6) /4+5))t.down()t.forward(branchLen)ang = random.uniform(15, 45)t.right(ang)tree(branchLen-random.uniform(12, 16), t)t.left(2*ang)tree(branchLen-random.uniform(12, 16), t)t.right(ang)t.up()t.backward(branchLen)
这个函数是整个程序最核心的部分,也是最值得理解的地方。
递归的逻辑是这样的:
画一段树枝,然后在枝头分叉,左边长一根更短的枝,右边也长一根更短的枝。每根新枝又重复同样的过程,直到枝条短到一定程度就停下来,挂上爱心。
用代码说就是:tree() 函数在自己内部调用了两次自己,每次传入的 branchLen 都比当前小12到16(随机),所以树枝越来越短,最终触发终止条件。
两个终止条件:
branchLen <= 5:枝条太短,什么都不画,直接返回。
branchLen < 20:枝条进入"细枝"阶段,颜色换成绿色,画完这段之后调用 love() 挂爱心,然后返回,不再继续分叉。
画完要退回来:
注意每次 t.forward(branchLen) 之后,递归结束时都有 t.backward(branchLen)。这是因为 turtle 画完左边的分支之后,要退回到分叉点,才能去画右边的分支。不退回来,右边的枝就会从错误的位置开始长。
粗细随机:
t.pensize(random.uniform(...)) 让每段树枝的粗细有轻微随机,树看起来更自然,不像用尺子量出来的。
角度随机:
ang = random.uniform(15, 45) 每次分叉的角度在15到45度之间随机取,所以每次运行程序,树的形状都不一样。
myWin = turtle.Screen()t = turtle.Turtle()t.hideturtle()t.speed(1000)t.left(90)t.up()t.backward(200)t.down()t.color("brown")t.pensize(32)t.forward(60)tree(100, t)myWin.exitonclick()
主程序做的事情:创建画布和主画笔,把画笔转向上方(left(90),turtle 默认朝右),从屏幕下方出发,先画一段粗树干(粗细32,长度60),然后调用 tree(100, t) 开始递归画树。
myWin.exitonclick() 让窗口在点击之后才关闭,不然程序跑完画面一闪就没了。
直接运行这个 .py 文件就行,不需要安装额外的库,turtle 是 Python 标准库自带的。
程序跑起来会看到树一点一点长出来,枝条越来越细,最后爱心一颗一颗挂上去。因为用了随机角度和随机粗细,每次跑出来的树形状都不完全一样。
爱心里的文字现在是"妈妈\n我爱你",改成你想写的就行,找到第42行改掉:
lv.write("妈妈\n我爱你", font=("Arial", 10, "normal"), align="center")背景色默认是白色,加一行 myWin.bgcolor("black") 换成黑色背景,爱心会更显眼。
树干初始长度是100,改大树会更高更茂盛,改小树会更矮更稀疏。
这段代码我第一次看的时候,觉得递归部分有点绕——画完左边要退回来再画右边,这个"退回来"的动作很容易忽略。
理解递归有个办法:不要试图在脑子里模拟整棵树的生长过程,那会绕晕。只需要相信:tree(branchLen, t) 这个函数能正确画出一棵长度为 branchLen 的树。然后问自己:如果这个假设成立,那么画完当前这段枝、分叉、调用两次自己,整棵树就画完了吗?答案是对的,递归就理解了。
母亲节快乐。