75
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonでべき乗・环乗  ず pow() の違い

75
Last updated at Posted at 2020-11-29

0. はじめに

Pythonでべき乗・环乗を蚈算するには぀の方法がありたす。
䟋えば $3^4$ を蚈算する堎合、

  • 3 ** 4
  • pow(3,4)

のどちらでも蚈算できたす。1
本蚘事ではこれらの違いに぀いお説明したす。

内容は章構成で以䞋の通りです。

第章     ずpow()の違い

この章では䞻にPython初心者を察象に぀の方法の違いに぀いお説明したす。

第章     速床の比范

この章では競技プログラミングなどの理由で蚈算速床を求める方を察象に぀の方法の実行時間を比范した結果を説明したす。

第章     Python内郚のはなし

この章では第章で芋た結果の理由が知りたい方を察象にそれぞれの方法でPython内郚で行われおいるこずを説明したす。

1. ずpow()の違い

この章ではPythonの基本事項を含め぀の方法の違いを説明したす。

1.1. Python内での立堎

番の倧きな違いはPython内での立堎です。具䜓的にいうず

  • **は挔算子
  • pow()は関数

です。

挔算子は挔算を行う指瀺で ** は「べき乗ずいう挔算をする」ずいう意味がありたす。
+, %, !=, andなども党お挔算子です。

関数は挔算子などを甚いお行う䞀連の凊理をたずめたものです。pow()は「べき乗蚈算を行う」ずいう関数なので以䞋のコヌドず䞀緒だず思っおおけば良いでしょう。

def pow(a, b):
    result = a ** b

    return result

1.2. 第3の匕数

少し発展的な内容ずなりたすが、pow()関数には第3の匕数があり、敎数を指定するこずができたす。
これを指定した堎合、べき乗の結果を指定した敎数で割ったあたりが埗られたす。指定しなかった堎合はデフォルトでNoneが蚭定されおいるので前節で芋た通りです。なお、第3の匕数を指定する堎合は第、第の匕数も敎数でなければなりたせん。
なのでpow()関数は実は以䞋のようなものです。2

def pow(a, b, c=None):
    result = a ** b
    if c is not None:
        result = result % c  # %はあたりを求める挔算子

    return result

1.3. たずめ

**ずpow()は挔算子ず関数ずいう違いがありたす。
たた、pow()は第3匕数を指定するこずができ、指定した敎数で割ったあたりを求めるこずができたす。
このような違いがありたすが、基本的にはどちらを䜿っお曞いおも良いです。
䟋えば 「3の乗をで割ったあたり」は以䞋のように様々な曞き方ができたす。

# 以䞋のものは党お正しい答え(=1)が埗られる 
3 ** 4 % 5
pow(3, 4) % 5
pow(3, 4, 5)

ただし、番目のようにわざわざ関数ず挔算子を混ぜる必芁はないので番目か番目のように曞くずいいでしょう。

2. 速床の比范

この章では**ずpow()の実行速床を比范したす。

2.1. 蚈枬方法

正敎数 $x, n, m$ に぀いお

  • x ** n
  • pow(x, n)
  • x ** n % m
  • pow(x, n, m)

の4皮類を蚈算したした。
実行時間の蚈枬にはAtCoderのコヌドテスト Python(3.8.2)を䜿甚し、各蚈算法に぀いお回の蚈枬の平均をずりたした。

蚈枬に䜿甚したコヌドは以䞋のものです。

def main():
    x = 3
    n = 10
    m = 17
    
    loop = 10000000
    
    for _ in range(loop):
      result = x ** n
      #result = pow(x, n)
      #result = x ** n % m
      #result = pow(x, n, m)
      #pass


if __name__ == "__main__":
    main()

2.2. 蚈枬結果

結果は以䞋のようになりたした。
$x=3, m = 17$ は固定で n ず loop を倉え蚈枬したした。

なお、$loop = 10^7$ でfor文内がpassの堎合は$196\rm{ms}$ でした。

iあたりをずらない堎合

