Skip to content
This page has been auto-translated and may contain errors.View in English

数字和算术

docs.scrimba.com

几乎你写的每一个程序都会用到数字。购物车计算价格。游戏更新分数。脚本计数某件事发生的次数。Python 为你提供了像纸上数学一样工作的算术运算符,再加上一些值得从一开始就知道的额外运算符。

Python 的算术运算符涵盖了标准集合加上整除、模运算和幂运算。一些行为与其他语言不同,在实践中很重要:/ 总是返回浮点数,地板除法向负无穷舍入,模运算遵循真实模运算语义。

Python 整数没有溢出(在具有固定大小整数的语言中会出现的环绕或错误):它们根据内存允许的范围增长。浮点数是通常的双精度小数,对于大多数工作都足够,但对金钱永远不精确。算术运算符遵循数学定义而不是许多语言复制的 C 约定:// 是地板除法(向负无穷舍入),% 采用除数的符号,而不是被除数的符号。两种选择都保持相同的结果,无论你的输入是正还是负,这是你对循环和换行想要的行为。

运算符

数学中的四个运算符(+-*/)的工作方式完全如你所预期。Python 另外添加了三个你会经常使用的运算符:整除、余数和幂运算。

四个标准运算符的行为如预期,有一个显著的规则:/ 总是返回浮点数,即使结果是整数。三个额外的运算符扩展了你无需额外工作就能表达的内容。

每个运算符都连接到一个双下划线方法(一个用双下划线命名的方法,Python 在幕后调用):+ 连接到 __add__// 连接到 __floordiv__% 连接到 __mod__** 连接到 __pow__ 等等。实际的好处是,你自己的类可以定义那些方法,然后直接使用 +*,就像内置数字一样。在一个操作中混合 intfloat 总是给出 float/ 返回 float 无论你给它什么。

python
price = 12.99
quantity = 3

print(price * quantity)   # 38.97
print(price + 2)          # 14.99
print(price - 1.00)       # 11.99
运算符名称示例结果
+加法5 + 38
-减法5 - 32
*乘法5 * 315
/除法5 / 31.6666...
//整除5 // 31
%余数5 % 32
**幂运算5 ** 3125
Juno运算符+-*/ 像纸上的数学一样工作。三个额外的运算符值得早期学习:// 整除到整数,% 给你余数,** 计算幂。我使用 % 的频率远比我预期的要多。
Juno运算符 四个基本运算符如预期工作,有一条规则要牢记:/ 总是返回 float。额外的运算符有 // 用于整除,% 用于余数,** 用于幂运算。每一个都为你省去了编写辅助函数的工作。
Juno运算符 每个运算符都路由到一个双下划线方法,所以定义了 __add____mul__ 的类可以直接接入 +*。混合 intfloat 总是扩展到 float/ 无论你给什么都返回 float

除法:/ vs //

/ 总是给你精确的小数结果,即使答案是整数。// 丢弃小数部分并向负无穷舍入。对于正数这意味着截断小数,但对于负数它从零向外走一步:

/ 总是返回 float,无论输入是否为整数。// 返回结果的地板:小于或等于真实结果的最大整数。对于正数这与截断相同。对于负数,不是:

/ 是真实除法,总是返回 float// 是地板除法:它向负无穷舍入真实商,而不是许多其他语言所做的向零截断。在实践中为什么这很重要:Python 的 //% 对每个整数输入都满足 a == (a // b) * b + (a % b),包括负数。截断除法破坏了负数的这个恒等式,所以你写的任何循环或换行逻辑都保持正确,无需特殊处理符号。

python
10 / 2     # 5.0   (总是 float,即使它能整除)
10 / 3     # 3.3333333333333335

10 // 3    # 3
7 // 2     # 3
-7 // 2    # -4    (向负无穷舍入,而不是向零舍入)

你主要会在正数上使用 //。当它们出现时请记住负数的行为。

Juno除法:/ vs /// 总是返回小数,即使它能整除:4 / 22.0// 丢弃小数部分,但要注意负数:-7 // 2-4,而不是 -3,因为它向下舍入而不是截断。这是我第一次做的时候才意识到的。

Python 将其称为地板除法,因为它应用了数学地板函数。其他语言向零截断,对于负数给出不同的结果。名字 // 是一个提示:除,然后向下舍入。

