1.3 SymPy 幾何模組入門:Point, Line, Circle#
學習動機#
1.1 與 1.2 節告訴我們一個故事:幾何從尼羅河畔的「食譜」演進到希臘的「演繹證明」。但這本書的真正主題在於——2000 年後的我們,如何用電腦把這些幾何知識變成可執行、可驗證、可探索的物件?
答案是 SymPy 的 sympy.geometry 模組。它的設計思想其實非常「希臘」:
- 幾何物件是「一等公民」:點、線、圓彼此不是數字,而是具名物件(像《幾何原本》裡有名字的「點 \(\alpha\) 」「直線 \(l\) 」);
- 操作即陳述:
t.is_right()是「這是否為直角三角形?」,l1.intersection(l2)是「這兩條線交在哪?」——這些正是歐幾里得命題的自然語言形式; - 保持符號精確:
Point(sqrt(2), 1/3)的座標是精確的根式與分數,不是浮點近似——這正是巴比倫書記夢寐以求的計算能力。
這一節是全書的技術工具箱:我們將系統地走過 Point、Segment、Line、Circle、Triangle(以及 Polygon)這些基本物件,每一個都以「歷史中的幾何問題 → SymPy 的現代實作」的對照來講。讀完本節,你就能在本書每一章中自由地把幾何直覺翻譯成程式。
1.3.1 開發環境與 import 慣例#
全書統一使用以下 import 慣例:
import sympy as sp
print(sp.__version__) # 預期輸出(因版本而異,例如): 1.13.3
安裝:
pip install sympy或pip install "sympy[plotting]"(後者附帶繪圖依賴 matplotlib——第 4 章起作圖時才需要)。
幾何物件集中在模組頂層,可直接使用:
import sympy as sp
print(sp.Point, sp.Line, sp.Circle, sp.Triangle, sp.Segment)
# 預期輸出: <class 'sympy.geometry.point.Point'>
# <class 'sympy.geometry.line.Line'>
# <class 'sympy.geometry.ellipse.Circle'>
# <class 'sympy.geometry.polygon.Triangle'>
# <class 'sympy.geometry.point.Segment'>
名稱衝突警告:
sp.Point的x、y屬性與我們可能自訂的變數容易混淆。本書慣例:座標變數一律用xx,yy或x1, y1等後綴避免衝突。
物件工廠:每個幾何實體都是一個類別#
SymPy 幾何模組的核心設計是類別繼承關係。理解它,能幫你預期每個物件「能做什麼」:
graph TD
G["GeometryEntity<br/>(所有幾何物件的共同祖先)"] --> P["Point"]
G --> L["LinearEntity"]
L --> LS["Line"]
L --> SG["Segment"]
L --> RY["Ray"]
G --> E["Ellipse / Circle"]
G --> PG["Polygon"]
PG --> T["Triangle"]
PG --> SR["RegularPolygon"]現在逐項檢視每一個物件。
1.3.2 Point:幾何的最小單位#
建構與座標#
import sympy as sp
P = sp.Point(0, 0)
A = sp.Point(1, 2)
B = sp.Point(sp.Integer(1) / 2, sp.Integer(2) / 3) # 分數座標保持精確
C = sp.Point(sp.sqrt(2), sp.sqrt(3)) # 根式座標一樣精確
print(A.x, A.y) # 預期輸出: 1 2
print(B.x, B.y) # 預期輸出: 1/2 2/3
print(C.x, C.y) # 預期輸出: sqrt(2) sqrt(3)
注意:
Point(1/2, 2/3)若字面使用 Python 整數除法,會因為1/2在 Python 是浮點循環引發浮點轉換警告——因此一律使用sp.Rational(a, b)或sp.Integer(a)/sp.Integer(b)(本書一律推薦Rational)。
兩點之間的所有基本操作#
以 1.2 節的「金字塔與影子」為情境:我們用 Point 把相似三角形比例法自動化了:
import sympy as sp
foot = sp.Point(0, 0) # 金字塔底
apex = sp.Point(0, sp.Rational(147)) # 金字塔頂
nail = sp.Point(sp.Rational(147, 2), 0) # 影尖
height = apex.y - foot.y # 塔高
shadow = nail.distance(foot) # 影長(距離)
print(height, shadow) # 預期輸出: 147 147/2
# distance 內部做的是 sqrt(dx² + dy²),符號化精確
hypo = sp.Point(3, 4).distance(sp.Point(0, 0))
print(hypo) # 預期輸出: 5
中點與反射#
泰勒斯定理的逆命題曾令我們困擾;SymPy 對幾何變換的第一等支援是把「演繹證明的模板」直接寫成方法:
import sympy as sp
A = sp.Point(0, 0)
mid = sp.Point(sp.Rational(1, 2), sp.Rational(1, 2))
print(mid.midpoint(sp.Point(1, 1))) # 預期輸出: Point2D(3/4, 3/4)
(midpoint 接受另一端點,回傳兩點的中點。)
平移、縮放與反射則被歸到 .translate(x, y)、.scale(factor_x, factor_y)、.reflect(line)。其歷史呼應是第 7 章「愛爾蘭根綱領」——當時克萊因主張「幾何是一個變換群下的不變性研究」,SymPy 幾乎把這些變換都做成了方法。這是 2000 年間「幾何是什麼」的思想軌跡留在我們 IDE 裡的實際痕迹。
小結 Point 的常用方法#
| 方法 | 意義 | 歷史呼應 |
|---|---|---|
.distance(P) | 兩點距離 \(d=\sqrt{(\Delta x)^2+(\Delta y)^2}\) | 3.1 的座標幾何 |
.midpoint(Q) | 中點 | 希臘的比例理論 |
.translate(dx, dy) | 平移 | 7.2 的等距變換 |
.scale(fx, fy) | 縮放 | 7.2 的相似變換 |
.is_collinear(*points) | 多點共線? | 測量的「直線」概念 |
.conjugate / reflect(l) | 反射 | 7.3 的群論 |
1.3.3 Segment 與 Line:線是「一段」還是「全年段」?#
古希臘幾何一開始就區分兩個概念,而 SymPy 也保留之:
- Segment(線段):有兩端點、有固定長度——拉繩人摸得到的「繩」;
- Line(直線):無限延長——歐幾里得的「未定義的第一概念」。
import sympy as sp
A = sp.Point(0, 0)
B = sp.Point(3, 4)
seg = sp.Segment(A, B)
print(seg.length) # 預期輸出: 5
print(seg.midpoint) # 預期輸出: Point2D(3/2, 2)
line = sp.Line(A, B)
print(line.length if hasattr(line, 'length') else 'Line is infinite') # Line 有 length 屬性嗎?
(第三行揭露真相:Line 是無限延長的,沒有有限長度;而 Segment 才有 length。)
交點:演繹幾何的第一個「合成動作」#
「兩條線交在哪裡?」是古代測量與歐幾里得命題共有的問題。SymPy 把它變成物件之間的動作:
import sympy as sp
l1 = sp.Line(sp.Point(0, 0), sp.Point(2, 2)) # 直線 y = x
l2 = sp.Line(sp.Point(0, 2), sp.Point(2, 0)) # 直線 y = 2 - x
print(l1.intersection(l2)) # 預期輸出: [Point2D(1, 1)]
注意回傳值是一個串列:兩條線交點恰一個;一條線與一條線段可能交零個或一個;兩條重合的直線「交點」本身仍是無限條——此時回傳的是整條直線:
import sympy as sp
l1 = sp.Line(sp.Point(0, 0), sp.Point(1, 1))
l2 = sp.Line(sp.Point(2, 2), sp.Point(10, 10)) # 同一條直線(斜率相同且共享)
print(l1.intersection(l2)) # 預期輸出: [Line2D(Point2D(2, 2), Point2D(10, 10))]
平行與垂直:不動手就能「推平行」#
古希臘畫平行線靠平行公設(第五公設)——這條「不只漫長而且不夠雅觀」的假設引發非歐幾何的誕生(第 6 章)。SymPy 的 Line 提供了這一系列的幾何陳述式方法:
import sympy as sp
l1 = sp.Line(sp.Point(0, 0), sp.Point(1, 2))
l2 = sp.Line(sp.Point(3, 0), sp.Point(4, 2)) # 沿平移方向
print(l1.is_parallel(l2)) # 預期輸出: True
print(l1.slope == l2.slope) # 預期輸出: True
l3 = sp.Line(sp.Point(0, 0), sp.Point(2, -1)) # 斜率 ∓1/2
print(l1.is_perpendicular(l3)) # 預期輸出: False(此例斜率為 2 與 -1/2,相乘 ≠ -1)
等等—— \(l_1\) 的斜率是 2, \(l_3\) 的斜率是 \(-1/2\) , \(2 \times (-1/2) = -1\) ,應該垂直。為什麼會顯示 False?這是個常見陷阱:我們檢查的 \((-1/2)\) 陳述是精確的,而 is_perpendicular 對精確斜率方能工作。 \(l_3\) 的兩點 \((0,0)\) 與 \((2,-1)\) 給出斜率 \(-\frac{1}{2}\) ——此時它應該傳回 True。讓我修正這個例子:
import sympy as sp
l1 = sp.Line(sp.Point(0, 0), sp.Point(1, 2)) # 斜率 2
l3 = sp.Line(sp.Point(0, 0), sp.Point(2, -1)) # 斜率 -1/2
print(l1.slope) # 預期輸出: 2
print(l3.slope) # 預期輸出: -1/2
print(l1.slope * l3.slope) # 預期輸出: -1
print(l1.is_perpendicular(l3)) # 預期輸出: True
於是正確的答案是
True。像這種「先讀出屬性、再檢查關係」的工作流程,在除錯 SymPy 幾何代碼時最為有用。
還有一個方便的方法:給一條直線 \(l\) 與一點 \(P\) ,直接產生過 \(P\) 的平行線 / 垂直線,完全跳過「自己算斜率」的過程:
import sympy as sp
l1 = sp.Line(sp.Point(0, 0), sp.Point(1, 2)) # 斜率 2
P = sp.Point(5, 3)
print(l1.parallel_line(P).slope == l1.slope) # 預期輸出: True
print(l1.perpendicular_line(P).slope) # 預期輸出: -1/2
# 點到直線的距離(解析幾何的核心公式)
print(P.distance(l1)) # 預期輸出: 7*sqrt(5)/5
「點到直線的距離」這個公式在 3.3 節解析幾何中會推成顯式形式 \(d = \dfrac{|ax_0+by_0+c|}{\sqrt{a^2+b^2}}\) ;在此只需記住:一個 SymPy 方法就能直接查表得到。
1.3.4 Circle:圓的物件化#
以歐幾里得《幾何原本》的第三定義「圓是由一點(圓心)到另一點等距的所有點」為出發點,SymPy 把圓建構成一個物件:
import sympy as sp
c = sp.Circle(sp.Point(0, 0), 5)
print(c.center) # 預期輸出: Point2D(0, 0)
print(c.radius) # 預期輸出: 5
print(c.area) # 預期輸出: 25*pi
print(c.circumference)# 預期輸出: 10*pi
print(c.equation()) # 預期輸出: Eq(x**2 + y**2 - 25, 0) 或 (x - 0)**2 + (y - 0)**2 - 25
注意 area 是 25*pi——精確的圓周率(相較 1.1 節埃及的 \(\frac{256}{81}\) 近似)。3000 年之間,人類用了約 2000 年把 \(\pi\) 逼近極限,而 SymPy 只需記住一個符號。
另一種建構方式:直徑#
import sympy as sp
# 以?不在直徑上的額外一點,由三點建構圓(其中兩點恰好是直徑兩端)
d = sp.Circle(sp.Point(-3, 0), sp.Point(0, 3), sp.Point(3, 0))
print(d.center) # 預期輸出: Point2D(0, 0)
print(d.radius) # 預期輸出: 3
print(d.equation()) # 預期輸出: x**2 + y**2 - 9
技巧:三點建構法
Circle(A, B, C)是 SymPy 產生圓最通用的一途——阿基米德書中的「過三點定圓」(歐幾里得第 III 卷命題 5)在此直接成為一行建構子。半徑與圓心也由三點自動求得。
圓與線、圓與圓的交點#
第 1.2 節的泰勒斯定理可以用這些方法稍微「實㕸化」:
import sympy as sp
c = sp.Circle(sp.Point(0, 0), 5)
A = sp.Point(-5, 0); C = sp.Point(5, 0)
l = sp.Line(A, C) # 直徑所在直線
print(c.intersection(l)) # 預期輸出: [Point2D(-5, 0), Point2D(5, 0)]
# 過 A 的非直徑直線,與圓的配個交點
l2 = sp.Line(A, sp.Point(0, 5))
print(c.intersection(l2)) # 預期輸出: [Point2D(-5, 0), Point2D(0, 5)]
# 圓上取點:用 Symbolic 的 cos/sin(在 1.2 已補上)
th = sp.th = sp.Symbol('th', real=True)
B = sp.Point(5*sp.cos(th), 5*sp.sin(th))
print(B) # 預期輸出: Point2D(5*cos(th), 5*sin(th))
(Symbol('th') 是 SymPy 的「符號變數」,把任何幾何量變成一個可進行代數操作的未知數——這是符號計算對浮點計算的最大解藥。)
相切與交型式判定(3 種關係)#
import sympy as sp
c1 = sp.Circle(sp.Point(0, 0), 2)
c2 = sp.Circle(sp.Point(5, 0), 2) # 距離 5 > r1 + r2 = 4 ⇒ 外離
c3 = sp.Circle(sp.Point(3, 0), 1) # 距離 3 = r1 + r2 ⇒ 外切
c4 = sp.Circle(sp.Point(1, 0), 1) # 距離 1 = r1 - r2 ⇒ 內切
print(c1.intersection(c2)) # 預期輸出: []
print(c1.intersection(c3)) # 預期輸出: [Point2D(2, 0)]
print(c1.intersection(c4)) # 預期輸出: [Point2D(2, 0)]
兩圓相交的例子則回傳 2 個交點:
import sympy as sp
c1 = sp.Circle(sp.Point(0, 0), 2)
c5 = sp.Circle(sp.Point(2, 0), 2)
pts = c1.intersection(c5)
print(len(pts)) # 預期輸出: 2
print(pts) # 預期輸出: [Point2D(1, -sqrt(3)), Point2D(1, sqrt(3))]
這裡的
sqrt(3)是精確的:若你把它換成 NumPy 的np.sqrt(3),得到的是循環小數的浮點近似。我可魂的是,幾何交點在數學上的確是 \(\sqrt{3}\) ,SymPy 保留這個精確形式,就是為了使後續推理不失真。
Ellipse:圓的「親戚」(到第 4 章才細講)#
import sympy as sp
e = sp.Ellipse(sp.Point(0, 0), 3, 2) # 長半軸 3,短半軸 2
print(e.area) # 預期輸出: 6*pi
print(e.eccentricity) # 預期輸出: sqrt(5)/3
(離心率 \(e = \sqrt{1 - b^2/a^2} = \sqrt{5}/3\) 。這就是 4.2 節克卜勒行星軌道的開端物。)
1.3.5 Triangle:36 條「歐幾里得性質」的字典#
三角形是歐幾里得幾何的核心,也是《幾何原本》第一卷的主題。SymPy 的 Triangle 把歐幾里得書中關於三角形的所有常見性質做成了屬性與方法——這本質上相當於把《幾何原本》第 I 卷、第 III 卷的命題列表,編成一本可查詢的字典。
建構方式有多種#
import sympy as sp
t1 = sp.Triangle(sp.Point(0, 0), sp.Point(1, 0), sp.Point(0, 1)) # 三頂點建構:直角三角形
print(t1.area) # 預期輸出: 1/2
# 只知道三邊長時(SSS),最直覺的作法是「邊長翻倍成座標」:
# 想要 3-4-5 直角三角形,取 3、4 為兩直角邊
t2 = sp.Triangle(sp.Point(0, 0), sp.Point(3, 0), sp.Point(0, 4))
print(t2.area) # 預期輸出: 6
print([s.length for s in t2.sides]) # 預期輸出: [3, 5, 4]
(「三邊確定 ⇒ 三角形唯一」是《幾何原本》命題 I.8 證明的 SSS 結論;Triangle.sides 回傳三條 Segment,長度一查即得。順帶一提,SSA(兩邊一角、但角不夾邊)不唯一確定三角形——這正是古希臘早已洞悉的曖昧情況。)
重要屬性#
import sympy as sp
A = sp.Point(0, 0)
B = sp.Point(3, 0)
C = sp.Point(0, 4)
t = sp.Triangle(A, B, C)
print(t.area) # 預期輸出: 6
print(t.perimeter) # 預期輸出: 12
print(t.is_right()) # 預期輸出: True
print(t.is_equilateral()) # 預期輸出: False
print(t.angles) # 預期輸出: 以三頂點為鍵、各內角為值的字典
求中線與重心、內心、外心——三角心#
古希臘幾何家(歐幾里得《幾何原本》第 III 卷)研究了三角形的三條特殊直線與它們的交點——各種「三角心」。SymPy 把這些心做成物件屬性:
import sympy as sp
t = sp.Triangle(sp.Point(0, 0), sp.Point(6, 0), sp.Point(0, 8))
print(t.centroid) # 重心:預期輸出 Point2D(2, 8/3)——即 ((0+6+0)/3, (0+0+8)/3)
print(t.circumcenter) # 外心:預期輸出 Point2D(3, 4)
print(t.circumcircle) # 外接圓:預期輸出 Circle(Point2D(3, 4), 5)
print(t.incircle) # 內切圓:預期輸出 Circle(Point2D(2, 2), 2)
print(t.inradius) # 內切圓半徑:預期輸出 2
print(t.circumradius) # 外接圓半徑:預期輸出 5
由重心的座標可以感受 SymPy 的精確性: \(((0+6+0)/3, (0+0+8)/3) = (2, 8/3)\) ,分數的不循環不近似。
內視:三角形「內心」在座標下的計算#
import sympy as sp
t = sp.Triangle(sp.Point(0, 0), sp.Point(4, 0), sp.Point(0, 3))
print(t.incenter) # 預期輸出: Point2D(1, 1)
print(t.inradius) # 預期輸出: 1
在 3-4-5 直角三角形中,內切圓半徑公式 \(r = \dfrac{a+b-c}{2} = \dfrac{3+4-5}{2} = 1\) ——誠與 SymPy 的 inradius 一致。這種「幾何公式可以被程式條捷」並配平的場景,會在整本書反覆出現。
1.3.6 Polygon:三邊到 n 邊#
四邊形與 n 邊形可以由 Polygon 建立;正多邊形則有專用 RegularPolygon:
import sympy as sp
q = sp.Polygon(sp.Point(0, 0), sp.Point(4, 0), sp.Point(4, 3), sp.Point(0, 3))
print(q.area) # 預期輸出: 12(即 4x3 矩形)
print(q.perimeter) # 預期輸出: 14
p = sp.RegularPolygon(sp.Point(0, 0), 1, 6) # 內接圓半徑 1 的正六邊形
print(p.area) # 預期輸出: 3*sqrt(3)/2(約 2.598)
print(sp.N(p.area)) # 預期輸出: 2.59807621135
歷史呼應:阿基米德用內接與外切正多邊形(96 邊)來夾逼 \(\pi\) ;本段最後兩行把這個思路整理成程式——第 2 章將用它建立「內接 n 邊形面積 × 2 的幾何遞進」做課後探討。
1.3.7 全書工作流#
本節的同時也是全書的使用手冊。請回顧本書潛在的共同作業流:
flowchart LR
H["歷史故事<br/>(第 1-2 頁)"] --> M["數學推理<br/>(推導與命題)"] --> S["SymPy 實作<br/>(物件 + 符號計算 + 可執行代碼)"] --> V["驗證 / 誤差分析<br/>(輸出註解)"]
V -.->|"產生新問題"| H節的體裁永遠是:歷史背景 → 理論推導 → SymPy 代碼 → 註解輸出(預期輸出) → 習題。1.1 與 1.2 節已經展示了這個節奏;往後 10 章,都會沿此原則推進——讀者隨時都能自己複製程式碼與本書同步驗證。
習題#
習題 1.3.1(Point 基礎) 用 Point 與 Rational 表達 1.2 節影射法的完整資料:金字塔位於原點、塔高未知;把泰勒斯的身高、影長、塔影長設成各點,最後直接用物件方法算出金字塔高度——不看任何手動運算式。
習題 1.3.2(Line 的相交與平行) (a) 建構直線 \(l_1: y = 2x+1\) 與 \(l_2: y = 2x-3\) ,值出兩者不相交的原因。 (b) 令 \(l_3: y = -\frac{1}{2}x + 4\) ,求 \(l_1\) 與 \(l_3\) 的交點,並用 intersection 檢驗。 (c) 求點 \((3, 7)\) 到 \(l_1\) 的最短距離(distance方法),並與解析公式 \(\frac{|2x_0-y_0+1|}{\sqrt{5}}\) 對照。
習題 1.3.3(Circle:泰勒斯定理自動化) 以圓 \((0,0)\) 半徑 \(r\) ,使用 Circle.intersection 求直線 \(x = r/2\) 與該圓的兩個交點;再驗證:這兩個交點與兩個直徑端點一起構成直角 \(\triangle\) (用 Triangle + is_right)。
習題 1.3.4(Triangle 的心) 用 Triangle 計算三角形 \((0,0), (6,0), (0,8)\) 的重心、內心、外心,並驗證: (a) 重心在各中線上(把重心代入中線的直線方程,用 Line 檢驗共線); (b) 內心到三邊距離相等(distance to side); (c) 外心到三頂點距離相等。
習題 1.3.5(內接正 n 邊形損逼 \(\pi\) ) 半徑 1 的圓內接正 \(n\) 邊形面積為 \(A(n) = \frac{n}{2}\sin(2\pi/n)\) 。用 SymPy 樣式化(sp.sin)計算 \(n = 6, 12, 24, 48, 96\) 的 \(A(n)\) ,並與 \(\pi\) 比較出誤差。阿基米德的 96 邊近似誤差有多大?
小結#
| 物件 | 最基本用法 | 代表性方法 |
|---|---|---|
Point | sp.Point(x, y) | .distance .midpoint .is_collinear |
Segment | sp.Segment(P, Q) | .length .midpoint |
Line | sp.Line(P, Q) | .slope .intersection .distance |
Circle | sp.Circle(O, r) | .area .radius .equation() .intersection |
Ellipse | sp.Ellipse(O, a, b) | .area .eccentricity |
Triangle | sp.Triangle(A, B, C) | .area .angles .is_right() 各型心 |
Polygon / RegularPolygon | sp.Polygon(...) | .area .perimeter |
- SymPy 幾何模組的物件皆可互操作:請再讀一次本節的每一塊代碼塊,所有交點、圓、三角形都是同一底線的
GeometryEntity,可以交叉通信。 - 物件導向 + 符號精確是本書的兩大支柱:前者給予「幾何是一本內容外部的語言」,後者給予「數學的完整性保留了」。
- 下一章(第 2 章)我們真誠進入歐幾里得《幾何原本》——歷史上第一次把這些物件組織成公理 → 定理 → 證明的系統。