見出し画像

Pythonの変数 関数の初期化

 新しいプログラミング言語の習得を始めるととかく最初に変数の振る舞いに苦戦する時期が訪れる。今思い出すとPythonもそうだった。僕の好きなYoutuberのひとりPythonプログラミングVTuberのサプーさんのメンバーシップ動画に出てきたサンプルでとても興味の沸いたコードがあり、それを考えていたらPythonを学び始めた当初に苦戦したことを思い出したので、振り返ってまとめてみた。まずは問題のコード

# sample01.py
def func(x, x_list=[]):
    x_list.append(x)
    return(x_list)

print(func(1))
print(func(2))

コードの解説
 定義されている関数には2つの引数があり、1番目の引数の値を2番目の引数のリストに追加して、戻り値として返す。ただ2番目の引数にはディフォルト値として空のリストが割り当ててある。プログラムは2度この関数を2番目の引数を省略して呼び出す。

このコードを実行してみると・・・

実行結果

$ python sample01.py
[1]
[1,2]

 となります。定義したfuncを2回呼び出しています。2回とも2番目の引数を省略していますので、通常は空のリストで初期化されると思われがちです。つまり予想実行結果はこんな感じでしょう。

[1]
[2]

 しかし2度目のfuncの呼び出しで初期化が行われず1度目の値を引き継いだ戻り値が返ってきてしまいます。

 ちなみにこれがリストでないとどうなるのかというと

#sample02.py
def func2(x, y=1):
    y += x
    return(y)


print(func2(1))
print(func2(10))

実行結果

# python sample02.py
1
11

 と想定どおりyは毎回1で初期化されています。前のyの値を引きづることはありません。 

Pythonっておもしろいですね

変数の初期化はいつ行われているのか?

  Pythonの初期化の流れを以下のような関数で調べてみましょう。

def func(s=datetime.datetime.now()):
    return(s)

 これを使えば初期化の処理が走った時刻が分かりそうです。これを使って以下のようなプログラムを実行してみます。

import datetime
import time

print(f'define func : time: {datetime.datetime.now()}')


def func(s=datetime.datetime.now()):
    return(s)


print(f'call func : time: {datetime.datetime.now()}')
print(func())

print(f'slee 10 sec')
time.sleep(10)

print(f'call func : time: {datetime.datetime.now()}')
print(func())

 これを実行してみると結果はこの様になります。

define func : time: 2022-07-07 20:12:38.225859
call func : time: 2022-07-07 20:12:38.225957
2022-07-07 20:12:38.225953
slee 10 sec
call func : time: 2022-07-07 20:12:48.236007
2022-07-07 20:12:38.225953

 10秒後にfuncをコールしても同じ値が返ってきますね。初期化は1回のみ呼ばれているようです。考えられるのは関数オブジェクトが作られた時でしょうか。次は関数を2つ定義してみましょう。2つの関数の定義の間に10秒間スリープを入れてみます。

import datetime
import sys

print(f'define func1 : time: {datetime.datetime.now()}')


def func1(s=datetime.datetime.now()):
    return(s)


print('sleep 1') 
sys.sleep(10)

print(f'define func2 : time: {datetime.datetime.now()}')


def func2(s=datetime.datetime.now()):
    return(s)


print('sleep 2') 
sys.sleep(10)

print(f'call func1 : time: {datetime.datetime.now()}')
print(func1()) 
print(f'call func2 : time: {datetime.datetime.now()}')
print(func2())

 この実行結果は

define func1 : time: 2022-07-07 20:17:50.762401
sleep 1
define func2 : time: 2022-07-07 20:18:00.772707
sleep 2
call func1 : time: 2022-07-07 20:18:10.782808
2022-07-07 20:17:50.762483
call func2 : time: 2022-07-07 20:18:10.782871
2022-07-07 20:18:00.772763

 こうなるとfunc2の初期化はsleep 1の後に実施されているようですね。Pythonはインタープリター言語ですから、スクリプトファイルを読み込んで一行一行実行し、関数の定義をする、その時に変数の初期化がされ関数オブジェクトと一緒にメモリーに展開されているように思えます。

 初期値が与えられた変数に値を渡して関数を呼び出すと、変数の値が引数として与えられた値にオーバーライトされたようになり動きます。その後また省略して呼び出すと関数が定義された時に初期化した値が使われます。

もし呼び出し時に初期化したい場合はどうする?

 さて最初のサンプルコードで思ったとおりに動かすためにはどういうコードを書けばいいでしょうか?いろいろとありますがおそらくもっとも簡単な書き方はこれではないでしょうか?

print(f'define func')


def func(x, x_list=None):
    if x_list == None:
        x_list = []
    x_list.append(x)
    return(x_list)


print(f'call func')
print(func(1))
print(func(2))
print(func(3, [1, 2]))

 これを実行すると、想定どおりになるように思います。

define func
call func
[1]
[2]
[1, 2, 3]

 funcが2回目の呼び出しでも、前の値は入っていません。2番目の引数を渡せば値が追加されています。


まとめ

 関数の初期化はその関数オブジェクトが定義されるタイミングで割り当てられ、関数呼び出し時にデフォルト値で初期化がされることはない。呼び出し時に値を入れると初期値は上書きされた様になる。


引用


この記事が気に入ったらサポートをしてみませんか?