Juno除法:/ vs /// 总是返回 float,即使 4 / 2 给出 2.0// 向下舍入而不是截断,所以 -7 // 2-4。对于正数这两个看起来相同,差异仅在负数上出现。

// 计算 floor(a / b),而不是截断。它也对浮点数工作:7.5 // 23.0,向下舍入的商以 float 而不是 int 的形式返回。

Juno除法:/ vs //// 向下舍入商,所以它对每个输入都满足 a == (a // b) * b + (a % b),包括负数。这是你在循环逻辑中应该依靠的性质。注意 float 操作数使 // 返回 float,比如 7.5 // 2 给出 3.0

余数运算符 %

% 给你整除后剩余的部分。如果 10 // 33(因为 3 进入 10 三次),那么 10 % 31(因为 3 × 3 = 9,10 - 9 = 1)。最常见的用法是检查一个数字是奇数还是偶数。

% 是模运算符。偶数/奇数检查是最常见的用法,但它推广到任何循环或换行问题:将计数器保持在范围内、将项分配给组、重复序列。模式总是 value % limit,它返回 0limit - 1 之间的某个值。

Python 的 % 是真实模运算:结果总是采用除数的符号。许多其他语言采用被除数的符号,所以对于 -7 % 3 它们会给出 -1,而 Python 给出 2。Python 的答案来自将模运算定义为 a - (a // b) * b,其中 // 向负无穷舍入。好处是 value % limit 即使在 value 为负时也保持在 0limit - 1 范围内,所以在列表末尾换行的索引无需保护子句就在范围内。

python
10 % 3    # 1
10 % 2    # 0  (整除)
10 % 7    # 3

6 % 2     # 0  (偶数)
7 % 2     # 1  (奇数)
Juno余数运算符 %% 是整除后剩余的部分。10 % 31 因为 3 进入 10 三次余 1。你会主要在偶数/奇数检查上遇到它:n % 2 对偶数是 0,对奇数是 1
Juno余数运算符 %% 是模运算,它远不止偶数/奇数:任何时候你需要将一个值保持在范围内,value % limit 将其换行到 0limit - 1。计数器、组分配、重复循环,都是相同的形状。
Juno余数运算符 %% 遵循除数的符号,所以 -7 % 32,而不是 -1。这就是让 value % limit 对负输入保持在范围内的原因,无需对换行索引的保护子句。

幂运算 **

** 将一个数字提升到一个幂。使用两个星号,而不是 ^ 符号(在 Python 中表示别的东西):

** 是幂运算。它也对浮点数工作,这让你能将根表示为分数幂,而不是一个单独的函数调用:

** 计算幂,分数幂形式很方便:n ** 0.5 是平方根,无需导入任何东西。两个 int 操作数给出 int,任何 float 操作数给出 float。在你对大值或精确值工作之前要知道一件事:9 ** 0.5 通过浮点数学进行,所以对某些输入它可能着陆在 2.9999999999999996。当精确性很重要时,math.isqrt() 对于整数根和 math.pow() 是需要权衡的工具,涵盖在模块章节中。

python
2 ** 10    # 1024
3 ** 3     # 27
9 ** 0.5   # 3.0  (平方根:计算 0.5 次幂)
Juno幂运算 **** 将一个数字提升到一个幂,所以 2 ** 101024。分数幂给你一个根:9 ** 0.53.0。唯一的陷阱是符号,使用 **,而不是 ^,它在 Python 中做别的东西。
Juno幂运算 **** 是幂运算,它取浮点数,所以 9 ** 0.5 给你一个平方根,无需单独的函数调用。当你想要一个快速的内联根时,使用分数幂形式。
Juno幂运算 ** 两个 int 给出 int,任何 float 给出 float。使用 ** 0.5 根技巧的问题是它通过浮点数学运行并可能漂移,所以当精确性有问题时,使用 math.isqrt()math.pow()

运算符优先级

Python 遵循标准数学顺序:幂运算优先,然后乘法和除法,然后加法和减法。当你不确定时,使用括号。它们使意图清晰,成本为零:

Python 遵循标准 PEMDAS/BODMAS 顺序。让人们困惑的部分:///% 都共享相同的优先级别,当混合时从左到右求值。括号是免费的;只要顺序不是一眼就清楚,就使用它们:

从最高到最低的算术运算符的优先级:**,然后一元 -(单个值前面的减号),然后 * / // %(共享一个级别时从左到右),然后 + -。在真实代码中出现陷阱的一件事:-2 ** 2-(2 ** 2),即 -4,因为 ** 的绑定比前导减号更紧密。如果你曾经平方一个可能为负的值,将其括号化为 (-2) ** 2,否则符号会错误而没有错误警告你。

python
2 + 3 * 4      # 14, 不是 20
2 ** 3 + 1     # 9,  不是 512
10 - 4 / 2     # 8.0, 不是 3.0

(2 + 3) * 4    # 20
10 / (2 + 3)   # 2.0
Juno运算符优先级 与纸上数学相同的顺序:幂优先,然后乘法和除法,然后加法和减法。当顺序一眼不清楚时,添加括号。它们成本为零并清楚地表达你的意思。
Juno运算符优先级 标准 PEMDAS 顺序,有一个问题:///% 都坐在同一级别,混合时从左到右运行。从左到右的规则是人们误读的部分,所以当分组不清楚时括号化。
Juno运算符优先级 陷阱是一元减号的绑定比 ** 更松散,所以 -2 ** 2-4。任何可能为负的东西平方为 (-2) ** 2,否则符号翻转而没有错误标记它。

intfloat 如何交互

Python 有一个一致的规则:/ 总是返回小数,即使 4 / 2 给出 2.0任何混合整数和小数的操作也给出小数。当你需要一个整数时,使用 // 或用 int() 转换。

类型规则是可预测的:/ 总是返回 float//% 在两个整数上返回 int。任何混合 intfloat 的操作返回 float。这意味着 4 / 22.0,而不是 2,当你需要一个整数时这很重要(例如,用作索引)。

规则是固定的且值得内化:int 在任何时候与 float 混合时扩展到 float/ 总是返回 float,而 // 只在两个操作数都是 int 时返回 int。这防止的 bug 是一个 float 偷偷进入需要整数的东西,比如列表索引或字典键,其中 2.02 不可互换。当计算可能产生 float 而你需要 int 时,在那个点用 int() 转换,而不是希望除法产生整数。

python
4 / 2      # 2.0   (float, 总是)
4 // 2     # 2     (int)
4 + 2      # 6     (int)
4 + 2.0    # 6.0   (float)
4 * 0.5    # 2.0   (float)
Junoint 和 float 如何交互/ 总是给出小数,所以 4 / 22.0,而不是 2。在任何地方混合一个整数与小数,你会得到小数。当你需要一个整数时,使用 // 或用 int() 包装它。
Junoint 和 float 如何交互/ 总是 float//% 在两个 int 上保持 int,任何 intfloat 混合出来是 float。让人们抓住的问题是 4 / 22.0 当你想要一个索引时。
Junoint 和 float 如何交互 来自 / 的一个散落的 float 破坏任何需要精确 int 的东西,比如索引或字典键,其中 2.0 不是 2。在那个边界用 int() 转换,而不是相信除法会产生整数。

浮点精度

有一个陷阱几乎每个人在某个时刻都会惊讶:

python
0.1 + 0.2   # 0.30000000000000004

那个微小的错误不是 Python bug。计算机用二进制存储小数,某些值如 0.1 无法精确表示。它类似于 1/3 无法在十进制中精确写出。对于大多数日常计算这不重要。对于显示金钱,round():.2f 格式说明符将保持输出整洁。

浮点数给你大约 15 到 16 位有效十进制数字的精度。不精确浮出水面是因为某些分数无法在二进制中精确存储,所以 0.1 + 0.2 产生 0.30000000000000004。漂移仅在你检查原始值时显示;用 :.2fround() 格式化隐藏了输出中的它。

对于分数积累的金融工作,Python 在标准库中提供 decimal.Decimal,带有精确的基 10 算术。这在模块章节中涵盖。

float 以二进制(基数 2)存储数字,任何分母不是 2 的幂的分数(比如 1/10)没有精确的二进制形式,就像 1/3 没有精确的十进制形式一样。这就是 0.1 + 0.2 着陆在 0.30000000000000004 的全部原因。每个操作的错误很小,但它在长链算术中积累,所以两种计算相同总数的方式可能在最后几位数字不同。

防止 bug 的规则:永远不要对金钱使用 float,也不要用 == 测试浮点数的精确相等。当你需要精确的基 10 算术时使用 decimal.Decimal(计费、会计),或当你需要精确的比率而完全不舍入时使用 fractions.Fraction。两者都在 Python 的标准库中,在模块章节中涵盖。

Juno浮点精度 计算机用二进制存储小数,某些值如 0.1 没有精确的二进制形式,所以 0.1 + 0.2 变成 0.30000000000000004。它不是 Python bug,每种语言都这样。对于日常输出,round():.2f 保持它整洁。
Juno浮点精度 二进制无法精确保持每个十进制,所以 0.1 + 0.2 漂移到 0.30000000000000004。对大多数数学都好,:.2fround() 整理显示。当美分必须相加时,切换到 decimal.Decimal
Juno浮点精度 拯救你的规则:金钱无浮点,浮点上永远无 ==,因为错误在长链操作中积累。对精确基 10 使用 decimal.Decimal,对精确比率使用 fractions.Fraction

可读的数字字面量

Python 允许你在数字字面量中放入下划线来使大数字更可读。Python 完全忽略它们;它们是为了你:

下划线在数字字面量中的任何地方有效,在解析时被剥离,对值没有影响。对常数中的千位分隔符很有用,对二进制或十六进制字面量中的数字分组有用:

下划线在 Python 看到值之前被剥离,所以它们对值没有零影响。它们在整数、浮点和基数字面量中工作(0xFF_FF0b1010_00011_234.567_890),这使得它们对在十六进制常数中分组字节很方便,不仅在十进制中的千位。唯一的限制:下划线不能坐在开始、结束或小数点或指数标记旁边。

python
population = 8_100_000_000
distance_km = 384_400
pi_approx = 3.141_592_653
Juno可读的数字字面量 将下划线放入一个长数字来使其可读:8_100_000_0008100000000 是相同的值。Python 完全忽略它们,它们是为了你的眼睛,而不是解释器。
Juno可读的数字字面量 下划线在数字字面量内的任何地方有效并在解析时消失,所以值不变。好用于常数中的千位分隔符,对二进制或十六进制中的数字分组同样有用。
Juno可读的数字字面量 下划线在整数、浮点和基数字面量中装饰,所以 0xFF_FF0b1010_0001 读得更清晰。仅有的规则:不在开始、结束或小数点或指数标记旁边。

有用的内置函数

abs()

abs() 返回绝对值:总是正,无论输入的符号如何。当你关心一个数字离零有多远,而不是哪个方向时使用它。

abs() 返回一个数字的大小。对整数和浮点数工作。对距离计算、误差边界和任何方向无关而你仅需要值大小的情况有用。

abs() 返回一个数字的大小,返回类型与输入匹配:一个 int 输入给出 int 输出,一个 float 给出 float。日常用法是比较两个值离目标多远,无论它们落在哪一边,例如 abs(measured - expected) < tolerance 作为容差检查,它比两个针对有符号范围的比较读得更清晰。

python
abs(-5)     # 5
abs(3.7)    # 3.7
abs(-0.5)   # 0.5
Junoabs()abs() 剥离符号并返回正大小:abs(-5)5abs(3.7)3.7。当你关心一个数字离零有多远,而不是它倾向于哪个方向时使用它。
Junoabs()abs() 给出 intfloat 的大小,方向丢弃。这是编写距离和误差边界检查的干净方式,无论符号不是问题的地方。
Junoabs()abs() 保持输入类型,int 保持 int。值得保持的模式是 abs(measured - expected) < tolerance:一个容差检查而不是两个比较围绕有符号范围。

round()

round() 默认舍入到最近的整数。传递第二个参数来保持特定数量的小数位:

python
round(3.7)          # 4
round(3.2)          # 3
round(3.14159, 2)   # 3.14

值得知道的一件事:round(2.5) 给出 2,而不是 3。当一个值正好在两个选项之间的一半时,Python 舍入到最近的偶数。

round() 使用银行家舍入:当值正好在一半时,它舍入到最近的偶数,而不是总是向上。这在统计工作中最小化了累积错误,但如果你期望 0.5 总是向上舍入,可能会让你惊讶:

python
round(2.5)   # 2  (舍入到最近的偶数)
round(3.5)   # 4
round(4.5)   # 4  (不是 5)
round(3.14159, 2)   # 3.14

round() 使用向偶数舍入(银行家舍入):精确的一半去到最近的偶数整数,而不是总是向上。这防止舍入错误在长列图形中朝一个方向偏置,这就是为什么它是统计工作的默认值。两个实际的注意事项。首先,一个 float 一半一旦在二进制中往往不是精确的一半,所以 round(2.675, 2) 给出 2.67,而不是 2.68,因为 2.675 无法精确存储。其次,当美分必须相加时,用 decimal.Decimal 和显式舍入模式做它,而不是依靠 round() 在浮点数上。

python
round(2.5)   # 2
round(3.5)   # 4
round(4.5)   # 4
Junoround()round(x) 去到最近的整数,round(x, n) 保持 n 小数位。惊讶:一个值正好在一半时舍入到最近的偶数,所以 round(2.5)2,而不是 3
Junoround()round() 使用银行家舍入,所以精确的一半去到最近的偶数:round(2.5)2round(4.5)4。它在聚合工作中减少偏置,但当你假设一半总是向上时期望它。
Junoround() 银行家舍入加二进制意味着 round(2.675, 2)2.67,因为 2.675 在幕后不是精确的一半。对于货币,用 decimal.Decimal 和选定的模式舍入,而不是相信 round() 在浮点数上。

divmod()

divmod() 在一个单独的调用中给你商和余数。它返回一对值,一个元组(在元组和集合章节中涵盖),你可以同时分配给两个名字:

divmod(a, b) 等价于 (a // b, a % b) 但在单个步骤中计算。当你无论如何都需要两个值时使用它:分页、时间转换或将项分配给组。

divmod(a, b) 做一次除法并返回地板商和模余数作为一对,所以你避免通过 // 然后 % 重新计算相同的除法。它读得最清晰在你无论如何都需要两个一半的地方:时间转换(divmod(seconds, 60))、分页或在固定大小行中布置项。

python
divmod(10, 3)   # (3, 1): 商 3, 余数 1
divmod(7, 2)    # (3, 1)
divmod(9, 3)    # (3, 0)

quotient, remainder = divmod(10, 3)
print(quotient)    # 3
print(remainder)   # 1
Junodivmod()divmod(a, b) 一起返回商和余数:divmod(10, 3)(3, 1)。你可以用 quotient, remainder = divmod(10, 3) 同时解包两者。
Junodivmod()divmod(a, b)(a // b, a % b) 在一个步骤中。当你需要两个片段时使用它,比如时间转换或分页,而不是计算两次除法。
Junodivmod() 一个除法,两个结果:divmod() 跳过了分离 //% 的冗余工作。它读得最好在你想要两个一半的地方无论如何,比如 divmod(seconds, 60) 或行布置。

实践中

一个小费计算器:

python
bill = 45.50
tip_rate = 0.18
tip = round(bill * tip_rate, 2)
total = round(bill + tip, 2)

print(f"Bill:  ${bill}")
print(f"Tip:   ${tip}")
print(f"Total: ${total}")

round() 保持输出看起来像金钱,而不是长长的小数位。

为分页计数页面并将进度跟踪为百分比:

python
total_items = 153
items_per_page = 10

full_pages, leftover = divmod(total_items, items_per_page)
total_pages = (total_items + items_per_page - 1) // items_per_page

print(f"Full pages: {full_pages}, leftover: {leftover}")
print(f"Total pages needed: {total_pages}")   # 16
python
total_files = 847
processed_files = 312

percent = round(processed_files / total_files * 100, 1)
print(f"Progress: {processed_files}/{total_files} ({percent}%)")

天花板除法公式 (n + d - 1) // d 是一个标准的整数技巧,无需转换到 float 就能向上舍入。

最小值-最大值规范化和百分比变化:两个在数据工作中不断出现的模式:

python
# 最小值-最大值规范化:将值缩放到 0.0 到 1.0 范围
value = 75
minimum = 0
maximum = 100

normalised = (value - minimum) / (maximum - minimum)
print(f"Normalised: {normalised:.2f}")   # 0.75

# 两个测量值之间的百分比变化
before = 1_200
after = 1_380

change = (after - before) / before * 100
print(f"Change: {change:.1f}%")          # 15.0%

两个模式都归结为一个比率:一个值相对于参考范围或参考大小。float 的精度对大多数分析工作都足够;累积错误仅在计算链接数十个操作或涉及相差许多数量级的值时才重要。