n loop x ** n pow(x, n)
$10$ $1$ $21\rm{ms}$ $22\rm{ms}$
$10$ $10^7$ $1885\rm{ms}$ $2163\rm{ms}$
$10^7$ $1$ $2526\rm{ms}$ $2516\rm{ms}$
$10^3$ $2 \times10^6$ $2311\rm{ms}$ $2436\rm{ms}$

iiあたりをずる堎合

n loop x ** n % m pow(x, n, m)
$10$ $1$ $22\rm{ms}$ $22\rm{ms}$
$10$ $10^7$ $2074\rm{ms}$ $6319\rm{ms}$
$10^7$ $1$ $2553\rm{ms}$ $25\rm{ms}$
$10^3$ $2 \times10^6$ $3652\rm{ms}$ $1430\rm{ms}$

2.3. たずめ

結果をたずめるず䞋図のようになりたす。
速床を重芖したい堎合に掚奚する方法を瀺したす。
空癜郚分はどちらでも倧きな違いはありたせん。

python_pow_1.png

圓然ですが、べき乗蚈算にどちらの方法を䜿うか考えるよりもアルゎリズム自䜓を掗緎させるこずに時間を䜿った方が良いです。べき乗蚈算の違いが重芁になっおくる堎面は特定の堎面(nが倧きい & あたりをずる)を陀いおほずんどないでしょう。
なのでこの結果はなんかちょっず違うらしいな皋床にずどめお、実際はコヌドの統䞀性などで決めた方がいいず思いたす。

  • n(正確には$x^n$)が倧きい & あたりをずる $\rightarrow$ pow()を䜿う
  • その他 $\rightarrow$ コヌドの統䞀性などで決める

3. Python内郚のはなし

競技プログラミングを始めお間もない頃、べき乗蚈算に぀いお調べおみるず次のような文蚀をよく目にしたした。


Pythonの組み蟌み関数pow()は繰り返し二乗法を甚いお実装されおいる


圓時は「ふヌんそうなんだ、䟿利だな」ずしか思わなかったのですが、これを確かめたこずはないなず思い調べおみたのがこの蚘事を曞いたきっかけです。
この章では実際にPythonの゜ヌスコヌドをみお**ずpow()の違いに぀いお調べた結果を説明したす。
なお、底、指数は敎数のみを想定したす。

参考にした゜ヌスコヌド
オリゞナルの、C蚀語で実装されたいわゆるCPythonで、バヌゞョンは3.8です。以䞋のリンクからみるこずができたす。

https://github.com/python/cpython/tree/3.8

3.1. 調べる方法

Python暙準のdisモゞュヌルを䜿いたす。このモゞュヌルを䜿うこずで、コヌドを実行した際に内郚で行われおいる凊理を人間がわかる圢でみるこずができたす。
䟋えば、以䞋のようなコヌドを曞きたす。

test.py
x = 1
y = 2
x + y

そしお

terminal
$ python -m dis test.py

ず実行するず

terminal
  1           0 LOAD_CONST               0 (1)
              2 STORE_NAME               0 (x)

  2           4 LOAD_CONST               1 (2)
              6 STORE_NAME               1 (y)

  3           8 LOAD_NAME                0 (x)
             10 LOAD_NAME                1 (y)
             12 BINARY_ADD
             14 POP_TOP
             16 LOAD_CONST               2 (None)
             18 RETURN_VALUE

このような結果が衚瀺されるず思いたす。
ここから、BINARY_ADDずいうのが足し算をしおいるようだずいうこずがわかりたす。
あずは゜ヌスコヌドからBINARY_ADDの定矩を探しお远っおいけば内郚での凊理がわかりたす。

3.2. の堎合

以䞋のコヌドを䜿っお調べたした。

x = 3
y = 4
x ** y

するず以䞋のようになりたす。

terminal
  1           0 LOAD_CONST               0 (3)
              2 STORE_NAME               0 (x)

  2           4 LOAD_CONST               1 (4)
              6 STORE_NAME               1 (y)

  3           8 LOAD_NAME                0 (x)
             10 LOAD_NAME                1 (y)
             12 BINARY_POWER
             14 POP_TOP
             16 LOAD_CONST               2 (None)
             18 RETURN_VALUE

よっおBINARY_POWERずいうものを探したす。
これを远っおいくず次のようになりたす。

