3.3 用 SymPy 做解析幾何:直線、距離、交點#
學習動機#
前兩節建立了「點 = 數對、向量 = 序組、距離與角度 = 內積」的座標世界觀。本章開始把這套世界觀工具化:SymPy 的 geometry 模組把解析幾何的物件直接做成可實例化的類——Point、Line、Segment、Polygon——並內建了斜率、距離、交點、面積、平分線等操作。學習的目標不只是「查得到方法」,更是你手上有了一台解析幾何的計算器之後,過去必須靠巧思的題目(三線是否共點、兩線是否垂直、點到線最短路徑在哪)全都變成可以直接呼叫的機械操作。本節將完整走一遍 Line.slope、Line.distance、Line.arbitrary_point、Polygon.area 等核心 API,並說明它們背後的數學來源。
一、直線的四種座標表示#
解析幾何的直線,在歷史上逐步累積出四種典型寫法,各有用途:
| 名稱 | 方程 | 適用情境 |
|---|---|---|
| 點斜式 | \(y - y_0 = m(x - x_0)\) | 已知一點與斜率 |
| 一般式 | \(ax + by + c = 0\) | 統一處理、SymPy 預設 |
| 斜載式 | \(y = mx + b\) | 函數視角、微積分 |
| 向量式 | \(P = P_0 + t\,v\) | 所有參數化、顯式包含方向 |
SymPy 的 Line 物件在內部幾乎都是「兩點式」記錄的:Line(p1, p2) 儲存兩個點,其他性質皆由此推導。這個實作的選擇本身就體現解析幾何的根本:兩點定一直線,其餘皆可計算。
1.1 從兩點推一切#
給定 \(A(x_1, y_1)\) 、 \(B(x_2, y_2)\) :
- 方向向量 \(\vec{v} = (x_2 - x_1,\ y_2 - y_1)\)
- 斜率 \(m = \dfrac{y_2 - y_1}{x_2 - x_1}\) ( \(x_1 \ne x_2\) )
- 一般式:由「 \(\vec{v}\) 與 \(P - A\) 垂直」的條件,取法向量 \(n = (-(y_2 - y_1),\ x_2 - x_1)\) ,得
展開即一般式 \(ax + by + c = 0\) 。注意:直線方程的本質是「點與兩定點組成的向量垂直」——內積思想的第一次實戰應用。
from sympy import symbols, Point, Line, simplify
A = Point(1, 2)
B = Point(4, 8)
l = Line(A, B)
print(l.slope)
# 2
print(l.equation())
# -6*x + 3*y (即 -6x + 3y = 0,約簡即 y = 2x)
Line.equation()回傳 \(ax + by + c = 0\) 左端的表達式(SymPy 把常數為 \(0\) 時略去 \(c\) )。這是解析幾何的一般式在 SymPy 中的樣貌。
1.2 垂直與平行#
Line 直接內建判斷與建構:
from sympy import Point, Line, symbols
A, B = Point(1, 2), Point(4, 8)
l = Line(A, B)
# 1. 過給定點的平行線
P = Point(0, 1)
print(l.parallel_line(P).slope)
# 2
# 2. 過給定點的垂直線
print(l.perpendicular_line(P).slope)
# -1/2
# 3. 兩線是否平行 / 垂直
m = Line(Point(0, 0), Point(1, 2))
print(l.is_parallel(m)) # True
print(l.is_perpendicular(Line(Point(0, 0), Point(2, -1)))) # True (斜率 2 * (-1/2) = -1)
記住兩條性質背後的數學:
- 平行 \(\iff\) 方向向量成比 \(\iff\) 斜率相等;
- 垂直 \(\iff\) 方向向量內積為 \(0 \iff m_1 m_2 = -1\) (見 3.2 節)。
二、交點:解方程的幾何#
2.1 兩直線交點#
兩直線
的交點就是二元一次方程組的解。解析幾何在這裡與「解 linear system」合流——解不存在則平行,解唯一則相交,解不唯一則共線。這個三態對應,正是把「幾何關係」翻譯成「解空間分類」的第一例,也是線性代數(3.4 節)登場的預告。
from sympy import Point, Line
l1 = Line(Point(0, 0), Point(4, 2)) # y = x/2
l2 = Line(Point(1, 0), Point(1, 5)) # x = 1
print(l1.intersection(l2))
# [Point2D(1, 1/2)]
SymPy 統一用 intersection 處理所有情形,且回傳所有交點(包括圓與圓的兩個交點):
from sympy import Point, Line, Circle
c = Circle(Point(0, 0), 1)
l = Line(Point(-2, 0), Point(2, 0))
print(c.intersection(l))
# [Point2D(-1, 0), Point2D(1, 0)]
# 相切時只有一點:
t = Line(Point(0, 1), Point(2, 1))
print(c.intersection(t))
# [Point2D(0, 1)]
2.2 三線共點:以代數驗證幾何定理#
例(Ceva 定理的簡單情形):三角形 \(A(0,0), B(6,0), C(2,6)\) ,其三條中線是否共點?
from sympy import Point, Line, Segment
A, B, C = Point(0, 0), Point(6, 0), Point(2, 6)
M_bc = Segment(B, C).midpoint # 中點
M_ca = Segment(C, A).midpoint
M_ab = Segment(A, B).midpoint
med1 = Line(A, M_bc)
med2 = Line(B, M_ca)
med3 = Line(C, M_ab)
p12 = med1.intersection(med2)[0]
print(p12)
# Point2D(8/3, 2)
print(med3.intersection(med1))
# [Point2D(8/3, 2)] → 三線交於同一點(重心)
在純歐氏幾何中,「三中線共點」需要一整套構造與全等三角形論證;在座標世界,它就是「三個二元方程組有共同解」。解析方法的證明效率由此一覽無遺。
三、距離:內積觀點的集大成#
3.1 點到直線距離#
點 \(P(x_0, y_0)\) 到直線 \(ax + by + c = 0\) 的距離:
公式怎麼來的?兩條路都值得認識:
- 最短化路徑:在直線上取參數點 \(Q(t) = P_0 + t\,\vec{v}\) ,把 \(|P - Q(t)|^2\) 當 \(t\) 的二次函數,取其最小值(配方法或微分)。最短距離的 \(Q\) 恰好是 \(P\) 的正交投影。
- 內積路徑(上節 2.5): \(P - Q_{\mathrm{proj}}\) 垂直於直線方向,殘差長度即距離。
兩者其實是同一件事的兩種語言。SymPy 的 Line.distance 對應第 1 條路:
from sympy import Point, Line
l = Line(Point(0, 0), Point(3, 0)) # x 軸
P = Point(2, 5)
print(l.distance(P))
# 5
P = Point(2, 5)
l2 = Line(Point(0, 0), Point(3, 4)) # 方程 4x - 3y = 0
print(l2.distance(P))
# 7/5 (|4*2 - 3*5|/5 = 7/5)
3.2 點到線段距離 vs 點到直線距離#
distance 在 Line 與 Segment 上語義不同:對 Segment 而言,若垂足不在線段內,最短距離取到端點的距離。
from sympy import Point, Segment
seg = Segment(Point(0, 0), Point(3, 0))
print(seg.distance(Point(5, 4)))
# sqrt(20) = 2*sqrt(5) (垂足不在段內,取到端點 (3,0))
seg_l = Segment(Point(-1, 0), Point(10, 0))
print(seg_l.distance(Point(5, 2)))
# 2 (垂足在段內,取垂直距離)
3.3 任意參數點:arbitrary_point#
這是一個結構豐富的方法,值得放在重要位置。Line(p1, p2).arbitrary_point(t) 回傳
它不是工具箱中的花邊,而是解析幾何真正的「動態眼睛」:
from sympy import Point, Line, symbols, Rational, simplify
A, B = Point(0, 0), Point(6, 4)
t = symbols('t', real=True)
ln = Line(A, B)
ap = ln.arbitrary_point(t)
print(ap)
# Point2D(6*t, 4*t)
# (1) 把 t 指定為 1/2 → 中點
print(ap.subs(t, Rational(1, 2)))
# Point2D(3, 2)
# (2) 把參數點代入直線方程驗證自動成立
xs, ys = ln.equation().free_symbols
eq = simplify(ln.equation().subs(xs, ap.x).subs(ys, ap.y))
print(eq)
# 0
有了「參數點」,許多幾何條件可以一次翻譯為「 \(t\) 的一次方程」。例如「求直線 \(y = 2x\) 上到 \((4, 3)\) 距離最小的點」——把距離平方表為 \(t\) 的函數、微分(或配方)取最小:
from sympy import Point, Line, symbols, diff, solve, simplify
t = symbols('t', real=True)
l = Line(Point(0, 0), Point(1, 2))
P = Point(4, 3)
ap = l.arbitrary_point(t) # (t, 2t)
fsq = simplify(sum([ (ap[i] - P[i])**2 for i in range(2)]))
print(fsq)
# 5*(t - 2)**2 + 5
print(solve(fsq.diff(t), t))
# [2] → 最近點 (2, 4)
print(l.distance(P))
# sqrt(5) → 最短距離。而柱長 |ap - P| 在 t = 2 時 = sqrt(5) ✓
print(Point(2, 4).distance(P))
# sqrt(5)
這一段展示了 3.1 節所說「解析幾何使微積分成為可能」的最小例子:把幾何最佳化問題化為一元函數極值。第 9 章曲線曲率計算,會在同一消息上繼續。
四、多邊形:面積、周長與定向#
4.1 Polygon.area 與鞋帶公式#
Polygon(*points) 支持 \(n\) 多邊形,area 回傳其有向面積(SymPy 對座標排序時可能翻轉符號,取 abs 更穩靠):
from sympy import Point, Polygon, Abs
tri = Polygon(Point(0, 0), Point(5, 0), Point(1, 3))
print(tri.area)
# 15/2
quad = Polygon(Point(0, 0), Point(4, 0), Point(6, 3), Point(1, 3))
print(quad.area)
# 27/2
# 顛倒頂點順序時的「有向性」:
rev = Polygon(Point(1, 3), Point(6, 3), Point(4, 0), Point(0, 0))
print(rev.area)
# -27/2 → 有向面積為負,表示頂點順時針
print(Abs(rev.area))
# 27/2
Polygon.area的實作原理就是 3.2 節的鞋帶公式——本質是條邊向量叉積。知道臉孔,你就能在 NumPy 中自己實作(8.4 節將會做到)。
4.2 多邊形的角與中線#
from sympy import Point, Polygon, Segment
tri = Polygon(Point(0, 0), Point(6, 0), Point(2, 6))
print(tri.perimeter)
# 6 + 2*sqrt(10) + 2*sqrt(13) ← 各邊長之和:|AB|=6、|BC|=2√10、|CA|=2√13
print(tri.centroid)
# Point2D(8/3, 2) ← 與 3.2 節的重心點一致
# 過頂點 (0,0) 的高(字典以各「頂點」為鍵)
alt = tri.altitudes[Point(0, 0)]
print(alt)
# Segment2D(Point2D(0, 0), Point2D(54/13, 36/13))
print(alt.slope)
# 2/3 垂直於對邊 BC(斜率 -3/2):2/3 * (-3/2) = -1 ✓
altitudes、medians、angles都是「以頂點為鍵」的字典。其中三角形中心族(重心、外心、內心)的座標,SymPy 全部提供:
from sympy import Point, Polygon, simplify
tri = Polygon(Point(0, 0), Point(6, 0), Point(2, 6))
print(tri.centroid) # Point2D(8/3, 2) 重心
print(tri.circumcenter) # Point2D(3, 7/3) 外心
inc = tri.incenter
print(simplify(inc.x), simplify(inc.y))
# 6*(1 + sqrt(10))/(3 + sqrt(10) + sqrt(13)) 18/(3 + sqrt(10) + sqrt(13))
執行常式驗證:外心 \((3, 7/3)\) 到三頂點距離皆為邊 \(\sqrt{130}/3\) ;內心到三邊距離相等(可用 Segment.distance 檢驗)。這些中心點的座標公式在純幾何中需個別構造證明,在座標世界只是解方程組。順帶一提,「三心」是否重合(即等座標),正是下一節談變換的絕佳練習。
五、綜合範例:五個「經典題家中線」的座標重演#
以下五題都是歷史上「需巧思」的題目;在 SymPy 下每題都是數行呼叫。
5.1 垂直平分線與外接圓#
過 \(P(2, 5)\) 、 \(Q(6, 3)\) 的線段的鉛平分線,就是圓心的候選:
from sympy import Point, Segment, Line, simplify
P, Q = Point(2, 5), Point(6, 3)
M = Segment(P, Q).midpoint
base = Line(P, Q)
perp = base.perpendicular_line(M) # 過中點的垂直線 = 垂直平分線
print(simplify(perp.slope))
# 2 (PQ 斜率 = (3−5)/(6−2) = −1/2,垂線斜率取負倒數 2)
print(M)
# Point2D(4, 4)
外接圓的圓心是三條垂直平分線之交(三線又共點!)。這與 2.2 的「三中線共點」是同一套計算模板——交點、垂線、中點三個簡單基本的合成。實際上 SymPy 的 Polygon.circumcenter 已經幫你做掉了。
5.2 圓上一點的切線#
半徑 \(OP\) 與切線互相垂直(內積為零)——這是 3.2 節「零內積即垂直」的直接應用。過 \(P\) 的切線就是「過 \(P\) 且垂直於 \(OP\) 」的直線:
from sympy import Point, Line, Circle
c = Circle(Point(0, 0), 5)
P = Point(3, 4)
radius_line = Line(Point(0, 0), P)
tangent = radius_line.perpendicular_line(P)
# 1. 切線性驗證:切線與圓的交點只有 P
print(c.intersection(tangent))
# [Point2D(3, 4)]
# 2. 垂直性驗證
print(radius_line.is_perpendicular(tangent))
# True
5.3 對稱點:鏡射一個點過一條線#
點 \(P\) 對直線 \(l\) 的對稱點:取 \(P\) 的垂足 \(F\) ,則 \(P' = 2F - P\) 。「取垂足」用 projection:
from sympy import Point, Line
P = Point(3, 5)
l = Line(Point(0, 0), Point(1, 0)) # x 軸
F = l.projection(P)
print(F)
# Point2D(3, 0)
Pprime = Point(2*F.x - P.x, 2*F.y - P.y)
print(Pprime)
# Point2D(3, -5)
5.4 點在圓的內外判定#
from sympy import Point, Circle
c = Circle(Point(0, 0), 5)
for q in [Point(3, 4), Point(1, 1), Point(6, 0)]:
print(q in c, c.center.distance(q))
# True 5
# False sqrt(2) (在圓內)
# False 6 (在圓外)
注意:
in只判斷「在圓周上」。內外判定要靠距離比較。這是幾何 API 設計與集合語言的差異,在實作中值得留意。
六、過程中你會踩到的事:API 使用要點#
- 所有回傳值都是 SymPy 物件:
Point2D(1, 1/2)的1/2是Rational(1,2),不是浮點數。計算嚴格、可符號化——這就是 1.3 節入門強調的「符號幾何」的紅利。 - 回傳類型經常是 list:交點是
[Point2D(...), ...]。取第一個用[0]。 - 斜率在鉛直線上是
oo(正無窮):Line(Point(1,0), Point(1,5)).slope == oo為 True;此時垂直線斜率為0。程式要對此做分支處理,不要預設斜率是有限數。 - 字典型屬性:
Polygon.altitudes、medians等回傳 dict;Polygon.angles也是。 Point的加減與乘以純量可用,但 SymPy 幾何類的Point與 3.2 節的向量Matrix是兩套系統,兩者可互轉(p.x, p.y或Matrix([p.x, p.y]))。
七、小結#
| 方法 | 用途 | 背後數學 |
|---|---|---|
Line(p1, p2).slope | 斜率 | \(\Delta y / \Delta x\) |
l.equation() | 一般式方程 | 法向量 · 內積條件 |
l.intersection(m) | 交點(list) | 解二元一次方程組 |
l.distance(P) | 點到直線距離 | 正交投影的最小平方 |
l.arbitrary_point(t) | 參數點 | 參數化曲線(微積分入口) |
l.perpendicular_line(P) | 垂線 | 方向向量內積 = 0 |
Segment.midpoint | 中點 | 平均公式 |
Polygon.area | 面積 | 鞋帶公式 = 叉積 |
Polygon.centroid 等 | 三心 | 座標平均 / 垂直平分線交點 |
習題#
習題 3.3-1(直線方程) 用 SymPy 求過 \(A(2, -1)\) 與 \(B(-4, 11)\) 的直線:(1) 斜率;(2) 一般式方程並化為最簡;(3) 在 \(x\) 軸上的截距;(4) 用 l.is_perpendicular 驗證過 \((0, -1)\) 的垂線斜率為 \(-1/2\) 。
習題 3.3-2(三線共點的推廣) 三角形 \(A(0,0), B(8,0), C(2,8)\) 。求:(1) 三條中線交點;(2) 三條高所在直線的交點(垂心);(3) 內角平分線用 SymPy 的 bisectors 或以角度參數構造並驗證三條平分線共點(內心)。三個答案都在三角形內部嗎?
習題 3.3-3(距離與參數點) 過 \(A(1, 1)\) 、 \(B(4, 7)\) 的直線 \(l\) 與 \(P(0, 5)\) :(1) 用 arbitrary_point(t) 將 \(|P - Q(t)|^2\) 表為 \(t\) 的函數;(2) 微分求最小值點 \(Q^*\) ;(3) 驗證 \(Q^* - P\) 與直線方向向量垂直(內積為 0);(4) 用 l.distance(P) 驗證最小距離。
習題 3.3-4(切線與對稱) (1) 圓 \(x^2 + y^2 = 25\) 上點 \((4, 3)\) 的兩個切線?(只有一條)用 perpendicular_line 求、用 intersection 驗證切點唯一;(2) 點 \(P(7, 3)\) 對該圓心「圓的對稱」:對過圓心直線 \(y = x\) 對稱後求對稱點 \(P'\) ,驗證 \(O, P, P'\) 兩兩距離相等。
習題 3.3-5(面積與選點) 四個點 \((0,0), (7, 2), (5, 8), (-2, 4)\) :(1) 用 Polygon.area 算四邊形面積(注意頂點需依序);(2) 把任意 \(Y\) 到 \(X\) 頂點順序亂掉時面積會如何?試一組錯序並解釋絕對值;(3) 用鞋帶公式實作(一道 sum(...) 表達式)驗證,並與 Matrix.det() 版本對比。