見出し画像

pythonプログラム初歩の初歩11/関数とスコープ

タイトル:pythonプログラム初歩の初歩11/
関数とスコープ


こんにちはmakokonです。プログラム書いていますか?
今日のテーマは関数とスコープです。スコープとは変数や関数などの名前がプログラム内で参照できる範囲のことです。
つまり、ある変数がプログラムのどこで利用できるかが決まっているということです。
そんなのが何の役に立つのでしょうか?利用できる範囲が決まっているならば別の関数を書いたときに、同じ名前を使ってもお互いに干渉しないということなんです。
本当に基本的なことなんですが、プログラミング言語の主力をCからpythonに移行したときに、当たり前と思ってスルーしていたので改めて確認です。


ローカルスコープとグローバルスコープ

概念の説明

  • グローバルスコープ: ある変数がプログラムのどこからでもアクセスできる状態。プログラム全体で共有される変数で、どこからでもその変数の値を読み書きできます。

  • ローカルスコープ: ある変数が特定の関数やブロック内でのみアクセスできる状態。その関数やブロックの中で定義された変数は、外部からは見えず、外部の影響を受けません。

スコープは、プログラムの構造を理解し、変数や関数の可視性を管理するための重要な概念です。スコープを適切に使用することで、名前の衝突を避け、プログラムの読みやすさや保守性を向上させることができます

確認コード

グローバル変数とローカル変数がどう見えるか確認します。

# lesson11-00.py
# グローバル変数
global_var = "この変数はグローバル変数です"

def demo_function():
    # ローカル変数
    local_var = "この変数はローカル変数です"
    print("関数内部:")
    print("ローカル変数の値:", local_var)
    print("グローバル変数の値:", global_var)

# 関数を呼び出す
demo_function()

print("\n関数外部:")
# グローバル変数は関数外部からもアクセス可能
print("グローバル変数の値:", global_var)

# ローカル変数は関数外部からはアクセス不可能
# 以下の行を実行するとエラーになります
# print("ローカル変数の値:", local_var)

$ python lesson11-00.py
関数内部:
ローカル変数の値: この変数はローカル変数です
グローバル変数の値: この変数はグローバル変数です

関数外部:
グローバル変数の値: この変数はグローバル変数です

実行結果

このように、グローバル変数は関数の外でも、関数の中でも参照できますが、ローカル変数は関数の中からしか参照できません。
コードの最後の行のコメントアウトを外すと
NameError: name 'local_var' is not definedのエラーがでます。

同じ名前はローカル優先

関数の中と外で同じ名前の変数があったらどうなるんでしょう。
関数の中で、ローカル変数が使えなくなるのも困りますし、グローバル変数が勝手に書き換わるのも許されないことです。当然ローカル優先のはずですが、確認してみます。

確認コード

単純なループで関数を繰り返し呼び出してみます。
ループ制御には同じ変数名 'i' を使います。

# lesson11-01.py

def demo_function():
    # ローカル変数
    for i in range(2):
        print("ローカル",i)

# グローバル変数
# 関数を呼び出す
for i in range(3):
    print("グローバル",i)
    demo_function()

$ python lesson11-01.py
グローバル 0
ローカル 0
ローカル 1
グローバル 1
ローカル 0
ローカル 1
グローバル 2
ローカル 0
ローカル 1

実行結果

外側のループも内側のループも全く独立に動いていますね。

グローバル変数を関数内で変更する

普段のプログラムでは、余りおすすめしませんが、グローバル変数を関数内で変更することも試してみましょう

正しくないコード

# lesson11-02.py
# グローバル変数
global_var = "この変数はグローバル変数です"

def demo_function():
    # ローカル変数
    print("関数内部:")
    print("グローバル変数の値:", global_var)

    print("グローバル変数の値を変更します")
    global_var="この変数はグローバル変数で今変更されました"
    print("グローバル変数の値:", global_var)

# 関数を呼び出す
demo_function()

print("\n関数外部:")
# グローバル変数は関数外部からもアクセス可能
print("グローバル変数の値:", global_var)

$ python lesson11-02.py
関数内部:
Traceback (most recent call last):
File "lesson11-02.py", line 15, in <module>
demo_function()
File "lesson11-02.py", line 8, in demo_function
print("グローバル変数の値:", global_var)
UnboundLocalError: local variable 'global_var' referenced before assignment

実行結果

エラーになってしまいました。global_varへの代入文があるので、global_varはローカル変数優先の原則によって、ローカル変数になっています。ローカル変数なのに、何も代入しないうちにprintしようとしたからエラーになったんですね。
つまり、グローバル変数を変更するにはなにか技がいるということです。


修正コード globalキーワード

# lesson11-03.py
# グローバル変数
global_var = "この変数はグローバル変数です"

