Pythonで7を2で割ったら、3ではなく3.5が返ってきた。逆に、整数で答えがほしいのにうまくいかない。そんな戸惑いを感じたことはありませんか?
Pythonの割り算には、/と//と%という3つの演算子があります。見た目はよく似ていますが、返ってくる値の種類も計算のルールもそれぞれ違います。
特に注意したいのが、マイナスの数を割ったときの動きです。-7 // 2の答えは、多くの人が予想する-3ではなく-4になります。
今回は、この3つの演算子とdivmod()関数の違いを、実際に動かした結果とともに整理していきます。サンプルの実行結果は、手元のPython 3.11で確かめたものです。
Pythonの割り算は3種類ある¶
まずは全体像をつかむために、3つの演算子を表で見比べてみましょう。どれも7と2を使った例です。
| 演算子 | 名前 | 7と2での結果 | 返ってくる型 |
|---|---|---|---|
| / | 真の除算 | 3.5 | 常にfloat |
| // | 切り捨て除算 | 3 | 整数どうしならint |
| % | 剰余(余り) | 1 | 整数どうしならint |
真の除算とは、数学で習う普通の割り算のことです。切り捨て除算は、割り算の答えを小さいほうの整数へ切り捨てた値を返します。
剰余は、割り算の余りを求める計算です。小学校で習った7÷2=3あまり1の、3が//で、1が%にあたると考えるとイメージしやすいでしょう。
/は割り切れても小数を返す¶
いちばんよく使うのが/です。Python 3では、整数どうしを割っても結果は必ずfloat型、つまり小数になります。
割り切れる場合でもこのルールは変わりません。実際に確かめてみましょう。
print(7 / 2) # 3.5
print(8 / 4) # 2.0
print(type(8 / 4)) # <class 'float'>
8 / 4の答えが2ではなく2.0になっているのがわかりますね。ここを知らないと、あとで文字列に埋め込んだときに2.0と表示されて驚くことになります。
Python 2では整数の割り算が切り捨てだった¶
実は、昔のPython 2では、整数どうしの/は切り捨ての割り算でした。7 / 2が3になっていたのです。
この仕様は、整数が混ざるだけで計算結果が変わってしまうため、バグの温床になっていました。そこでPEP 238という提案で、/は真の除算に、//は切り捨て除算にと役割が分けられ、Python 3から現在の形になりました。
古いブログ記事や書籍のサンプルで、7 / 2が3になると書かれていたら、それはPython 2時代の情報です。調べものをするときは、どのバージョンの話なのかを確認するクセをつけておくと安心ですね。
大きな整数を/で割ると精度が落ちる¶
/が常にfloatを返すことには、もうひとつ注意点があります。floatで正確に表せる整数には限りがあるため、とても大きな整数を/で割ると、下の桁が失われることがあります。
たとえば、10の17乗に1を足した数を1で割ってみると、その違いがはっきりします。
n = 10**17 + 1
print(n // 1) # 100000000000000001
print(int(n / 1)) # 100000000000000000
//を使った場合は末尾の1がきちんと残っています。一方で/を経由すると、floatに変換された時点で1が消えてしまいました。
IDや金額のように、1の位まで正確さが求められる整数を扱うときは、/ではなく//を使いましょう。小数の誤差そのものについては、こちらの記事で詳しく解説しています。
【関連記事】Pythonの丸め誤差とは?初心者にもわかる原因と対策
//は小さいほうへ切り捨てる割り算¶
//は、割り算の答えを切り捨てて整数にしてくれる演算子です。整数どうしなら、結果もint型で返ってきます。
ただし、片方がfloatのときは結果もfloatになります。次の例で確かめてみましょう。
print(7 // 2) # 3
print(7.5 // 2) # 3.0
print(-7 // 2) # -4
7.5 // 2の結果は3ではなく3.0です。//を使えば必ずintになると思い込んでいると、ここでつまずきます。
そして3行目の-7 // 2が、今回いちばん伝えたいポイントです。答えは-3ではなく-4になっています。
切り捨ての向きはゼロではなくマイナス方向¶
なぜ-4になるのでしょうか。それは、Pythonの//が数直線の小さいほうへ切り捨てるというルールで動いているからです。
-7 ÷ 2の本当の答えは-3.5です。-3.5より小さい整数のうち、いちばん近いのは-4ですよね。これを専門用語では床関数(floor)と呼び、math.floor()と同じ向きの切り捨てになります。
一方で、int()で小数を整数に変換すると、ゼロに近いほうへ切り捨てられます。こちらはmath.trunc()と同じ動きです。
| 書き方 | -7と2での結果 | 切り捨ての向き |
|---|---|---|
| -7 // 2 | -4 | マイナス無限大の方向 |
| int(-7 / 2) | -3 | ゼロの方向 |
| math.floor(-7 / 2) | -4 | マイナス無限大の方向 |
| math.trunc(-7 / 2) | -3 | ゼロの方向 |
正の数だけを扱っているうちは、どちらの方法でも結果は同じです。違いが表に出るのは、マイナスの数が混ざったときだけです。
int()による型変換の動きについては、こちらの記事でまとめています。
【関連記事】Pythonの型変換とは?int()・float()・str()・bool()の使い方
%は割り算の余りを求める¶
%は剰余演算子と呼ばれ、割り算の余りを返します。7 % 3なら1、10 % 5なら割り切れるので0です。
余りの計算は地味に見えますが、実務ではとても出番が多い演算子です。代表的な使い方を次のコードにまとめました。
# 偶数か奇数かを判定する
for n in [4, 7]:
if n % 2 == 0:
print(n, 'は偶数')
else:
print(n, 'は奇数')
# 3回に1回だけ処理をする
for i in range(7):
if i % 3 == 0:
print(i, '回目: ログを出力')
n % 2が0なら偶数、それ以外なら奇数です。i % 3 == 0のような書き方は、大量のデータを処理するときに進捗を間引いて表示する場面でよく使います。
マイナスの余りは割る数の符号に合わせる¶
%にも、マイナスの数で気をつけたい動きがあります。Pythonの%の結果は、割る数(右側)と同じ符号になるというルールです。
試しに-7 % 3を計算すると、答えは-1ではなく2になります。これは、次の関係が常に成り立つように決められているからです。
a, b = -7, 3
print(a // b, a % b) # -3 2
print((a // b) * b + (a % b) == a) # True
商に割る数をかけて余りを足すと、元の数に戻る。この関係を守るために、//がマイナス方向へ切り捨てるなら、%は割る数と同じ符号になるわけです。
Python公式のFAQでも、この設計の理由として時計の例が挙げられています。いま10時なら200時間前は何時かを考えると、-190 % 12が2になるほうが便利だというわけです。
曜日の計算も同じ考え方です。日曜日を0とした曜日番号から1日戻すとき、(0 - 1) % 7と書けば、ちゃんと土曜日の6が返ってきます。
divmod()で商と余りをまとめて受け取る¶
商と余りの両方がほしい場面では、組み込み関数のdivmod()が便利です。divmod(a, b)は、(a // b, a % b)のペアをタプルで返してくれます。
たとえば、秒数を分と秒に変換するコードは次のように書けます。
total = 3725
minutes, seconds = divmod(total, 60)
hours, minutes = divmod(minutes, 60)
print(f'{hours}時間{minutes}分{seconds}秒') # 1時間2分5秒
//と%を別々に書くよりも、何をしたいのかが一目で伝わりますね。動画の再生時間やバッチ処理の所要時間を表示するときに、よく使う書き方です。
私は10年ほどエンジニアとして開発に関わってきましたが、この時間変換を//と%で書いたコードに、60と3600の打ち間違いが紛れ込んでいたことがあります。divmod()を2段重ねにする書き方に変えてからは、レビューでも読みやすいと言われるようになりました。
割り算でよく出会うエラーと落とし穴¶
ここからは、割り算まわりで初心者がつまずきやすいポイントを見ていきます。どれも実際の開発でよく見かけるものです。
0で割るとZeroDivisionErrorになる¶
/・//・%のどれを使っても、0で割ろうとするとZeroDivisionErrorという例外が発生します。ユーザーの入力や集計結果で割り算をするときは、事前に0でないかを確かめておくと安全です。
平均値を求める処理で、データが0件だったために落ちる。これは本当によくあるパターンです。
私自身、新人のころに売上の平均を出すバッチを書いて、売上が1件もない日に処理が止まってしまった失敗があります。それ以来、割る数がどこから来た値なのかを必ず確認するようにしています。
例外が起きたときの対処方法は、こちらの記事が参考になります。
【関連記事】Pythonのtry・except・else・finallyとは?例外処理の基本の流れ
小数どうしの//は直感とずれることがある¶
floatで//や%を使うと、思わぬ結果になることがあります。たとえば1 // 0.1を計算すると、10.0ではなく9.0が返ってきます。
これは、0.1がコンピュータの中ではぴったり0.1ではなく、ほんの少しだけ大きな値として保存されているためです。1を少しだけ大きい数で割るので、切り捨てると9になってしまいます。
金額の計算のように正確さが必要な場面では、floatではなくdecimalモジュールを使うのがおすすめです。
【関連記事】Pythonのdecimalとは?
割り算の切り上げは//の工夫で書ける¶
23件のデータを1ページ10件ずつ表示するとき、必要なページ数は3です。このような切り上げの割り算は、math.ceil(23 / 10)で求められます。
ただ、大きな整数ではfloatの精度が気になりますよね。そんなときは-(-23 // 10)と書くと、floatを経由せずに整数のまま切り上げられます。
マイナスにしてから切り捨て、もう一度マイナスにすると切り上げになる。//がマイナス方向へ切り捨てる性質を逆手に取った、ちょっとした小技です。
他の言語との違いに気をつけよう¶
最後に、他のプログラミング言語を知っている人向けの注意点です。C言語やJavaでは、整数の割り算はゼロの方向へ切り捨てられ、余りは割られる数と同じ符号になります。JavaScriptの%も、割られる数と同じ符号の余りを返します。
言語によって答えが変わる様子を、表で見比べてみましょう。
| 計算 | Python | C言語・Java |
|---|---|---|
| 7 ÷ 2(整数どうし) | 7 // 2 → 3 | 7 / 2 → 3 |
| -7 ÷ 2(整数どうし) | -7 // 2 → -4 | -7 / 2 → -3 |
| -7の2での余り | -7 % 2 → 1 | -7 % 2 → -1 |
正の数だけなら、どの言語でも同じ答えになります。違いが出るのはマイナスの数を扱うときだけなので、気づきにくいのが厄介なところです。
JavaやJavaScriptで書かれた処理をPythonへ移植するときは、マイナスの割り算がないかを特に注意して確認してください。他の言語と同じ動きにしたい場合は、int(a / b)やmath.fmod()を使うと、ゼロ方向の切り捨てや割られる数に合わせた符号の余りが得られます。
まとめ¶
Pythonの割り算は、目的に応じて演算子を使い分けるのが基本です。小数の答えがほしいなら/、整数の商がほしいなら//、余りがほしいなら%を選びましょう。
商と余りの両方が必要なときは、divmod()でまとめて受け取るとコードがすっきりします。そして、マイナスの数では//はマイナス方向へ切り捨て、%は割る数と同じ符号になることを覚えておけば、思わぬバグを防げます。
正の数だけを扱っているうちは、この違いに気づく機会はほとんどありません。だからこそ、いまのうちに一度手元で-7 // 2を実行して、自分の目で確かめておくことをおすすめします。
ここまでお読みいただきありがとうございました。