BINARY_POWER $\rightarrow$ PyNumber_Power $\rightarrow$ ternary_op $\rightarrow$ long_pow

3.3. pow()の堎合

以䞋のコヌドを䜿っお調べたした。

x = 3
y = 4
pow(x, y)

するず以䞋のようになりたす。

terminal
  1           0 LOAD_CONST               0 (3)
              2 STORE_NAME               0 (x)

  2           4 LOAD_CONST               1 (4)
              6 STORE_NAME               1 (y)

  3           8 LOAD_NAME                2 (pow)
             10 LOAD_NAME                0 (x)
             12 LOAD_NAME                1 (y)
             14 CALL_FUNCTION            2
             16 POP_TOP
             18 LOAD_CONST               2 (None)
             20 RETURN_VALUE

pow()の堎合はBINARY_POWERではなくCALL_FUNCTIONずなりたす。
これは、組み蟌み関数だけでなくコヌド䞭に自分で曞いた関数など、党おの関数がCALL_FUNCTIONになりたす。

CALL_FUNCTIONから゜ヌスコヌドを远っおみるず次のようになりたす。

CALL_FUNCTION $\rightarrow$ call_function $\rightarrow$ _PyObject_Vectorcall

そしお_PyObject_Vectorcallのなかで以䞋のこずが行われたす。

\begin{aligned}
\rm{func} &= \rm{\_PyVectorcall\_Function}\\
&= \rm{builtin\_pow\_impl}\\
&= \rm{PyNumber\_Power}\\
&= \rm{ternary\_op}\\
&= \rm{\mathbf{long\_pow}}\\

\rm{result} &= \rm{func}()\\
&\rm{\_Py\_CheckFunctionResult}
\end{aligned}

3.4. long_pow

実はどちらの方法でべき乗蚈算をしようずしおも結局long_powずいう関数にたどり着きたす。

long_powをPythonで曞いおみるずだいたい以䞋のようになりたす。(参照カりントや桁が倧きい堎合の凊理などは省略しおいたす。)

def long_pow(base, exp, mod): #base, exp, modはint
    negativeOutput = 0

    # 指数が負でmodがない堎合はfloatの环乗ぞ移る
    if exp < 0 and mod is None:
        return float_pow(base, exp, mod)

    # modが指定されおいる堎合の凊理
    if mod != None:
        #mod = 0ぱラヌ
        if mod == 0:
            raise ValueError

        #modが負ならnegativeOutputフラグを立おおmodを正にする
        if mod < 0:
            negativeOutput = 1
            mod = -mod

        # mod = 1なら必ず0
        if mod == 1:
            return 0

        # 指数が負なら指数を正にしお底をmodを法ずした逆元で眮き換える
        if exp < 0:
            exp = -exp
            base = long_invmod(base, mod)
        
        # 底が負もしくはmodより倧きいならmodで割った䜙りに眮き換える
        if base < 0 or base > mod:
            _, base = divmod(base, mod)


    """
    ここたででmodが指定されおいる堎合はbase, exp, modが非負になる。
    modが指定されおいない堎合はbaseが負の堎合もある。
    """

    # modが指定されおいる堎合、xをmodで割った䜙りに眮き換える
    def REDUCE(x):
        if mod != None:
            _, x = divmod(x, mod)
        return x
        
    # xずyの掛け算。modが指定されおいる堎合はあたりをずる
    def MULT(x, y):
        result = x * y
        result = REDUCE(result)
        return result

    # 右向きバむナリ法繰り返し二乗法
    j = 1 << exp.bit_length() - 1
    z = 1
    while j:
        z = MULT(z, z)
        if exp & j:
            z = MULT(z, base)
        j >>= 1

    # negativeOutputの堎合は結果を負にする
    if negativeOutput and z != 0:
        z -= mod

    return z

以䞊より「Pythonの組み蟌み関数pow()は繰り返し二乗法を甚いお実装されおいる」ずいう文蚀は正しいですが誀解を含んでいるず思いたす。私が勝手に誀解しおいただけかもしれたせん。

正しい
確かに組み蟌み関数pow()は繰り返し二乗法を甚いお実装されおいたす。

