开头先不写公式,我先说一个很多初学者在第一遍会踩进去的坑。
Python 海龟绘图 100 题里的“钝角三角形”,看起来只比等边三角形多了“一个角大于 90°”,但真用turtle画的时候,很多人会下意识照搬等边三角形的写法:画三条边,每次转同一个角度。结果要么画出来不是三角形,要么图形是对的,代码里角度却根本解释不通。
真正的问题不是forward和left不会用,而是你不知道海龟转弯时到底应该转哪个角。第 12 题的难度不在命令层,而在几何层:你得把“一个钝角三角形”翻译成“海龟依次走的方向和旋转量”。更准确说,这道题考察的是三件事:
- 选一个方便计算的顶点顺序。
- 算清楚每个内角对应的外角是多少。
- 知道旋转方向是左转还是右转,才不会绕出多余路径。
这篇文章会把这三件事拆开讲透,顺便给你一套以后画任意三角形都能用的判断和调试路径。
1. 先别急着画三角形,先问自己:题目要你画的到底是图形还是路径
turtle的核心不是“画一个三角形”,而是“让一支笔按一个路径走完”。同一个三角形,顶点顺序不同,代码里的角度和方向就会不同。如果不先理解这一点,后面所有参数都会变成试出来的魔法数字。
1.1 海龟画图不是按“脑内图形”执行,而是按“方向变化量”执行
在纸上画三角形,你可以先从任何一条边开始,甚至可以一笔画完。海龟也一样,它只有两个基本动作:
forward(distance):沿当前方向前进。left(angle)或right(angle):在原地改变前进方向。
所以,当你看到“钝角三角形”这个题时,不能只想着“三个顶点在哪里”,而要想着“海龟从哪个顶点出发,第一步朝哪个方向走,走到下一个顶点后,原地面向要转动多少度,然后继续走”。
我见过很多同学,事先在纸上画好了三角形,也标出了三个内角,然后写代码时直接让海龟在每个顶点转“内角”。比如一个三角形内角是 120°、30°、30°,有人就写:
t.forward(100) t.left(30) # 错误示范 t.forward(100) t.left(120) # 错误示范 t.forward(100)这样画出来的图形,大概率不会回到起点。原因很简单:海龟在顶点处转的角度不是这个顶点切出来的内部夹角,而是“当前前进方向”与“下一段前进方向”之间的夹角,也就是外角。
1.2 第 12 题为什么要拿钝角三角形当“分水岭”
等边三角形容易误导人。因为等边三角形的每个内角都是 60°,而画等边三角形时海龟每次转 120°。120° 正好是 180° 减去 60°,也就是外角。
初学者很容易产生一个错觉:画三角形就是转 120°,转三次即可。这个结论只对等边三角形成立。到了钝角三角形,三个内角不相等,外角也不相等。如果你还按“转同样的角度”去写,图形一定乱。
更关键的是,钝角三角形里有一个大于 90° 的内角。比如 120° 的内角,对应的外角只有 60°;而两个 30° 的内角,对应的外角却是 150°。这个“大内角配小外角、小内角配大外角”的反直觉关系,只有在亲手绘制时才会真正记住。
所以,第 12 题看似只是“画一个钝角三角形”,实际是在帮你建立一条转换链:
- 已知三角形角度 → 计算每个顶点的外角。
- 已知每条边的方向 → 判断海龟在顶点处该左转还是右转。
- 转完三次之后 → 检查海龟是否回到起点,并且总旋转量是否等于 360°。
2. 核心计算:内角、外角、转弯角,三者的关系
现在进入正题。画钝角三角形前,先拿一组好算的参数做例子。
2.1 一个典型的钝角三角形参数
常见的演示三角形是“120°,30°,30°”的等腰钝角三角形:
- 钝角顶角 A = 120°
- 两个底角 B = C = 30°
- 两条腰 AB = AC = 100
- 底边 BC 的长度可以用余弦定理计算
余弦定理写作:
a² = b² + c² - 2bc × cos(A)这里 A 是顶角 120°,b 和 c 都是腰长 100,所以底边:
a² = 100² + 100² - 2 × 100 × 100 × cos(120°) cos(120°) = -0.5 a² = 10000 + 10000 + 10000 a² = 30000 a ≈ 173.2也就是说,图形可以定义为:
- 底边 BC ≈ 173.2
- 两条腰都是 100
- 两条腰从底边两端出发,在中间上方汇聚成 120° 的钝角
这组参数的优点是:角度简单、边长只有一个小数,计算过程干净;缺点是如果你手写173.2,最后闭合时会有一点误差。这在海龟绘图中通常可以接受,但我会建议用 Python 的math.sqrt(30000)来算,避免手工取近似值累积误差。
2.2 海龟从底边开始走的外角怎么算
设三个顶点依次为 B、C、A,其中:
- B 是左下角,内角 30°
- C 是右下角,内角 30°
- A 是上方的钝角顶点,内角 120°
绘制顺序选择B → C → A → B,也就是先画底边,再画左边那条腰,最后画右边那条腰回到起点。为什么这样选?因为在屏幕上海龟默认朝右,从 B 到 C 正好是正东方向,第一段不用额外转向,最容易观察。
在 B 点,海龟初始方向是东,也就是 0°。
在 C 点,内角是 30°。下一段海龟要从 C 走向 A,方向是左上。从“正东”转到“左上”,大约需要向左转 150°。这个 150° 不是 C 的内角,而是 C 的外角:
外角 = 180° - 内角 = 180° - 30° = 150°在 A 点,海龟当前方向是左上,也就是从 C 到 A 的方向。下一段要从 A 走回 B,方向变成左下。从左上转到左下,大约需要向左转 60°。A 的内角是 120°,所以外角:
外角 = 180° - 120° = 60°在 B 点,这一步实际上已经回到了起点。如果希望海龟回到 C 方向继续画下一轮,还要再转一次,从“左下”转回“正东”,需要左转 150°。对应 B 的内角 30°,外角也是 150°。
三次外角加起来:
150° + 60° + 150° = 360°这就是验证规则:一个凸多边形,沿相同方向绕一圈,外角和一定是 360°。反过来说,如果代码里的三个转角加起来不是 360°,几乎可以肯定要么内角算错,要么漏了一次转角。
2.3 转角方向:为什么示例里全是左转
画这个三角形时,我建议全部用left,也就是逆时针绕行。绕行顺序B → C → A → B是一个逆时针方向,所以在每个顶点都左转。这样最容易检查:
- 方向变化量:每次都是左转外角。
- 总旋转量:三次外角相加为 360°。
- 闭合条件:三次完成之后海龟正好回到起点。
如果你用顺时针绕行,也没问题,只要把所有left换成right,外角公式依然成立。真正怕的是混着转:一会儿左一会儿右,出来的图形会非常难排查。
import turtle import math base = math.sqrt(30000) # ≈ 173.2 side = 100 t = turtle.Turtle() t.speed(3) # 路径: B -> C -> A -> B t.forward(base) # B 到 C t.left(150) # C 点外角 t.forward(side) # C 到 A t.left(60) # A 点外角 t.forward(side) # A 到 B,回到起点 t.hideturtle() turtle.done()这段代码的执行顺序很清楚。海龟从原点出发画底边,接着转到左上画左腰,再转到左下画右腰。第三次forward(side)完成后,海龟会恰好回到起点,但“恰好”依赖第三段长度精确等于 100。实际运行时,如果底边用了173.2而不用math.sqrt,最后可能会有 0.1 像素级别的偏差,肉眼看不出来,但长期写复杂图形时会积累。
3. 三种画钝角三角形的实现思路
第一种思路是按“外角 + 边长”写,适合题目里直接给角度。第二种思路是按坐标定位写,适合图形顶点坐标更容易算出的场景。第三种思路是把绘图封装成可复用函数,适合以后需要画多组三角形的场景。
3.1 思路一:按“底边开始 + 外角”绘制
这就是上面那段代码。它适合底边是水平方向、两个底角已知的情况。优点是逻辑清楚,每一步都在检查海龟方向;缺点是你得先把三个内角算出来,再把每个内角都换算成外角。
如果要改进,可以先定义变量:
angle_b = 30 angle_a = 120 angle_c = 30 turn_b = 180 - angle_b turn_c = 180 - angle_c turn_a = 180 - angle_a然后再把它们填进前进和旋转流程里。这样代码的可读性会好很多,别人看代码时能直接对照几何图。
3.2 思路二:用顶点坐标画,先算点,再连线
有些题目会给三个顶点的坐标,比如:
- B 点:(0, 0)
- C 点:(173.2, 0)
- A 点:(86.6, 50)
注意,这里 A 的纵坐标怎么来的?在底边 BC 长为 173.2、腰长为 100 的等腰三角形里,底边中点到 A 的高度可以这样算:
A 点横坐标 = 86.6 A 点纵坐标 = sqrt(100² - 86.6²)约等于 50。这种场景下,你不需要关心外角,直接把三个坐标给海龟:
import turtle import math base = math.sqrt(30000) side = 100 height = math.sqrt(side * side - (base / 2) ** 2) points = [ (0, 0), (base, 0), (base / 2, height), (0, 0), ] t = turtle.Turtle() t.penup() t.goto(points[0]) t.pendown() for x, y in points[1:]: t.goto(x, y) t.hideturtle() turtle.done()这种方案用goto替代forward + left,表面上看“不是海龟画图”,但它在很多复杂图形里反而更稳:不需要判断转角方向,只需要把坐标算对。缺点是你需要具备一定解析几何能力,不适合只想快速检验角度的场景。
3.3 思路三:封装成函数,让不同边长、不同顶角都能画
题目如果只固定“钝角三角形”,没有指定具体角度,那么最好写一个通用函数。函数的输入可以是“腰长 + 钝角度数”,也可以直接传三个边长,先通过余弦定理算出角度,再画。
下面是一个较强健的示例结构:
import turtle import math def draw_isosceles_triangle(t, side, base_angle=30): """ 绘制等腰钝角三角形。 顶角 = 180 - 2 * base_angle """ apex_angle = 180 - 2 * base_angle if not (0 < base_angle < 90 and 90 < apex_angle < 180): print("这不是一个钝角三角形,请重新输入") return base = 2 * side * math.cos(math.radians(base_angle)) turn_at_base = 180 - base_angle turn_at_apex = 180 - apex_angle t.forward(base) t.left(turn_at_base) t.forward(side) t.left(turn_at_apex) t.forward(side)这个函数的关键在于先用条件判断了一次:base_angle小于 90°,且算出来的顶角apex_angle大于 90°,才允许继续画。否则用户可能传入错误角度,画出一个锐角三角形或异常图形。
这里要提醒一点:函数内部使用了math.cos,而turtle.left()接收的是角度,所以 Python 会把角度转成弧度给math, 但传给turtle时仍然用度。很多新手在这里容易混:三角函数要求弧度,海龟转向要求角度。
4. 从“能画出来”到“能验证出来”:三角形条件判断与钝角判断
画完一个三角形,只是第一步。更常见的问题是:题目只给三条边长,但并没有告诉你它是不是钝角三角形。如果你直接拿任意三条边去画,可能画不出来,或者画出来后已经越界。
4.1 三边到底能不能构成三角形
三角形存在的必要条件:任意两边之和大于第三边。这个条件在直接绘图中常被忽略,但它是避免“画着画着发现闭合不上”的第一道防线。
用函数写就是:
def can_form_triangle(a, b, c): return a + b > c and a + c > b and b + c > a如果这个函数返回False,那就不用画了。因为后面无论你怎么调角度,都不可能组成一个闭合三角形。
4.2 如何判断一个三角形是不是钝角三角形
判断方法不是看三条边谁长,而是找最大边。假设三条边是a <= b <= c,最大边是c,那么用勾股定理的推广来判断:
- 如果
a² + b² < c²,这个三角形是钝角三角形。 - 如果
a² + b² = c²,是直角三角形。 - 如果
a² + b² > c²,是锐角三角形。
理由可以从余弦定理推出来。最大边c对应的角是最大角。如果最大角大于 90°,那么cos(最大角) < 0,由余弦定理可以得到:
c² > a² + b²所以,第 12 题如果要画“任意三边长构成的钝角三角形”,绘图流程应该是:
- 判断三边是否满足三角形不等式。
- 排序三边,找到最大边。
- 用最大边长平方与另外两边平方和对比,确认是钝角。
- 根据边长用余弦定理算出三个内角。
- 再把内角转换成外角,交给海龟绘制。
4.3 一个“先验证、再绘图”的完整框架
我建议把代码拆成两层:一层负责几何验证,一层负责绘图。几何验证和画面完全解耦,方便以后复用,也方便写单元测试。
import math import turtle def classify_triangle(a, b, c): edges = sorted([a, b, c]) x, y, z = edges if not (x + y > z): return "not_triangle" diff = x * x + y * y - z * z if diff < 0: return "obtuse" if diff == 0: return "right" return "acute" def compute_angles(a, b, c): # 返回三个内角,单位度 A = math.degrees(math.acos( (b * b + c * c - a * a) / (2 * b * c) )) B = math.degrees(math.acos( (a * a + c * c - b * b) / (2 * a * c) )) C = 180 - A - B return A, B, C def draw_triangle_with_sides(a, b, c): kind = classify_triangle(a, b, c) if kind != "obtuse": print(f"当前输入不是钝角三角形,分类结果是: {kind}") return A, B, C = compute_angles(a, b, c) # 选择顺序: a 边对应 A 角,先画 a 边 t = turtle.Turtle() t.speed(4) t.forward(a) t.left(180 - B) t.forward(c) # 因为 C 角对 c 边,需要调一下对应关系 t.left(180 - C) t.forward(b) t.hideturtle()这段代码的目的不是让你一步不漏地背下来,而是提醒你注意“边和角的对应关系”。很多画三角形失败的真实原因,不是不会用turtle,而是把角度和边对应错了。比如先画了一条边a,在它旁边的角应该是B或C,而不是边a对的那个角。
5. 常见错误与排查链路:角度、方向、坐标、屏幕边界
画钝角三角形如果画不出来,先不要急着改代码,按照下面顺序排查,大部分问题能在 5 分钟内定位。
5.1 先检查转向角是不是外角
最典型的错误是直接写了内角:
t.forward(100) t.left(120) # 如果这里想画 120 度内角,实际会转错 t.forward(100)在逆时针绕行时,内角为 120° 的顶点,对应的外角是 60°,不是 120°。你可以在每个顶点打印当前t.heading(),看看转完之后方向是否和预想一致。
5.2 再检查三次外角之和是不是 360°
无论你画的是钝角三角形还是任意三角形,只要沿同一个方向绕一圈,所有外角加起来必须等于 360°。如果不等,大概率是某一个顶点少算了一次转弯,或者把某个内角直接当外角用了。
你可以在代码里加上打印:
turn1 = 150 turn2 = 60 turn3 = 150 print(turn1 + turn2 + turn3)看到360后,再继续画图。这个方法虽然简单,却能过滤掉大量低级错误。
5.3 确认第三段能不能闭合
有些同学画三角形时,只画两条边,然后在心里认为封闭了。其实海龟只执行你写的命令,不会自动知道“我应该回到起点”。所以要让海龟走第三条边,即使第三条边的长度非常短。
如果第三边长度算错,三角形虽然看起来像,但最后会出现一个缺口。这时要回去用余弦定理重新算第三边长度,而不是手工调屏幕上的终点坐标。
5.4 最后检查屏幕边界、初始位置和画笔状态
钝角三角形如果腰很长、底边又很斜,可能画到屏幕外面。最直接的改进是把图形中心移动到屏幕中央,或者缩小边长:
t.penup() t.goto(-side / 2, 0) t.pendown() t.setheading(0)另一个常见问题是penup()和pendown()状态不对。如果你用goto连多个点,中途忘记pendown(),看起来就像没有画线。实际不是角度问题,而是画笔一直抬着。
6. 把这个“第 12 题”变成以后都通用的绘图能力
如果只记这一题的标准答案,价值有限。真正值得沉淀的是下面四层能力:环境准备、单图绘制、参数化复用、几何判断。
6.1 先确认turtle能运行
turtle是 Python 标准库,在大多数官方 Python 版本里不需要额外安装。打开终端或vscode集成终端,输入:
python -c "import turtle; print('turtle ok')"如果报错No module named 'turtle',通常是因为当前 Python 环境不是官方安装包,或者安装时没带 Tk 支持。可以先确认你用的是哪个 Python 解释器,不要在一个环境里刚配完又跑到另一个环境运行。
在 Windows 下有时会有人误装多个 Python 版本,执行python和pip指向的不是同一个解释器。排查时建议用:
where python pip --version确认解释器和pip一致。这个问题和第 12 题本身无关,但它会浪费你很多时间。先把环境跑通,再谈画图。
6.2 从“画一个三角形”到“画一组三角形”
当你把代码改成函数后,你可以连续画好几个不同角度的钝角三角形。如果想让每个三角形分开,可以每次创建新Turtle,也可以移动画笔到不同区域。
更好的办法是避免让函数内部创建固定实例,改成传入Turtle对象:
def draw_obtuse_triangle(t, side, apex_angle=120): base_angle = (180 - apex_angle) / 2 ...传入同一支笔的好处是:画完第一个后,可以继续画第二个,不用反复弹出窗口。这种方式更接近真实项目里的“工具函数”写法。
6.3 把几何判断放在绘图之前
前面提到,画图前先做“三边能否成三角形”和“是否为钝角”的判断。这样做不仅是为了避免错误,更是为了让代码可读。如果你只写一段“画线然后转三次”的代码,别人很难知道你想表达什么。如果你先分类三角形,再计算角度,再绘制,逻辑就清晰很多。
一个更工程化的流程是:
- 输入边长。
- 判断分类。
- 计算所有坐标或角度。
- 调用绘图函数。
- 检查最终位置是否等于起点。
- 断言总旋转量是否接近 360°。
6.4 用这题检验你的“几何转代码”能力
实际上,第 12 题真正的价值在这里:它逼着你把抽象的图形关系转化为具体的机器动作。这种能力不会只用在海龟绘图里,以后你接触 SVG 坐标、机械臂关节运动、游戏角色寻路、机器人转向时,会遇到完全相同的逻辑——一个对象当前朝向是什么,下一个目标方向是什么,中间需要转多少度。
你现在画的是钝角三角形,但理解的是“方向向量变化量”和“多边形的外角和”。等你哪天写一个自动生成复杂多边形的程序时,会发现当年第 12 题留下的排查顺序仍然有用:
- 先确定起始朝向。
- 每一段走完都要知道当前朝向。
- 转弯角度必须和下一段方向匹配。
- 最后检查能否闭合。
- 不能闭合时,先从几何条件找原因,而不是盲目调代码。
回到开头那句话:钝角三角形并不是一道难题,但它是一道“照抄答案反而容易翻车”的题。当你不再把目光停在left(120)上,而是开始想“我走到的这个顶点,内角和外角分别是什么,下一步方向是什么”,说明你已经迈过了海龟绘图最关键的认知门槛。
如果你正在刷海龟绘图 100 题,我的建议是先别急着把第 12 题的标准代码复制进编辑器。拿一张纸,画出三角形,标出每个内角,再写出每个外角,最后才打开编辑器写forward和left。让程序帮你验证的是计算结果,而不是替代你思考。这一步迈过去后,后面那些旋转、嵌套、递归、正多边形和复杂组合图形,才会真正变得好懂。