リファクタリングをPythonで実践!基本手法とコード例・注意点まで
数字を扱う上で欠かせない「四捨五入」や「切り上げ」「切り捨て」の処理。
Pythonの場合、人間が思っている結果と異なる値を出力する場合があります。
参考書によっては載っていない内容になりますので、独学されている方は必見ですよ。
1.Pythonのround()関数とは?
round()関数は、Pythonの標準関数(組み込み関数)のひとつです。
公式ドキュメントでは以下のようにround()関数を紹介しています。
一見すると私たち人間がイメージする四捨五入と同じ結果を出力してくれそうですが、入力値によっては以下のように人間とは違う結果を出力します。
例えば
入力値 1.125
丸める桁数 2
プログラム
print( round(1.125, 2) )
結果
1.12
小数点以下3桁目の5を四捨五入すると繰り上がって1.13になりそうですが、Pythonのround()関数は上記の出力1.12が標準です。
なぜこのような結果を出力するか、どのように対応すればいいかなどをご紹介していきます。
2.round()関数の使い方
round()関数は、pip install なしですぐに使える関数です。基本的な書き方をご紹介します。
①基本構文
round()関数には、2つの引数を設定できます。
#サンプルコード
round(1.234, 3)
第1引数は整数型もしくはフロート型を入力し、第2引数は整数型を入力します。
処理結果のデータ型は、小数点以下がある場合はフロート型、小数点以下がない場合は整数型の値が返ってきます。
以下に詳しくご紹介していきます。
②第1引数のみ指定して整数に丸める
round()関数を第2引数なしで使用した場合、以下のような整数に四捨五入されます。
case_1 = round(1.2)
case_2 = round(1.55)
case_3 = round(2.5)
case_4 = round(999999999999999999999.999999999999999)
print(case_1)
print(case_2)
print(case_3)
print(case_4)
print(type(case_1))
結果
1
2
2
1000000000000000000000
<class 'int'>
小数点以下1桁目の値によって四捨五入されています。
ただし、3つ目の値「2.5」は四捨五入すると「3」になるはずですが、Pythonは「2」を出力しています。不思議ですね。
これについては後述します。
③第2引数を指定して任意の桁に丸める
round()関数の第2引数を設定して、四捨五入を行ってみます。このケースも、先程同様に少し注意が必要です。
第2引数が正のとき
round()関数の第2引数に「正(プラス)」の値を設定して、実行してみます。
今回は、小数点以下1桁で丸めるように、「1」を第2引数に設定しました。
case_1 = round(1.2, 1)
case_2 = round(1.25, 1)
case_3 = round(2, 1)
case_4 = round(999999999999999999999.999999999999999, 1)
print(case_1)
print(case_2)
print(case_3)
print(case_4)
print(type(case_1))
結果
1.2
1.2
2
1e+21
<class 'float'>
第1引数の値によって、さまざまな処理結果を取得できました。
この中で「1.25」の四捨五入値が「1.2」。あれ、「1.3」になるべきではないでしょうか?
第2引数が負のとき
四捨五入というと小数点以下を処理するイメージが強いですが、プログラムの世界では整数部分の四捨五入もできます。
case_0 = round(154, -1)
case_1 = round(154, -2)
case_2 = round(12345, -1)
case_3 = round(12345.678, -1)
case_4 = round(10000, -1)
case_5 = round(999999999999999999999.999999999999999, -1)
print(case_0)
print(case_1)
print(case_2)
print(case_3)
print(case_4)
print(type(case_1))
結果
150
200
12340
12350.0
10000
<class 'int'>
round()関数の第2引数に「-1」を設定することで、1の位を四捨五入します。「-2」を設定すると10の位を四捨五入。私たちの日常生活ではあまり使わない四捨五入になりますが、データ処理を扱う上では役に立つ処理ですね。
上記プログラムの変数case_2とcase_3、どちらも同じ1の位を四捨五入するように設定していますが、出力結果はどうでしょうか?
思っていた結果と異なる出力をする場合もあるround()関数も正しく使えば、安心して利用できます。
下記項目を参考にしてください。
3.round()関数の結果が予想とちがう理由は?
round(1.25, 1)が「1.2」、round(12345, -1)が「12340」など、私たち人間とは異なる結果を出力する理由をご紹介します。
①「銀行家の丸め」を使っているから
Pythonのround()関数は、通称「銀行家の丸め」と同じ結果を出力します。
「銀行家の丸め」とは、四捨五入を判定する値が「5」の場合、絶対的に数字を繰上げ処理するのではなく、結果が偶数になる方に丸める、という処理です。
実際にプログラムを見てみましょう。
n_1 = round(1.25, 1)
n_2 = round(1.35, 1)
n_3 = round(1.45, 1)
n_4 = round(1.55, 1)
n_5 = round(1.15, 1)
print(n_1)
print(n_2)
print(n_3)
print(n_4)
print(n_5)
print(type(n_1))
結果
1.2
1.4
1.4
1.6
1.1
<class 'float'>
「1.25」や「1.45」は、小数点2桁目を四捨五入すると結果が「1.3」「1.5」と奇数になりますが、Pythonのround()関数では偶数となる「1.2」や「1.4」を出力します。
ただ「1.15」も同じ理屈で考えると「1.2」になりそうですが、結果は「1.1」。あれ、「1.1」は奇数では、とツッコミたくなりますよね。
Pythonのround()関数が、なぜこのような処理結果を出力するか、Python側の事情を確認してみましょう。
②2進数では小数点以下を表現しきれないことがあるから
私たち人間の世界の数字は「10進数」です。Pythonをはじめプログラムが動くコンピューターの世界では、数字は「2進数」です。
見た目は同じ「1.25」でも、数字の基準が異なります。
そして2進数では、10進数の小数点以下を正しく再現できないケースがほとんどです。
・1.25の10進数 1.25
・1.25の2進数 1.01
・1.15の10進数 1.15
・1.15の2進数 1.0010011001・・・
このように画面上は「1.25」「1.15」と見えている数字も、コンピューターの内部的には一度2進数化を経て10進数として表示されています。
その結果、「1.25」は「1.25000000000・・・」ですが、「1.15」は「1.149999999999999
・・・」という数字がコンピューターの持つ本当の値です。
Pythonのround()関数を用いた場合も、一度2進数化を経て四捨五入されるため、私たちがイメージする結果とは異なる数字を出力する場合がある、ということになります。
ただ、コンピューターやPythonの都合で思い通りに四捨五入を扱えないのは不便ですよね。
以下にround()関数とは違う方法で四捨五入する方法をご紹介します。
4.一般的な四捨五入をするには?
Pythonの標準モジュール「decimal(デシマル)」を使うと、正確な四捨五入値を取得できます。
①decimalモジュールのROUND_HALF_UPモードを使う
decimalモジュールは、四捨五入や切り上げなどの丸め目規則を8種類、演算過程で生じる例外処理を9種類もつモジュールになります。
decimalモジュールを用いて四捨五入する方法をご紹介します。
基本的なdecimalモジュールの書き方
Decimal(文字型の値).quantize( Decimal(丸める桁数), rounding=丸め方 )
1.25を小数点以下2桁目で四捨五入する場合は、以下のようになります。
from decimal import Decimal, ROUND_HALF_UP
n_str = str(1.25)
result = Decimal( n_str ).quantize(Decimal('0.1'), rounding=ROUND_HALF_UP)
print(result)
結果
1.3
round()関数では正しく四捨五入できなかった「1.25」も、「1.3」と予定通りの結果を出力することができました。
丸める桁数は、上記では「0.1」と小数点以下1桁に丸める処理を行いました。小数点以下2桁に丸めたい場合は「0.01」、小数点以下3桁に丸める場合は「0.001」と記述します。
②formatも「銀行家の丸め」を使っているので注意
文字列の中に効率よく変数値を割り当てる方法に「formatメソッド」があります。
formatメソッドの中にも四捨五入をする機能はありますが、round()関数と同じ結果を返してしまうのです。
n_format_1 = "{:.1f}".format(1.23)
n_format_2 = "{:.1f}".format(1.25)
print(n_format_1)
print(n_format_2)
結果
1.2
1.2
文章内に四捨五入した正確な値を入れたい場合は、一度decimalモジュールで処理して、それから文字列内に挿入するようにしましょう。
5.切り捨て、切り上げをするには?
数字を切り捨て、切り上げする方法を4種類ご紹介します。
①「0に近づける」切り捨て
小数点以下を切り捨てる方法には、int()関数とdecimalモジュールのROUND_DOWNがあります。
この二つの特徴は、フロート型が正の場合も負の場合も、0に近づくように切り捨てられるというところです。
int()
整数型に変換するint()関数を用いると、0に近づく切り捨てを実行します。
n_float_plus_1 = 1.25
n_float_plus_2 = 1.84
n_float_minus_1 = -1.25
n_float_minus_2 = -1.84
n_int_plus_1 = int(n_float_plus_1)
n_int_plus_2 = int(n_float_plus_2)
n_int_minus_1 = int(n_float_minus_1)
n_int_minus_2 = int(n_float_minus_2)
print(n_int_plus_1)
print(n_int_plus_2)
print(n_int_minus_1)
print(n_int_minus_2)
結果
1
1
-1
-1
正の値も負の値も単純に、小数点以下が切り捨てられていることが確認できました。
decimalモジュールのROUND_DOWNモード
任意の小数点で切り捨てを行いたい場合は、decimalモジュールを活用できます。
from decimal import Decimal, ROUND_DOWN
n_float_plus_1 = 1.25
n_float_plus_2 = 1.86
n_float_minus_1 = -1.25
n_float_minus_2 = -1.86
n_str_plus_1 = str(n_float_plus_1)
n_str_plus_2 = str(n_float_plus_2)
n_str_minus_1 = str(n_float_minus_1)
n_str_minus_2 = str(n_float_minus_2)
n_str_plus_1 = Decimal( n_str_plus_1 ).quantize(Decimal('0.1'), rounding=ROUND_DOWN)
n_str_plus_2 = Decimal( n_str_plus_2 ).quantize(Decimal('0.1'), rounding=ROUND_DOWN)
n_str_minus_1 = Decimal( n_str_minus_1 ).quantize(Decimal('0.1'), rounding=ROUND_DOWN)
n_str_minus_2 = Decimal( n_str_minus_2 ).quantize(Decimal('0.1'), rounding=ROUND_DOWN)
print(n_str_plus_1)
print(n_str_plus_2)
print(n_str_minus_1)
print(n_str_minus_2)
結果
1.2
1.8
-1.2
-1.8
上記プログラムは、小数点以下1桁目に丸めるようにプログラムしましたので、小数点以下1桁目より下で切り捨てられていることが確認できます。
②「負の無限大に近づける」切り捨て
経営計画などのシミュレーション時には、計算結果をネガティブな方に設定したいときもありますよね。
小数点以下を負の方向に切り捨てる方法を2種類ご紹介します。
math.floor()
mathモジュールのfloor()関数を用いると、小数点以下に値がある場合、マイナス方向に整数として出力します。
import math
n_floor_plus_1 = math.floor(1.25)
n_floor_plus_2 = math.floor(1.86)
n_floor_minus_1 = math.floor(-1.25)
n_floor_minus_2 = math.floor(-1.86)
print(n_floor_plus_1)
print(n_floor_plus_2)
print(n_floor_minus_1)
print(n_floor_minus_2)
結果
1
1
-2
-2
正の値と負の値がある場合は、混乱しやすくなりますので注意しましょう。
decimalモジュールのROUND_FLOORモード
任意の小数点以下で負の方向に切り捨てを行いたい場合は、decimalモジュールのROUND_FLOORを使います。
from decimal import Decimal, ROUND_FLOOR
n_float_plus_1 = 1.25
n_float_plus_2 = 1.86
n_float_minus_1 = -1.20
n_float_minus_2 = -1.86
n_str_plus_1 = str(n_float_plus_1)
n_str_plus_2 = str(n_float_plus_2)
n_str_minus_1 = str(n_float_minus_1)
n_str_minus_2 = str(n_float_minus_2)
n_str_plus_1 = Decimal( n_str_plus_1 ).quantize(Decimal('0.1'), rounding=ROUND_FLOOR)
n_str_plus_2 = Decimal( n_str_plus_2 ).quantize(Decimal('0.1'), rounding=ROUND_FLOOR)
n_str_minus_1 = Decimal( n_str_minus_1 ).quantize(Decimal('0.1'), rounding=ROUND_FLOOR)
n_str_minus_2 = Decimal( n_str_minus_2 ).quantize(Decimal('0.1'), rounding=ROUND_FLOOR)
print(n_str_plus_1)
print(n_str_plus_2)
print(n_str_minus_1)
print(n_str_minus_2)
結果
1.2
1.8
-1.2
-1.9
小数点以下1桁目で丸めるように設定しましたので、小数点以下2桁目がある場合は、小数点以下1桁目をマイナス方向に丸められました。
③「0から遠ざける」切り上げ
比率を用いた統計処理などを行う場合、正の値はプラス方向に丸め、負の値はマイナス方向に丸めたい場合があります。
そのようなときに使用するのがdecimalモジュールのROUND_UPです。
decimalモジュールのROUND_UPモード
decimalモジュール処理のroundingにROUND_UPを設定すると0から遠ざける丸め処理を実行できます。
from decimal import Decimal, ROUND_UP
n_float_plus_1 = 1.22
n_float_plus_2 = 1.86
n_float_minus_1 = -1.22
n_float_minus_2 = -1.82
n_str_plus_1 = str(n_float_plus_1)
n_str_plus_2 = str(n_float_plus_2)
n_str_minus_1 = str(n_float_minus_1)
n_str_minus_2 = str(n_float_minus_2)
n_str_plus_1 = Decimal( n_str_plus_1 ).quantize(Decimal('0.1'), rounding=ROUND_UP)
n_str_plus_2 = Decimal( n_str_plus_2 ).quantize(Decimal('0.1'), rounding=ROUND_UP)
n_str_minus_1 = Decimal( n_str_minus_1 ).quantize(Decimal('0.1'), rounding=ROUND_UP)
n_str_minus_2 = Decimal( n_str_minus_2 ).quantize(Decimal('0.1'), rounding=ROUND_UP)
print(n_str_plus_1)
print(n_str_plus_2)
print(n_str_minus_1)
print(n_str_minus_2)
結果
1.3
1.9
-1.3
-1.9
小数点以下1桁目で丸めるようにプログラムしていますので、小数点以下2桁目が0以上の場合は、小数点以下1桁目の値を変更します。
数値が正の場合は小数点以下1桁目に1が足され、負の値の場合は、小数点以下1桁目から1が引かれる、という処理です。
④「正の無限大に近づける」切り上げ
今度は、負の方向に丸めるROUND_FLOORとは逆の、正の方向に丸める処理について紹介します。
math.ceil()
正の方向に整数として丸める処理には、mathモジュールのceil()関数が便利です。
import math
n_ceil_plus_1 = math.ceil(1.22)
n_ceil_plus_2 = math.ceil(1.86)
n_ceil_minus_1 = math.ceil(-1.22)
n_ceil_minus_2 = math.ceil(-1.86)
print(n_ceil_plus_1)
print(n_ceil_plus_2)
print(n_ceil_minus_1)
print(n_ceil_minus_2)
結果
2
2
-1
-1
小数点以下1桁目が0以上の場合は、整数1桁目が正の方向に丸められます。
decimalモジュールのROUND_CEILINGモード
任意の桁数で正の方向に丸めたい場合は、decimalモジュールのROUND_CEILINGを使います。
from decimal import Decimal, ROUND_CEILING
n_float_plus_1 = 1.22
n_float_plus_2 = 1.86
n_float_minus_1 = -1.20
n_float_minus_2 = -1.89
n_str_plus_1 = str(n_float_plus_1)
n_str_plus_2 = str(n_float_plus_2)
n_str_minus_1 = str(n_float_minus_1)
n_str_minus_2 = str(n_float_minus_2)
n_str_plus_1 = Decimal( n_str_plus_1 ).quantize(Decimal('0.1'), rounding=ROUND_CEILING)
n_str_plus_2 = Decimal( n_str_plus_2 ).quantize(Decimal('0.1'), rounding=ROUND_CEILING)
n_str_minus_1 = Decimal( n_str_minus_1 ).quantize(Decimal('0.1'), rounding=ROUND_CEILING)
n_str_minus_2 = Decimal( n_str_minus_2 ).quantize(Decimal('0.1'), rounding=ROUND_CEILING)
print(n_str_plus_1)
print(n_str_plus_2)
print(n_str_minus_1)
print(n_str_minus_2)
結果
1.3
1.9
-1.2
-1.8
小数点以下1桁目で丸めるようにプログラムしましたので、小数点以下2桁目があり、正の値の場合は、小数点1桁目に1が足される結果になりました。負の値の小数点以下1桁目は、変わっていないことに注意しましょう。