def demo_function():
    global global_var 
    # ローカル変数
    print("関数内部:")
    print("グローバル変数の値:", global_var)

    print("グローバル変数の値を変更します")
    global_var="この変数はグローバル変数で今変更されました"
    print("グローバル変数の値:", global_var)

# 関数を呼び出す
demo_function()

print("\n関数外部:")
# グローバル変数は関数外部からもアクセス可能
print("グローバル変数の値:", global_var)

$ python lesson11-03.py
関数内部:
グローバル変数の値: この変数はグローバル変数です
グローバル変数の値を変更します
グローバル変数の値: この変数はグローバル変数で今変更されました

関数外部:
グローバル変数の値: この変数はグローバル変数で今変更されました

実行結果

確かに、関数呼び出しによって、グローバル変数が変更できました。外部の変数を操作するには、global宣言が必要なのですね。
変数をどこでも操作できるなんてとても便利です。(白目)

グローバル変数を関数内で変更することの危険性

最初に、「余りおすすめしませんが」と言いました。なぜでしょう。
それは、以下のような危険性があって、プログラム初心者は理由がわからなくなることが多いからです。そして、ベテランプログラマーは後々の、保守の面倒さを考えて、結局使いたがらないという。

  1. 可読性の低下: グローバル変数を使うことで、プログラムのどこからでも変数にアクセスできるようになりますが、それが原因でプログラムの流れを追いにくくなり、可読性が低下します。特に大規模なプログラムや多人数での開発の場合、どこでその変数が変更されているのかを追うのが難しくなります。

  2. デバッグの困難さ: グローバル変数の値が予期せず変更されると、その原因を突き止めるのが難しくなります。関数やメソッド間で不意に値が変わると、バグの原因を特定するのが一層困難になり、デバッグのプロセスが遅延します。

  3. テストの難易度の増加: グローバル変数に依存するコードは、その状態に強く依存するためテストが難しくなります。特に単体テストを行う際に、グローバル変数の状態を管理することが難しくなり、テストの信頼性が低下する可能性があります。

  4. 並行処理での問題: マルチスレッドやマルチプロセス環境では、複数のスレッドやプロセスが同じグローバル変数にアクセスすると、予期しない競合状態やデータの不整合が発生する可能性があります。これはデータの整合性を保つ上で大きな問題となり得ます。

つまり、関数を呼び出す立場からすると、自分の管理している変数が、予告なしに変更される可能性があるということであり、わけがわからないということですね。

安全なグローバル変数の関数内での変更方法

pythonでは、この目的を実現することができるかっこいいやり方(クラスのインスタンス変数で管理とか)もあるのですが、初歩の初歩なので初心者らしく変更することがわかるような優しいやり方を説明します。この方法は正しいアプローチなので、ベテランになっても、カッコ悪くありませんよ。

# lesson11-04.py
# グローバル変数
global_var = "この変数はグローバル変数です"

def demo_function(global_var):
    # ローカル変数
    print("関数内部:")
    print("渡されたグローバル変数の値:", global_var)

    print("グローバル変数の値を変更します")
    global_var="この変数はグローバル変数で今変更されました"
    print("グローバル変数の値:", global_var)
    return global_var

# 関数を呼び出す
# この関数の結果がglobar_varに代入される
global_var = demo_function(global_var)

print("\n関数外部:")
# グローバル変数は関数外部からもアクセス可能
print("グローバル変数の値:", global_var)

$ python lesson11-04.py
関数内部:
渡されたグローバル変数の値: この変数はグローバル変数です
グローバル変数の値を変更します
グローバル変数の値: この変数はグローバル変数で今変更されました

関数外部:
グローバル変数の値: この変数はグローバル変数で今変更されました

実行結果

関数の戻り値を利用してglobal_varに代入することで、ちゃんと意識してグローバル変数を変更していることが、関数の内容を確認しなくてもわかります。このように書いておけば、グローバル変数の値が途中で変わっていても関数呼び出しのときに書き換えたことがわかりますね。

これは、安全なプログラム方法と言えます。ただし、やり方はかっこ悪くないと言いましたが、この関数の変数名はかっこ悪いかもしれない。

関数の引数はローカル変数?

11-04プログラムでは、引数で与えたグローバル変数をしれっと変更しましたが、本当にこれはローカル変数なのでしょうか?確認しておきましょう。こんな疑問を持つのもさっきの変数がダサいからです。

# lesson11-05.py
# グローバル変数
global_var = "この変数はグローバル変数です"

def demo_function(global_var):
    # ローカル変数
    print("関数内部:")
    print("渡されたグローバル変数の値:", global_var)

    print("グローバル変数の値を変更します")
    global_var="この変数はグローバル変数で今変更されました"
    print("グローバル変数の値:", global_var)
    #return  global_var

# 関数を呼び出す
demo_function(global_var)

