Python 的取模运算 r = m % n 相当于

# 或q = math.floor(m / n)
q = m // n
r = m - q * n

即取模的结果是被除数减去地板除的商和除数的乘积,这一规则对正数、负数乃至浮点数皆适用。

n 为正数时。显然任意实数 x 可以表示为 x = r + k * n,其中 0 <= r < nk 是某个整数。那么有

x // n = floor(r/n + k) = k
x % n = x - x // n = r

x % n 的结果总是一个大小在 [0, n) 之间的实数 r。当 n = 10 时,以 x = 12x = -12 为例:

number

如果以 n 为一个周期,那么 x = 12 就相当于往右一个周期再走 2 格,x % n 会消去这个周期,剩下不满一个周期的 2;x = -12 相当于往左两个周期后再往右走 8 格,x % n 会消去这两个周期,剩下不满一个周期且为正数的 8。

再本质点说,取模运算就是在 [0, 10) 的窗口内进行“衔尾蛇”移动:

  • 12 向右超出窗口两格, 12 % 10 = 2,即右边出两格那就左边进两格。
  • -12 向左超出窗口 12 格,-12 % n = 8,即左边出 12 格那就右边进 12 格,发现还是超出左边两格,再从右边进两格,最后距离零点 8 格。

下面介绍取模运算的两个应用。

地球的经度以本初子午线为起点,自西向东绕行一圈,经度的数值从 0° 增长到 360°。不过经度还可以大于 360°,表示绕行一圈以上,甚至还可以是负数,表示自东向西绕行。显然这跟取模运算的衔尾蛇特性完美契合,通过取模运算可以将 [0, 360) 范围外的经度变换回这个范围内:

import numpy as np

lon = np.arange(-360, 720 + 1, 180)
print(lon)
print(lon % 360)
[-360, -180,    0,  180,  360,  540,  720]
[   0,  180,    0,  180,    0,  180,    0]

另外一个常用的经度范围是 [-180, 180),即经度跨过太平洋上的对向子午线时经度会从正数跳变到负数。问题是如何将 [0, 360] 范围内的经度变换到 [-180, 180) 范围内。显然 [-180, 180) 是一个窗口,我们希望范围在 [180, 360] 的经度从窗口右边离开,再从窗口左边进入。但因为窗口范围不满足 [0, n) 的形式,所以不能直接取模,而是应该先向右偏移 180°,在正轴完成衔尾蛇移动后再偏移回负轴:

(lon + 180) % 360 - 180

lon

注意,这一算法中 180° 会被算到 -180°,360° 会被算到 0°:

lon = lon = np.arange(0, 360 + 1, 180)
print(lon)
print((lon + 180) % 360 - 180)
[    0,  180,  360]
[    0, -180,    0],

第二个应用是将月份换算成季节。气候学上春季指 3、4、5 月份,夏季指 6、7、8 月份,秋季指 9、10、11 月份,冬季指 12 月和来年 1、2 月。这里暂时不考虑冬季跨年的问题(可参考笔者的 Period 文章),只是将 [1, 12] 的月份映射到 [1, 4] 上,1、2、3、4 分别表示春夏秋冬。

首先可以想到,地板除能将 12 个月等分为 4 组:

month = np.arange(1, 13)
print(month)
print((month - 1) // 3 + 1)
[ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12]
[ 1,  1,  1,  2,  2,  2,  3,  3,  3,  4,  4,  4]

可惜春天是在 1、2、3 月的基础上向右偏移两个月;冬天是在 10、11、12 月的基础上向右偏移两个月,超出 12 月的部分从左边重新进入(即 1、2 月)。那么可以考虑通过取模把月份向左“旋转”两格,让春天排在前三格的位置,冬天排在最后三格的位置,这样就能应用地板除做分组了:

(month - 3) % 12 // 3 + 1

season

当然,这两个问题都可以用更简单的方式来解决:经度可以用 np.where(lon > 180, 360 - lon, lon) 转换,季节可以用 if 判断或字典来做映射。但取模运算能将你的代码精简至一行,同时方便迷惑其它读者(大雾)。