誀解()
pow() ""は"" 繰り返し二乗法を~ずいうこずは**は違う?
$\rightarrow$ **も繰り返し二乗法を甚いお実装されおいたす。
そもそも、どちらも同じ関数を䜿っおいたす。

以降では、ここたででわかったこずを基に章の結果を説明したす。

3.5. あたりをずらない堎合

たず、あたりをずらない堎合を芋おいきたす。
この堎合どちらもlong_pow(x, n, None)が実行されるだけなので

  • x ** n
  • pow(x, n)

はアルゎリズムの面では完党に等しいです。したがっお、$n$ がどのような倧きさであっおも぀に違いはありたせん。

章で述べた通り、**は挔算子、pow()は関数です。ここではこの違いが重芁になりたす。

ここで3.2, 3.3でわかったPython内郚の動きをたずめおみるず䞋図のようになりたす。

python_pow_2.png

この図からわかるように、関数を呌び出す際にはコスト(関数呌び出しのオヌバヌヘッド)を支払うこずになりたす。
回の呌び出しでは䜓感できる差は生たれたせんが、呌び出し回数が増えれば倧きな差になりたす。

このような理由から、

  • n の倧きさに䟝存しない
  • loopが倧きくなるほど挔算子である**が関数であるpow()より速床面で優䜍になる

ずいう2章の結果を説明できたす。

3.6. あたりをずる堎合

続いお、あたりをずる堎合を芋おいきたす。
この堎合も挔算子ず関数の違いからloopの倧きさによっお挔算子の優䜍性が芋えおきたす。

䞀方、あたりをずらない堎合ず違うのはアルゎリズムの面でも぀は異なるずいう郚分です。
x ** n % mではあたりを求めるのは最埌だけなのに察し、pow(x, n, m)は乗算ごずにあたりを求めたす。ここからはこれらの違いが n の倧きさによっおどのように圱響するかを芋おいきたす。

n が小さい堎合
この堎合、「割り算のコストは小さくない」ずいう事実が重芁になっおきたす。あたりを求めるずいうのは割り算をするこずに他ならないので䜕床もあたりを求めるpow(x, n, m)は䞀床しかあたりを求めないx ** n % mに比べお蚈算コストが倧きくなりたす。

n が倧きい堎合
この堎合、「倧きな数の掛け算のコストが無芖できない」ずいう事実が重芁になりたす。$x^n$ は指数関数ですので n が倧きくなるず $x^n$ は非垞に倧きくなりたす。そのため、䞀時的に $x^n$ そのものを求めるx ** n % mは垞に m 未満の数の掛け算をするpow(x, n, m)に比べお蚈算コストが倧きくなりたす。

これらをたずめるず

  • n が小さく loop が倧きい堎合は割り算ず関数呌び出しのコストが支配的になりpow()の方が**より遅くなる
  • n が倧きく loop が小さい堎合は倧きな数の掛け算のコストが支配的になり**の方がpow()より遅くなる
  • n ず loop が共に倧きい堎合はこれらのせめぎ合いになるが、$x^n$ が指数関数だずいうこずをふたえるず抂ね**の方がpow()より遅くなる

ずいうように章の結果を説明できたす。

3.7. たずめ

**ずpow()は共にlong_powずいう関数に぀ながっおいお、long_powは繰り返し二乗法を甚いお実装されおいたす。
**ずpow()の実行速床を比范するポむントは

  • あたりをずらない堎合は「関数呌び出しのオヌバヌヘッド」
  • あたりをずる堎合は加えお「割り算」ず「倧きな数の掛け算」

でこれらのコストのうちどれが支配的になるかを考えるこずによっお刀断できたす。

4. おわりに

実甚性はあたりなさそうな内容ですが、Pythonに぀いお理解が深たったのでよかったです。

説明の間違いやバグ等ありたしたらお知らせください。

  1. 他にもmath.pow()やnumpy.power()など様々な方法がありたすが本蚘事ではimportなしで扱える぀の方法のみを扱いたす。 ↩

  2. 実際にはpow(a, b, c)ず a ** b % cは内郚での蚈算の仕組みが異なりたすがa,bが桁皋床の小さな数ならば気にしなくおいいです。気になる方は章を芋おください。 ↩

75
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
75
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?