3.2 三维空间中的向量运算
有了这些Python函数,在三个维度上对向量运算的结果进行可视化就变得很简单了。二维平面上的所有算术运算在三维空间中都有对应的运算,而且其几何效果是类似的。
3.2.1 添加三维向量
在三维空间中,向量加法仍可以通过将坐标相加来完成。向量(2, 1, 1)和(1, 2, 2)相加为(2+1, 1+2, 1+2) = (3, 3, 3)。从原点开始,将两个输入向量首尾相接,就可以得到求和之后的点(3, 3, 3)(见图3-15)。
图3-15 三维向量加法的两种可视化示例
与在二维平面上一样,要想把任意数量的三维向量相加,可以将它们的所有坐标、所有坐标和所有坐标分别相加。有了这三个和,就能得到新向量的坐标。例如,对(1, 1, 3)、(2, 4, -4)和(4, 2, -2)求和。因为它们各自的坐标是1、2、4,相加为7,坐标的和也是7,而坐标的和是 -3,所以向量的和是(7, 7, -3)。这三个向量首尾相接看起来如图3-16所示。
图3-16 在三维空间中添加三个首尾相接的向量
在Python中,可以编写一个简洁的函数来对任意数量的输入向量求和,并在二维或三维(或之后更高的维度)空间中使用,如下所示。
def add(*vectors):
by_coordinate = zip(*vectors)
coordinate_sums = [sum(coords) for coords in by_coordinate]
return tuple(coordinate_sums)
下面来分析一下。在输入向量上调用Python的zip
函数,可以提取它们的坐标、坐标和坐标。例如:
>>> list(zip(*[(1,1,3),(2,4,-4),(4,2,-2)]))
[(1, 2, 4), (1, 4, 2), (3, -4, -2)]
(需要将zip
结果转换为列表来显示它的值。)如果将Python的sum
函数应用到每个分组坐标上,将会获得、和值的和,分别为:
[sum(coords) for coords in [(1, 2, 4), (1, 4, 2), (3, -4, -2)]]
[7, 7, -3]
最后,为了保持一致,需要将这个列表转换为元组,因为到目前为止,所有的向量都以元组的形式表示。结果就是元组(7, 7, 3)。add
函数也可以写成下面的单行代码(这可能不那么有Python风格)。
def add(*vectors):
return tuple(map(sum,zip(*vectors)))
3.2.2 三维空间中的标量乘法
将三维向量乘以标量,就是把其所有分量乘以标量系数。例如,向量(1, 2, 3)乘以标量2,会得到(2, 4, 6)。由此产生的向量长度是二维情况下的两倍,但两者指向相同的方向。图3-17显示了和它的标量乘积。
图3-17 乘以标量2之后返回指向同一方向的向量,该向量的长度是原向量的2倍
3.2.3 三维向量减法
在二维平面上,两个向量和的差值就是“从到”的向量,称为位移。在三维空间中也是一样的,换句话说,就是从到的位移,把这个向量与相加即可得到。将和看作从原点出发的箭头,那么的差值也是一个箭头,它的头部位于的头部,尾部位于的头部。图3-18显示了和的差值,它既是从到的箭头,本身也是一个点。
图3-18 从向量中减去向量,得到从到的位移
从向量中减去向量,在坐标上是通过取和的坐标之差来完成的。例如,的结果是(-1 - 3, -3 - 2, 3 - 4) = (-4, -5, -1),这些坐标与图3-18中的一致,表明它是一个指向负、负和负方向的向量。
在说乘以标量2让向量变成“2倍长”时,我是出于对几何相似性的考虑。如果将向量的3个分量分别翻倍,相当于框的长度、宽度和深度都翻倍,那么从一个角到其对角的距离也应该翻倍。为了实际测量和确认这一点,需要知道如何计算三维空间中的距离。
3.2.4 计算长度和距离
在二维平面上,我们通过勾股定理来计算向量的长度,因为箭头向量和它的分量构成了一个直角三角形。同样,平面内两点之间的距离也只是它们作为向量的差的长度。
计算三维空间中向量的长度需要更仔细地观察,不过仍然存在合适的直角三角形作为辅助。首先试着算出向量(4, 3, 12)的长度。分量和分量仍然构成了一个直角三角形的两条边,位于的平面中。这个三角形的斜边(对角线)长度为。如果这是二维向量,就已经计算完成了,但长度为12的分量拉长了这个向量(见图3-19)。
图3-19 应用勾股定理求平面内的斜边长度
到目前为止,我们研究的所有向量都位于平面内,其中。分量是(4, 0, 0),分量是(0, 3, 0),它们的向量和是(4, 3, 0)。分量(0, 0, 12)垂直于这三个向量。这很有用,因为有了它就有了图中的第二个直角三角形:由(4, 3, 0)和(0, 0, 12)两个向量首尾相连构成的三角形。这个三角形的斜边就是开始时想要计算长度的向量 (4, 3, 12)。接下来看第二个直角三角形,并再次使用勾股定理来算出斜边的长度(如图3-20所示)。
图3-20 再次使用勾股定理,算出三维向量的长度
计算两条已知边的平方,然后取平方根,就可以得到长度。这里两条边的长度是5和12,所以结果是。总而言之,下面是三维向量的长度公式。
长度
它恰好和二维长度公式很相似。无论对于二维还是三维,向量的长度都是其分量平方和的平方根。因为下面的length
函数并没有用到输入元组的长度,所以它对二维和三维向量都适用。
from math import sqrt
def length(v):
return sqrt(sum([coord ** 2 for coord in v]))
例如,length((3,4,12))
返回13。
3.2.5 计算角度和方向
像二维向量一样,三维向量可以被看作箭头或者沿一定方向发生的一定长度的位移。在二维平面上,这意味着两个数(一个长度和一个角度,构成一对极坐标)足以指定任何二维向量。在三维空间中,一个角度不足以确定方向,但两个可以。
对于第一个角度,可以再次考虑没有坐标的向量,就好像它仍然在平面上一样。另一种思考方式是,该角度是由来自非常高的位置的光投射在向量上形成的阴影。这个阴影与轴正方向形成一定的角度,类似于极坐标中的角度,并使用希腊字母来表示。第二个角度是向量与轴正方向的夹角,用希腊字母来表示。图3-21显示了这些角度。
图3-21 两个角度共同指定三维向量的方向
向量的长度用来表示,它与角度和一起可以描述三维空间中的任何向量。、和这三个数组成了球坐标系,与笛卡儿坐标系、和完全不同。仅使用之前讲过的三角学知识,根据笛卡儿坐标系计算球坐标是可行的,但这里不会深入讨论。事实上,本书不会再使用球坐标了,但我想简单地将其与极坐标比较一下。
对于极坐标,可以通过简单的角度加减来执行平面向量集合的任意旋转。对于极坐标,也能通过对两个向量的角度取差,来获取它们的夹角。在三维空间中,单凭角或都不能立即确定两个向量之间的角度。虽然通过加减角可以轻松地绕轴旋转向量,但在球坐标系中绕任何其他的轴旋转都不方便。
我们需要一些更通用的工具来处理三维空间中的角度和三角学。下一节会介绍两种这样的工具,称为向量积。
3.2.6 练习
练习3.3:将(4, 0, 3)和(-1, 0, 1)绘制为
Arrow3D
对象,使它们在三维空间中以两种顺序首尾相接。它们的向量和是多少?解:可以用我们的
add
函数找到向量和。>>> add((4,0,3),(-1,0,1)) (3, 0, 4)
为了绘制首尾相接的箭头,首先画出从原点到每个点的箭头,再画出从每个点到向量和(3, 0, 4)的箭头(见图3-22)。和二维的
Arrow
对象一样,Arrow3D
也先取箭头的头部向量,然后可选地取尾部向量(如果它不是原点)。draw3d( Arrow3D((4,0,3),color=red), Arrow3D((-1,0,1),color=blue), Arrow3D((3,0,4),(4,0,3),color=blue), Arrow3D((-1,0,1),(3,0,4),color=red), Arrow3D((3,0,4),color=purple) )
图3-22 首尾加法显示,(4, 0, 3) + (-1, 0, 1) = (-1, 0, 1) + (4, 0, 3) = (3, 0, 4)
练习3.4:假设设置
vectors1=[(1,2,3,4,5),(6,7,8,9,10)]
和vectors2=[(1,2),(3,4),(5,6)]
。在不使用Python求值的情况下,zip(*vectors1)
和zip(*vectors2)
的长度分别是多少?解:第一个
zip
的长度为5。因为两个输入向量中各有5个坐标,所以zip(vectors1)
包含5个元组,每个元组有两个元素。同样,zip(vectors2)
的长度为2。zip(vectors2)
的两个条目分别是包含所有分量和所有分量的元组。
练习3.5(小项目):下面的代码创建了一个包含24个Python向量的列表。
from math import sin, cos, pi vs = [(sin(pi*t/6), cos(pi*t/6), 1.0/3) for t in range(0,24)]
这24个向量的和是多少?把这24个向量绘制成首尾相接的
Arrow3D
对象。解:首尾相接地依次绘制这些向量,最终会形成螺旋状(见图3-23)。
from math import sin, cos, pi vs = [(sin(pi*t/6), cos(pi*t/6), 1.0/3) for t in range(0,24)] running_sum = (0,0,0) ←---- 在(0, 0, 0)处初始化动态和,从这里开始从头到尾相加 arrows = [] for v in vs: next_sum = add(running_sum, v) ←---- 绘制后续首尾相接的向量时,把它加到动态和上。最新的箭头把前一个动态和与下一个连接起来 arrows.append(Arrow3D(next_sum, running_sum)) running_sum = next_sum print(running_sum) draw3d(*arrows)
图3-23 求三维空间中24个向量的和
得到的和为:
(-4.440892098500626e-16, -7.771561172376096e-16, 7.9999999999999964)
大约是(0, 0, 8)。
练习3.6:编写函数
scale(scalar,vector)
,返回输入标量乘以输入向量的结果。具体地说,这个函数要同时适用于二维和三维向量,以及有任意多坐标的向量。解:通过推导运算,将向量中的每个坐标乘以标量。这是一个被转换成元组的生成器推导式。
def scale(scalar,v): return tuple(scalar * coord for coord in v)
练习3.7:设和。的结果是什么?
解:已知和,首先计算。那么就是(-1/2, 1/2, 3/2)。最终得到结果。顺便说一下,这正好是点和点的中点。
练习3.8:试着在不使用代码的情况下找到这个练习的答案,然后检查你的答案是否正确。二维向量(1, 1)的长度是多少?三维向量(1, 1, 1)的长度是多少?我们还没有讨论到四维向量,但是它们有四个坐标,而不是两个或三个。猜一下,坐标为(1, 1, 1, 1)的四维向量的长度是多少?
解:(1, 1)的长度为。(1, 1, 1)的长度是。正如你可能猜到的,同样的距离公式对高维向量也适用。(1, 1, 1, 1)的长度遵循同样的规律:它的长度是,也就是2。
练习3.9(小项目):坐标3、4和12能以任意顺序创建一个向量,其长度是整数13。这很不寻常,因为大多数数不是完全平方数,所以长度公式中的平方根通常返回无理数。找出另一组三个整数,以它们为坐标定义的向量也有整数长度。
解:下面的代码搜索满足条件的三元组,由小于100(可任意选择)的整数组成,并且整数按降序排列。
def vectors_with_whole_number_length(max_coord=100): for x in range(1,max_coord): for y in range(1,x+1): for z in range(1,y+1): if length((x,y,z)).is_integer(): yield (x,y,z)
它找到了869个具有整数坐标和整数长度的向量。最短的是(2, 2, 1),长度正好是3;最长的是(99, 90, 70),长度是150。
练习3.10:找到一个与(-1, -1, 2)方向相同但长度为1的向量。
提示:找到合适的标量与原向量相乘,以适当地改变其长度。
解:(-1, -1, 2)的长度大约是2.45,所以需要把这个向量乘以1/2.45,使其长度为1。
>>> length((-1,-1,2)) 2.449489742783178 >>> s = 1/length((-1,-1,2)) >>> scale(s,(-1,-1,2)) (-0.4082482904638631, -0.4082482904638631, 0.8164965809277261) >>> length(scale(s,(-1,-1,2))) 1.0
将每个坐标四舍五入到最接近的百分位,所求向量为(-0.41, -0.41, 0.82)。