print("\n関数外部:")
# グローバル変数は関数外部からもアクセス可能
print("グローバル変数の値:", global_var)

$ python lesson11-05.py
関数内部:
渡されたグローバル変数の値: この変数はグローバル変数です
グローバル変数の値を変更します
グローバル変数の値: この変数はグローバル変数で今変更されました

関数外部:
グローバル変数の値: この変数はグローバル変数です

実行結果

11-04との違いは、関数に戻り値がなく、関数呼び出しが代入文になっていないことです。関数外部の変数は全く影響を受けていませんね。引数はローカル変数だったようです。
いや、この説明は怪しいぞ。Cでも引数には、値渡しとポインタ(参照)私とかがあって、ポインタのときは、リストとか配列とかは変更できました。
pythonにもそんな関係があるのではないでしょうか
たまたま、今回の文字列が値渡しだったとか?

引数が不変な場合と変更可能な場合

pythonの場合、関数の引数の変数タイプによって、その引数が変更不能か、変更可能かが決まります。余り言葉の説明は好きではないのですが、話が通じないのも何なので軽く説明します。

  • 整数、浮動小数点数、文字列、タプルなどがimmutable(不変)です。これらのデータ型のオブジェクトは、一度作成されるとその状態を変更できません。関数にこれらの不変のオブジェクトが引数として渡されると、関数内でそのオブジェクトを変更しようとしても、元のオブジェクトは変更されず、新しいオブジェクトが作成されます。

  • リスト、辞書、セットなどはmutable(変更可能)なデータ型です。これらのデータ型のオブジェクトは、その状態を変更できます。関数にmutableなオブジェクトが引数として渡されると、関数内でそのオブジェクトを変更することができ、その変更は関数の外部にも影響を及ぼします

mutableな引数のテスト

一応どんなふうに使えるのか紹介しましょう。

# lesson11-06.py
# グローバル変数 変更可能なリスト構造にする
global_var = ["この変数はグローバル変数です"]

def demo_function(global_var):
    # ローカル変数
    print("関数内部:")
    print("渡されたグローバル変数の値:", global_var[0])

    print("グローバル変数の値を変更します")
    global_var[0]="この変数はグローバル変数で今変更されました"
    print("グローバル変数の値:", global_var[0])
    #return  global_var

# 関数を呼び出す
demo_function(global_var)

print("\n関数外部:")
# グローバル変数は関数外部からもアクセス可能
print("グローバル変数の値:", global_var[0])

$ python lesson11-06.py
関数内部:
渡されたグローバル変数の値: この変数はグローバル変数です
グローバル変数の値を変更します
グローバル変数の値: この変数はグローバル変数で今変更されました

関数外部:
グローバル変数の値: この変数はグローバル変数で今変更されました

実行結果

global_varをリスト["文字列"]にしました。これで関数にmutableな引数として渡すことができます。値の参照や変更の際にはglobal_var[0]のようにします。見事関数内で外側の変数の変更ができました。
これはglobalを用いた変更と同じく副作用のある危険な方法です。使い方には注意しましょう。引数としてリストや辞書を要求するタイプの関数を利用するときは、意図せず引数として渡した変数が変更される可能性には注意しましょう。
とは言っても引数のリスト渡しは、とても有効で便利な方法です。流行りのLLMの呼び出しや、再帰関数のテストなど愛用しているプログラマは結構います。関数をつくる人はこれらの危険性についてドキュメントで言及してほしいですし、関数を利用する人は、一度は渡したリストがどうなっているか確認してみましょう。

まとめ

この記事では、Pythonプログラミングにおける基本的な概念である「関数」と「スコープ」について、初心者にも理解しやすいように解説しました。
スコープとは、変数や関数の名前が参照できる範囲のことを指し、プログラムの構造を理解しやすくするための重要な概念です。
グローバルスコープとローカルスコープの違いについて説明し、関数内でのグローバル変数の扱い方や、グローバル変数を関数内で変更する際の注意点について説明しました。
また、変更可能な(mutable)データ型と不変な(immutable)データ型の違いについても説明し、引数として渡された変数が関数内でどのように扱われるかについての実践的な例も提供しました。
この記事がPythonプログラミングの基礎を固めるのに役立つ内容となっていれば幸いです。

おまけ タイトル画の説明 by gpt-4-v

この鮮やかな画像には、長い髪の人物がラップトップでコーディングをしている様子が描かれています。背景は、鮮明な色と多くの浮遊する泡で構成され、技術的な雰囲気を醸し出しています。泡の中にはコードやシンボルが見えます。中央には、Pythonプログラミング言語のロゴが大きく描かれており、この画像がPythonのコーディングに関連していることを示唆しています。全体的に、この画像は創造性と技術の融合を象徴し、プログラミングの魔法のような側面を表現しています。

ハッシュタグ:



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