Python で割り算をするときの切り上げと切り捨て



Python で割り算をするときの繰り上げと切り捨てについて、説明させていただきます。 3つのやり方があります。


1. 演算子のみを使う。

これがオススメ。欠点は、切り上げが伝わり辛い。利点は、表記が簡潔で実行速度が 10 倍速く、何も import しなくていい。

# 切り捨て
4 // 3
# 切り上げ
-(-4 // 3)

2. math モジュールを使う。

オススメしない。利点は、切り上げが伝わりやすいこと。欠点は、表記が冗長になり 10 倍遅く、math を import しないといけない。

import math
# 切り捨て
math.floor(4 / 3)
# 切り上げ
math.ceil(4 / 3)

3. int モジュールを使う。

オススメしない。利点は、無い。整数であることを強調したい時は、有効だと思います。欠点は、表記が冗長になり 10 倍遅い。

# 切り捨て
int(4 / 3)
# 切り上げ
# 無し

実行速度を比較する。

「1. 演算子のみを使う。」が、一番速い。10 倍も速い。 timeit は、与えられたコードを繰り返し実行して、その平均値を返してくれるツールです。

$ python -m timeit "4 // 3"
100000000 loops, best of 3: 0.0132 usec per loop
$ python -m timeit "(-(-4 // 3))"
100000000 loops, best of 3: 0.0132 usec per loop
$
$ python -m timeit -s "import math" "math.ceil(4 / 3)"
10000000 loops, best of 3: 0.136 usec per loop
$ python -m timeit -s "import math" "math.floor(4 / 3)"
10000000 loops, best of 3: 0.136 usec per loop
$
$ python -m timeit "int(4 / 3)"
10000000 loops, best of 3: 0.169 usec per loop
$

関数呼び出しは重い

ここで得られた知見は、Python の関数呼び出しは重いということです。

divmod という商と余りを一緒に返してくれる組み込み関数(import しなくても使える関数)があるのですが...

>>> divmod(5, 3)
(1, 2)
>>> 


それさえも割り算と除算を別個に行った方が、実行速度は早かったりします。 timeit は与えられたコードを繰り返し実行してその平均値を返してくれるツールです。

python3 -m timeit -s "a,b=5,3" "divmod(a, b)"
python3 -m timeit -s "a,b=5,3" "a//b"
$ python3 -m timeit -s "a,b=5,3" "divmod(a, b)"
2000000 loops, best of 5: 135 nsec per loop
$ python3 -m timeit -s "a,b=5,3" "a//b,a%b"
5000000 loops, best of 5: 89 nsec per loop
$


"5//3,5%3" ではなく "a//b,a%b" として最初に変数に代入しているのは、 直接数字を書いてしまうと最適化したコードをもとに実行時間を計測してしまうからです。

例えば "5//3,5%3" と書いて timeit で計測させると、 最適化されて "1,2" となり、そもそも除算する処理を省略して計測するだけになってしまうからです。 実際に計測してみると "5//3,5%3""1,2" は、計測時間がほぼ一致します。

# 直接数字で計算させると速くなる。
python3 -m timeit -s "a,b=5,3" "a//b,a%b"
python3 -m timeit "5//3,5%3"
python3 -m timeit "1,2"
$ # 直接数字で計算させると速くなる。
$ python3 -m timeit -s "a,b=5,3" "a//b,a%b"
5000000 loops, best of 5: 88.4 nsec per loop
$ python3 -m timeit "5//3,5%3"
50000000 loops, best of 5: 9.11 nsec per loop
$ python3 -m timeit "1,2"
50000000 loops, best of 5: 9.32 nsec per loop
$

あまり速度は気にしない方がいいかも...

速いとか遅いとか書いたのですが、正直あまり気にしなくていいかなと思います。 1回の割り算は、あまり時間を消費しないので。

もし時間が気になるようなら cProfile を使って一番処理が重たい時間を特定して、 そこから順番に対処して言くのがいいかなと思います。

割り算の書き方による計算時間が問題になることなんて、そうは滅多にないと感じたりします。
Python で実行時間を計測したい。




暗黙の型変換

割り算をするときに Python では int から float に自動的に型が変わります。 このような処理を難しい言葉で 暗黙の型変換, implicit type conversion と言います。

# 3 と 2 はそれぞれ int 型だけど
type(3) is int
type(2) is int

# 割り算をすると自動的に float 型になる
type(3/2) is float

暗黙の型変換は、明示的に指定しなくてもコンパイラの判断によって自動的に行われる型変換である。
型変換 - Wikipedia


Python には、その考え方を表現した PEP 20 という有名な言葉あります。 その中の文言に次のような言葉があります。

明示的であることは暗黙的であるより良い。
Explicit is better than implicit.
The Zen of Python


うまい説明が見当たらないのですが、 多少めんどくさくても変に自動化しないで明示的に書くようにした方が、 読みやすいコードになりますよ、ということなのかなと思っています。

では「暗黙の型変換」は、良いことなのでしょうか? Python 2 では、割り算の「暗黙の型変換」を許していませんでしたし。

>>> # Python 2
>>> # 割り算をすると整数が返ってくる
>>> 3 / 2
1
>>>


さらに、ごく初期の Python 2 よりももっと古い Python では足し算の「暗黙の型変換」さえ許していませんでした。

整数の割り算の問題 - The history of Python.jp
数値に関する大きな間違いは、高級言語ではなく、C言語に近いルールを採用してしまったことにある。 例えば、割り算を含む、標準の数値演算子の結果は、計算に使用したのと同じ型が常に返るようにした。 私は最初は、これとはまた別の間違ったルールを使用していた。それは、数値の型を混ぜて使うのを禁止したことである。 このルールは型の実装をお互いに独立させるのを目的としていた。 そのため、最初のPythonでは、int型にfloat型を足すこともできなかったし、int型とlong型を足すのもできなかったのである。


足し算の「暗黙の型変換」は、Tim Peters氏に、どう説得されたかは記述がないのですが、Python 1 の時点で、すぐに修正されたようです。

整数の割り算の問題 - The history of Python.jp
そのため、Pythonが一般向けにリリースされた直後に、Tim Peters氏から、このルールは本当に悪い考え方であるということを納得させられ、 通常の型強制ルールに従って数値型を混ぜて計算するモードを導入することになった。 例えば、int型とlong型が混ぜて使用された場合には、引数の型をint型からlong型に変換し、 計算結果はlong型を返す。また、float型が使用された場合には、int型やlong型の引数はfloat型に変換し、結果はfloat型で返すようになったのである。


割り算の「暗黙の型変換」については Python 2 ではしなかったけど、 計算ミスの原因になるから、Python 3 からは「暗黙の型変換」を導入したみたいです。

整数の割り算の問題 - The history of Python.jp
もし、例えば、月の満ち欠けの計算などである数値計算を行う関数を実装していたとしよう。 通常なら、引数として浮動小数点数を指定したいと思うだろう。 しかし、Python では型宣言がないため、呼び出し側が整数の引数を渡して呼び出すことを妨げることはできない。 C言 語のような静的な型を持つ言語であれば、コンパイラが強制的に型変換をして float にするが、 Python ではそのようなことはない。数値を混ぜるルールによって中間結果が float 型に変換されるまでは整数型で計算されることになる。


Python 2 のころは int という型の他に long という型がありました。簡単に言えば桁数の大きな int です。しかし、Python 3 では int と long は統合されました。詳しい経緯を調べたわけではないのですが、型を意識することなく計算できるという意味では、素晴らしい変更だと感じます。

# Python 2
type(2**62 - 1 + 2**62)  # <type 'int'>
type(2**63)  # <type 'long'>
# Python 3
type(2**62 - 1 + 2**62)  # <type 'int'>
type(2**63)  # <type 'int'>

初期の Python について

Python は教育用言語 ABC の影響を受けているせいか、かなり窮屈で色々な書き方ができない言語でした。 int と float の足し算さえできなかったのはそういうところにあるのかなと思ったりもします。 僕個人はそれがたまらなく好きでした。

int, float の足し算割り算の件については、 やはりあまりにも足し算、割り算をする頻度が高すぎるので explicit だと面倒だから、 implicit に暗黙の型変換を導入した方が良いということなのかなと思います。

Python は時が経つにつれて様々な書き方、文法が追加されているように感じます。 それはおそらく Guido 自身が様々な場面で Python を使う中で、 Guido の中で ABC の影響が薄れて、より実用性が重視されるようになったからかなと思ったりもします。

他言語について

僕個人は Explicit is better than implicit. 教の信者なので int と float が足し算できない仕様だと知ったきは、 そのままでよかったのにと一瞬思ってしまいました笑 動的型付けで型を意識しない Python のような言語の場合は、その方が良いかなと感じました。

ただ、常にそれが正義という訳ではなく、例えば静的型付けを行う Go 言語では暗黙の型変換を許してはくれません。 まだ読めてませんが、結局、ケースバイケースというわけです。 何故 Go が許してくれないのかは、また Go 言語を使うときに勉強したいと思っています。

Go 言語は、ご存知のように静的型付け言語です。さらに、異なる型間の暗黙的な変換を許容していません。
Go の定数の話 - Qiita


さっきから重箱の隅を突くような話をしてるなーと思われるかもしれません。 また、こんなの簡単だよーと思われるかもしれません。

しかし、この辺りの暗黙の型変換の設計に失敗すると、 例えば JavaScript のように等価演算子 == と厳密等価演算子 === の2つが定義されるという悲劇的な状態に陥ります。
JavaScript の == と === の違いを詳しくまとめてみる - Qiita

Python の開発者の方々には、本当に感謝の念が絶えません。 ちなみに下記のツイートの Armin Ronacher は Python の Web フレームワーク Flask の作者です。


暗黙の型変換の話はコードを自分で書くときに implicit にやるのか、それとも explicit にやるのかの良い判断基準の材料になるかなと思いご紹介させていただきました。 ちなみに Tim Peters氏 は PEP 20 の作者で、TimSort という有名なソーティングアルゴリズムの作者でもあります。
















2018/12/26 追記 ご指摘、誠にありがとうございます。 またご迷惑をおかけして申し訳ございません。 さきほど修正させていただきました。

Python で割り算をするときの切り上げと切り捨て - いっきに Python に詳しくなるサイト
※上記サイトサンプルコードのmath.ceilとmath.floorが逆になってしまっているので注意
Python: 切り上げ除算 - Snippets置き場