0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

演習形式で学ぶPythonプログラミング vol.15 ~ユーザー定義関数の基礎~

Last updated at Posted at 2025-05-06

まえがき

Python3.13環境を想定した演習問題を扱っています。変数からオブジェクト指向まできちんと扱います。続編として、データサイエンティストのためのPython(NumPyやpandasのような定番ライブラリ編)や統計学・統計解析・機械学習・深層学習・SQLをはじめCS全般の内容も扱う予定です。

対象者は、[1]を一通り読み終えた人、またそれに準ずる知識を持つ人とします。ただし初心者の方も、文法学習と同時並行で進めることで、文法やPythonそれ自体への理解が深まるような問題を選びました。前者については、答えを見るために考えてみることをお勧めしますが、後者については、自身と手元にある文法書を読みながら答えを考えるというスタイルも良いと思います。章分けについては[1]および[2]を参考にしました。

ストックしている問題のみすべて公開するのもありかなと考えたのですが、あくまで記事として読んでもらうことを主眼に置き、1記事1トピック上限5問に解答解説を付与するという方針で進めます。すべての問題を提供しない分、自分のストックの中から、特に読むに値するものを選んだつもりです。
また内容は、ただ文法をそのままコーディング問題にするのではなく、文法を俯瞰してなおかつ実務でも応用できるものを目指します。前者については世の中にあふれていますし、それらはすでに完成されたものです(例えばPython Vtuberサプーさんの動画[3]など)。これらを解くことによって得られるものも多いですし、そのような学習もとても有効ですが、私が選んだものからも得られるものは多いと考えます。
私自身がPython初心者ですので、誤りや改善点などがあればご指摘いただけると幸いです。
➡ 最近複数の方から、誤りのご指摘、編集リクエストによる修正/改良をいただいております。この場を借りてですが、感謝申し上げます。

今回から「ユーザー定義関数の基礎」について扱います。また、【1】などでは、この間に文字列、リストや辞書に対するメソッドや標準ライブラリやモジュールについて扱いますが、今回はいったん省略します。このあたりは、テキストを読んで適宜調べるなりChatGPTに問い合わせてみるという方法のが有益であると考えるからです(余裕が出来れば、適宜追加していこうと考えています)。それでは始めましょう!

Q.15-1

Pythonにおいて関数定義が実行時に評価されることの意味を、条件分岐内で関数を定義する構造によって説明せよ。

問題背景

【1】関数定義が実行時に評価されるとは?

Pythonでは、関数定義はコードが実行される時点で評価される。つまり、関数が定義された瞬間にその内容が実行されるのではなく、実際に関数が呼び出されるまでその内容が実行されるわけではない。Pythonは関数の定義をオブジェクトとして扱い、その関数オブジェクトを後から呼び出して実行する。

【2】条件分岐内で関数を定義する場合

関数を条件分岐内で定義すると、条件に応じてどの関数が定義されるかが決まる。そのため、関数定義は評価時(実行時)に行われるので、条件により異なる関数が定義されることになる。
例えば、ある条件に基づいて異なる動作をする関数を定義したい場合、その条件が真の時点で関数が定義される。この動作が遅延評価の一例であり、実行時に動的に関数を決定できる柔軟性をPythonが提供している。

def choose_function(condition):  # 【1】関数 choose_function を定義
    if condition:  # 【2】もし条件が真なら
        def greet():  # 【3】greet 関数を定義(条件内)
            return "Hello!"
    else:  # 【4】条件が偽なら
        def greet():  # 【5】別の greet 関数を定義
            return "Goodbye!"
    return greet  # 【6】定義された greet 関数を返す

このコードでは、choose_function が呼び出されると、condition によって異なる greet 関数が定義される。つまり、関数定義は choose_function が実行される時点で評価される。

【3】関数定義が評価されるタイミングとその影響

関数定義は実行時に行われるため、条件がどのような値を取るかによって定義される関数が決まる。この点がPythonの柔軟さであり、条件によって異なる処理を実行できる。

解答例とコードによる実践

条件分岐内で関数を定義する場合、その関数は条件が成立するまで存在しない。条件が成立した時点で関数が定義され、その後その関数を呼び出すことができる。

# 【1】関数 choose_function を定義
def choose_function(condition):  # 【1】引数 condition に基づき動作を決定する関数
    if condition:  # 【2】もし condition が True なら
        # 【3】condition が True の場合、greet を "Hello!" を返すように定義
        def greet():  # 【3】greet 関数を定義("Hello!")
            return "Hello!"
    else:  # 【4】もし condition が False なら
        # 【5】condition が False の場合、greet を "Goodbye!" を返すように定義
        def greet():  # 【5】greet 関数を定義("Goodbye!")
            return "Goodbye!"
    return greet  # 【6】greet 関数を返す(この時点で関数が評価される)

# 【7】関数を呼び出して、greet 関数を受け取る
greet_func = choose_function(True)  # 【7】condition が True の場合、"Hello!" を返す greet を取得
print(greet_func())  # 【8】greet_func を呼び出すと "Hello!" が返される

greet_func = choose_function(False)  # 【9】condition が False の場合、"Goodbye!" を返す greet を取得
print(greet_func())  # 【10】greet_func を呼び出すと "Goodbye!" が返される
実行結果
Hello!
Goodbye!

コードの解説

・ 関数定義は実行時に評価される
choose_function が呼ばれると、condition に応じて異なる greet 関数が定義される。この評価は関数の呼び出し時に行われるため、条件によって異なる関数が定義される。

・ 条件に基づく関数の定義
if condition: の部分で、conditionTrue なら "Hello!" を返す関数が定義され、False なら "Goodbye!" を返す関数が定義される。

・ 関数は動的に決定される
実行時に条件が評価され、適切な関数が返される。そのため、同じ関数 choose_function を呼び出しても、引数 condition によって返される関数が異なる。

本問のまとめ

・ 関数定義が実行時に評価されることは、Pythonの動的な特徴の一つである。この特性により、条件に基づいて関数の定義を切り替えることができる。
・ 条件分岐内で関数を定義することにより、異なる動作を持つ関数を動的に作成することができる。
・ この設計を使うことで、プログラムの柔軟性を高め、条件に応じた処理を簡潔に表現できる。

このような関数定義は、クロージャや遅延評価と組み合わせて使用することで、さらに強力なプログラム設計を可能にする。

Q.15-2

def func(): returndef func(): return None の実行上の挙動の違いを is 比較で確認せよ。

問題背景

【1】returnreturn None の違い

return の挙動
Pythonでは、関数内で return 文を使うと、関数はその位置で終了し、呼び出し元に戻り値を返す。しかし、return の後に値が書かれていない場合(つまり、単に return と書いた場合)、関数は None を返します。これがPythonの仕様である。

def func(): 
    return  # 返り値なし

上記の関数は、None を返します。Pythonでは返り値を明示的に指定しない場合、None が返される。

return None の挙動
一方で、return None と記述すると、関数は明示的に None を返す。return なしと同じ動作になるが、明示的に None を返すため、コードの意図がより明確に示される。

def func(): 
    return None  # 明示的に None を返す

この関数も None を返すが、None を明示的に返している点が異なる。

【2】is 演算子と == 演算子

is 演算子:オブジェクトの同一性をチェックする。is はオブジェクトが同じメモリ位置にあるかを確認する。
== 演算子:オブジェクトの値が等しいかをチェックする。is と異なり、同じ値を持つ異なるオブジェクトも等しいと判定される。

None に関しては、シングルトンとして唯一のインスタンスが存在するため、None 同士の比較には is を使うのが適切である。

【3】None のシングルトン性

None はPythonのシングルトンオブジェクトであり、プログラム全体で唯一の None オブジェクトが存在する。これは、None 同士の比較を行うと、is 演算子を使用した場合、常に同じオブジェクトであると判定されることを意味する。

is 演算子: is はオブジェクトの同一性を比較するための演算子であり、オブジェクトがメモリ上で同じであるかどうかを判定する。

例えば、以下のように None を比較する場合、is 演算子を使うとそのオブジェクトが同一であるかどうかを確認できる。

解答例とコードによる実践

returnreturn None の挙動の違いを is で比較する

# 【1】returnを使ってNoneを返す関数
def func1():
    return  # returnがあるが、明示的に値を返さない(Noneが返る)

# 【2】return Noneを使ってNoneを返す関数
def func2():
    return None  # 明示的にNoneを返す

# 【3】関数を呼び出して、それぞれの戻り値をis演算子で比較
result1 = func1()  # 【4】func1を呼び出して戻り値をresult1に格納
result2 = func2()  # 【5】func2を呼び出して戻り値をresult2に格納

# 【6】is演算子で同一性を比較
print(result1 is None)  # 【7】result1がNoneと同じか比較
print(result2 is None)  # 【8】result2がNoneと同じか比較
print(result1 is result2)  # 【9】result1とresult2が同じオブジェクトか比較
実行結果
True
True
True

コードの解説

None のシングルトン性:None はPythonのシングルトンオブジェクトであり、None 同士の比較を行うと、is 演算子で常に同一オブジェクトであると判定される。これは func1()func2() の両方で返される None が同じオブジェクトを指していることを意味する。

returnreturn None の違い: return 文がない場合でも、Pythonは暗黙的に None を返す。return None と記述する場合と挙動は同じであるが、return None の方が明示的である。どちらの場合も戻り値は None であり、None は唯一のオブジェクトであるため、is 演算子で比較した際に同一オブジェクトと判定される。

is 演算子と == 演算子の違い: is はオブジェクトの同一性を比較し、== は値の等価性を比較する。None に関しては、シングルトンであるため、is 演算子を使った比較が適切である。

Q.15-3

関数が複数回呼び出された際、関数定義そのものが毎回再解釈されるか否かについて、実行時オブジェクトの振る舞いから論ぜよ。

問題背景

【1】 関数定義と再解釈

Pythonにおいて、関数は def キーワードを使って定義される。関数を定義する際、その定義は一度だけ評価され、メモリに格納される。関数が複数回呼び出される場合でも、関数定義そのものは毎回再解釈されることはない。むしろ、関数定義は一度だけ評価された後、その後は関数オブジェクトとして使用される。
Pythonの関数は「ファーストクラスオブジェクト」として扱われ、変数や引数として他の関数に渡すことができる(Pythonにおいては関数もオブジェクト)。関数の呼び出しがある度にその定義が再評価されることはなく、最初に定義された関数オブジェクトが使用される。この動作により、関数が何度も呼ばれても効率よく実行される。

【2】 実行時オブジェクトの振る舞い

関数定義が評価されるタイミングは、プログラムが実行される際に最初の1回のみである。その後、関数呼び出しが行われる度に新たに評価されることはない。つまり、関数定義はメモリに関数オブジェクトとして格納され、呼び出しのたびに再利用される。このため、同じ関数を複数回呼び出しても、その関数定義が再解釈されることはなく、実行時に保存された関数オブジェクトが利用される。

【3】 関数オブジェクト

関数オブジェクトは、関数名を通じてアクセスされる実行可能なオブジェクトです。関数が定義されると、インタープリタはその関数をメモリに格納し、その後は関数名を通じて何度でも呼び出すことができます。実行時に関数が呼び出されると、関数の実行が行われ、関数内で定義された処理が実行される。

解答例とコードによる実践

# 【1】関数を定義する
def greet():  # 【2】関数 greet を定義
    print("Hello!")  # 【3】関数の中身

# 【4】関数を呼び出す
greet()  # 【5】1回目の関数呼び出し

# 【6】関数オブジェクトを変数に格納
greet_object = greet  # 【7】関数 greet を greet_object という変数に格納

# 【8】再度呼び出して確認
greet_object()  # 【9】greet_object を呼び出す(実際には greet と同じ関数)

# 【10】関数定義の参照を確認
print(greet is greet_object)  # 【11】greet と greet_object が同じ関数オブジェクトかを比較
実行結果
Hello!
Hello!
True

コード解説

・ 関数定義が一度だけ評価される
最初に greet() 関数を定義した時点で、関数オブジェクトが作成され、メモリに格納される。その後、greet() を呼び出しても、その関数定義が再解釈されることはない。
・ 関数オブジェクトの再利用
greet_object = greet の行では、greet 関数オブジェクトを変数 greet_object に代入している。この時点で、greet_objectgreet と全く同じ関数オブジェクトを指している。このため、greet_object() と呼んでも、greet() と同じ動作が実行される。
is 演算子によるオブジェクト比較
最後に、greet is greet_object で関数オブジェクトが同じであることを確認している。結果は True となり、greetgreet_object が同じオブジェクトであることが分かる。これは、関数定義がメモリに一度だけ保存され、その後はそのオブジェクトが使い回されるため。

Q.15-4

関数定義の中にクラス定義を含む設計を行ったとき、名前空間の扱いやスコープへの影響について記述せよ。

問題背景

【1】Pythonにおける名前空間とスコープとは何か

名前空間(namespace)とは、変数や関数、クラスなどの名前(識別子)と、それに対応するオブジェクトの対応関係を保持している辞書のような構造である。Pythonでは以下のような名前空間が存在する:

ローカルスコープ:関数の内部で定義された変数やクラス、関数が属する。
グローバルスコープ:モジュール内で定義された変数や関数が属する。
ビルトインスコープ:printlen など、Pythonが提供する組み込み関数が属する。

Pythonでは、LEGBルール(Local, Enclosing, Global, Built-in)に従って名前解決が行われる。

【2】関数定義の中でクラスを定義するとどうなるか

通常、クラスはモジュールレベル(つまり関数の外)で定義される。しかし、関数内でクラスを定義することも可能である。このときそのクラスは関数のローカルスコープに属することになる。

主な特徴:
関数外ではアクセス不可:関数の外では定義されたクラスにアクセスできない。
関数が呼び出されたときにクラスが定義される:関数のスコープが有効になって初めて、そのクラス定義も実体化する。
複数回呼び出すと、毎回別のクラスオブジェクトが生成される:クラス定義は1回ごとに新たな型を作成する。

def make_class():  # 【1】関数 make_class を定義
    class Inner:  # 【2】関数内でクラス Inner を定義
        def greet(self):  # 【3】クラスのメソッド greet を定義
            return "Hello from Inner"
    return Inner  # 【4】クラス型を返す

このコードでは、make_class() を呼び出すと、関数内で定義されたクラス Inner を返す。外部では Inner は直接アクセスできない。

解答例とコードによる実践

# 【1】関数の定義を開始する
def create_dynamic_class():  # 【1】create_dynamic_class という関数を定義する

    # 【2】関数の中でクラスを定義する
    class LocalClass:  # 【2】このクラスは create_dynamic_class のスコープ内でのみ有効である

        def __init__(self, value):  # 【3】コンストラクタ(初期化メソッド)を定義
            self.value = value  # 【4】インスタンス変数 value を初期化

        def double(self):  # 【5】値を2倍して返すメソッド
            return self.value * 2  # 【6】value を2倍にして返す

    return LocalClass  # 【7】定義した LocalClass を返す(型を返す)
    

# 【8】関数を呼び出して、その結果(クラス)を受け取る
MyClass = create_dynamic_class()  # 【8】MyClass という名前で、create_dynamic_class の戻り値(クラス)を受け取る

# 【9】受け取ったクラスを使ってインスタンスを生成する
obj = MyClass(10)  # 【9】MyClass(=LocalClass)を使って新しいオブジェクト obj を生成する

# 【10】メソッドを呼び出す
print(obj.double())  # 【10】obj の double() メソッドを呼び出して出力する
実行結果
20

追加検証:クラスが毎回再定義されるかの確認

以下のコードは、関数を2回呼び出した場合に、それぞれ異なるクラス型が生成されることを示す:

A = create_dynamic_class()  # 【1】1回目の呼び出しでクラスAを取得
B = create_dynamic_class()  # 【2】2回目の呼び出しでクラスBを取得

print(A is B)  # 【3】AとBが同じクラス定義かを比較
実行結果
False

この結果は、create_dynamic_class を呼び出すたびに新しいクラス型(別オブジェクト)が生成されることを示している。

本問のまとめ

・ 関数内にクラスを定義することで、そのクラスを関数のローカルスコープに限定できる。
・ クラスは関数呼び出し時に動的に定義されるため、毎回異なるクラス型が生成される。
・ この設計は、一時的な型の定義やスコープの制御をしたい場合に有効である。
・ ただし、関数外では直接参照できないため、返却して使うか、関数内でインスタンス化を完了させる必要がある。

このように関数内クラス定義は高度な名前空間制御のテクニックであり、スコープに対する深い理解が求められる機能である。

Q.15-5

ある関数が条件によって int もしくは str を返すとき、呼び出し元で型安全に処理するための構造と設計方針を記述せよ。

問題背景

【1】動的型付けと言語特性

Pythonは動的型付け言語であるため、関数の返り値は実行時に決定される。これは、同じ関数が引数や内部の状態に応じて異なる型(例えば intstr)を返すことを可能にする。しかし、この特性は型安全性を確保する上で課題を生じることがある。特に、関数が異なる型を返す場合、呼び出し元でその型に対して適切な処理を行う必要がある。

【2】型安全とは?

型安全とは、プログラムが誤った型に対して不適切な操作を行わないように設計されていることを意味する。型に依存する操作を実行する前に、型を確認して適切な処理を行うことが、型安全を保つために重要である。

【3】isinstance() を使った型チェック

Pythonでは、isinstance() 関数を使うことで、オブジェクトが特定の型かどうかを確認できる。これにより、関数の返り値がどの型であるかをチェックし、安全に処理を分岐させることができる。

isinstance(value, int)  # value が int 型かどうかを確認
isinstance(value, str)  # value が str 型かどうかを確認

【4】設計方針

関数が int または str を返す場合、以下の設計が型安全を保つために有効である。

  1. isinstance() による型チェック
    関数の戻り値が期待される型であるかどうかを確認し、その型に応じた処理を行う。
  2. 適切なエラーハンドリング
    予期しない型が返された場合には、エラーハンドリングを行い、型の不一致を検出する。

解答例とコードによる実践

# 【1】関数を定義する
def fetch_data(flag: bool) -> int | str:  # 【2】flag に応じて返り値が int または str になる関数を定義
    if flag:  # 【3】flag が True の場合
        return 42  # 【4】int 型の値を返す
    else:  # 【5】flag が False の場合
        return "Hello, world!"  # 【6】str 型の値を返す

# 【7】関数を呼び出して結果を取得する
result = fetch_data(True)  # 【8】True を渡して、int 型の値を取得

# 【9】型チェックと処理分岐を行う
if isinstance(result, int):  # 【10】result が int 型なら
    print(f"Received an integer: {result * 2}")  # 【11】整数型の場合の処理
elif isinstance(result, str):  # 【12】result が str 型なら
    print(f"Received a string: {result.upper()}")  # 【13】文字列型の場合の処理
else:  # 【14】その他の型の場合
    print("Unexpected type!")  # 【15】予期しない型の場合の処理
実行結果
Received an integer: 84

コードの解説

fetch_data 関数の定義
関数 fetch_data は引数 flag に応じて、int 型または str 型のいずれかを返す。True の場合は 42int)を、False の場合は "Hello, world!"str)を返す。
isinstance による型チェック
呼び出し元で、返り値 result の型を isinstance() を使って確認する。もし resultint 型であれば、その値を2倍にして表示する。もし resultstr 型であれば、その文字列を大文字にして表示する。
・ 型不一致時のエラーハンドリング
万が一、resultintstr 以外の型であった場合、else 節でエラーメッセージを表示する。これにより、予期しない型に対するエラーを適切に処理することができる。
・ 型安全な設計
このように、関数の返り値が異なる型を持つ場合でも、isinstance() を使って明示的に型を確認し、適切な処理を行うことで、型安全を保つことができる。

本問のまとめ

・ 関数が int または str を返す場合、isinstance() を使って返り値の型を確認し、その型に応じた処理を行うことが型安全を保つために重要である。
・ 予期しない型が返された場合には、エラーハンドリングを行って安全に処理することが求められる。
・ このような設計を採用することで、Pythonの動的型付けを活かしつつ、安全なプログラムを構築することができる。

Q.15-6

return 文を関数の途中に複数配置した際、静的解析においてどのように到達可能性を評価するかを説明せよ。

問題背景

【1】Pythonにおける return 文と関数の制御フロー

Pythonにおける return 文は、関数の実行を終了し、その値を呼び出し元に返すために使用される。return 文が実行されると、その時点で関数の実行は終了し、それ以降のコードは実行されない。この動作により、関数の制御フローは途中で分岐することができる。

例えば、以下のように return 文を関数内に複数配置することが可能である。

def example_func(x):
    if x > 0:
        return "Positive"
    elif x < 0:
        return "Negative"
    else:
        return "Zero"

上記の例では、引数 x に基づき、return 文が関数の途中に複数配置されているが、どの return が実行されるかは x の値に依存する。

【2】静的解析における到達可能性(Reachability)の評価

到達可能性とは、プログラムのコード内で、特定の命令やステートメントが実行される可能性があるかどうかを示す概念である。静的解析ツールはコードを解析し、実行時にどのコードが実行される可能性があるのかを推定する。
複数の return 文が存在する場合、静的解析はそれぞれの returnの前後のコードが到達可能かを評価する必要がある。return 文が関数の途中に複数ある場合、その後のコードが実行されることはないため、到達不可能なコード(unreachable code)が存在する可能性がある。
例えば、次のコードは静的解析ツールによって「到達不可能なコード」として識別される。

def func(x):
    if x > 0:
        return "Positive"
    print("This will never be printed.")  # 到達不可能なコード
    return "Zero"

【3】静的解析の方法

静的解析ツールは、以下の手順で到達可能性を評価する。
1) 関数のフローを追跡:各 return 文が関数内でどのように配置されているかを追跡し、分岐点ごとにどのコードが実行されるかを予測する。
2) 到達不可能なコードを識別:return 文が実行されるとその後のコードは実行されないため、return の後に位置するコードが到達不可能であると判断される。
3) 警告の生成:到達不可能なコードが存在する場合、その部分について警告を生成し、リファクタリングの必要性を示唆する。

解答例とコードによる実践

# 【1】関数を定義する
def process_value(x):
    if x > 0:  # 【2】x が 0 より大きい場合
        return "Positive"  # 【3】Positive を返す
    elif x < 0:  # 【4】x が 0 より小さい場合
        return "Negative"  # 【5】Negative を返す
    return "Zero"  # 【6】x が 0 の場合に "Zero" を返す

# 【7】関数を呼び出して結果を表示する
result = process_value(10)  # 【8】引数に 10 を渡して呼び出す
print(result)  # 【9】結果を表示("Positive" が表示される)
実行結果
Positive

到達不可能なコードの例

# 【1】関数を定義する
def process_value(x):
    if x > 0:  # 【2】x が 0 より大きい場合
        return "Positive"  # 【3】Positive を返す
    elif x < 0:  # 【4】x が 0 より小さい場合
        return "Negative"  # 【5】Negative を返す
    print("This will never be printed.")  # 【6】到達不可能なコード
    return "Zero"  # 【7】到達不可能なコード

# 【8】関数を呼び出して結果を表示する
result = process_value(0)  # 【9】引数に 0 を渡して呼び出す
print(result)  # 【10】結果を表示("Zero" が表示される)
実行結果
Zero

コードの解説

process_value 関数は、x の値によって異なる文字列("Positive", "Negative", "Zero") を返す。
・ それぞれの return 文は x の値に基づいて異なるコードのブロックに到達するが、どの return が実行されるかは条件に依存する。
・ 最後の print("This will never be printed.") の部分は、return 文が先に実行されるため、到達不可能なコードとして静的解析ツールによって検出される。これは、x が0の場合に関数が "Zero" を返すため、return "Zero" の後のコードは実行されない。

本問のまとめ

・ Pythonにおいて return 文を関数内で複数回使用する場合、それぞれの return 文は関数の制御フローを終了させ、後続のコードは実行されなくなる。
・ 静的解析ツールは、return 文後のコードが到達不可能であることを評価し、到達不可能なコードとして警告を出力する。
・ 関数内での return 文の使用は制御フローに直接的な影響を与えるため、静的解析によって関数のフローが適切に解析され、最適化が可能である。

Q.15-7

クラス内の関数名が同一クラス・親クラス・外部モジュールと衝突する場合の挙動と対処法を記述せよ。

問題背景

【1】クラス内の関数名と名前空間

Pythonでは、クラス内で定義された関数(メソッド)は、クラスの名前空間に属する。しかし、名前空間が同じ場合、同一の関数名が他の名前空間(親クラス、外部モジュール)と衝突することがある。衝突が発生すると、関数名が予期しない動作をすることがあり、名前解決の順序を理解しておくことが重要。

【2】名前解決の順序(MRO)

Pythonでは、名前解決の順序(メソッド解決順序、Method Resolution Order, MRO)が定められており、名前が衝突した場合にどの名前空間が優先されるかを決定する。クラス内で名前解決が行われる順序は次の通り:

  1. クラス自身:最初にクラス自身の名前空間が調べられる。
  2. 親クラス:次に、親クラス(super())の名前空間が調べられる。
  3. 外部モジュール:外部モジュールでインポートされた関数や変数は、適切に名前空間を指定してアクセスする必要がある。

衝突が発生した場合、Pythonはこの順序で名前を解決します。もし、クラス内のメソッド名が親クラスや外部モジュールの関数名と同じであれば、後者が優先される可能性がある。

【3】衝突の例と対処法
クラス内と親クラスでの衝突:子クラスで親クラスのメソッドをオーバーライドする際に、同じ名前のメソッドが親クラスと子クラスで定義されている場合、子クラスで定義したメソッドが優先される。

クラス内と外部モジュールでの衝突:外部モジュールでインポートした関数や変数がクラス内で同名の関数を持つ場合、名前空間を適切に指定することで衝突を回避できる。

解答例とコードによる実践

解答例を示すコード

# 【1】親クラスを定義
class Parent:
    def greet(self):  # 【2】親クラスに greet メソッドを定義
        print("Hello from Parent")  # 【3】親クラスの挨拶

# 【4】子クラスを定義し、親クラスのメソッドをオーバーライド
class Child(Parent):
    def greet(self):  # 【5】子クラスでも greet メソッドを定義
        print("Hello from Child")  # 【6】子クラスの挨拶

# 【7】インスタンスを生成
child = Child()  # 【8】Child クラスのインスタンスを生成

# 【9】子クラスの greet メソッドを呼び出す
child.greet()  # 【10】出力: "Hello from Child"(親クラスのメソッドは無視される)

# 【11】親クラスの greet メソッドを呼び出す
parent = Parent()  # 【12】Parent クラスのインスタンスを生成
parent.greet()  # 【13】出力: "Hello from Parent"
実行結果
Hello from Child
Hello from Parent

コードの解説

・ クラス内のメソッドと親クラスのメソッドの衝突
子クラス Child では親クラス Parentgreet メソッドをオーバーライドしている。この場合、子クラスで定義された greet メソッドが優先され、インスタンスで呼び出された際には子クラスの greet メソッドが実行される。
・ 親クラスのメソッドの呼び出し
親クラス Parentgreet メソッドは、親クラスのインスタンスで呼び出されると実行され、親クラス独自の挨拶が表示される。

外部モジュールと衝突する例

# 【1】外部モジュールとして math をインポート
import math  # 【2】math モジュールをインポート

# 【3】同じ名前の関数をクラス内に定義
class MyMath:
    def sqrt(self, x):  # 【4】自分で sqrt メソッドを定義
        return x ** 0.5  # 【5】平方根を計算

# 【6】外部モジュールの sqrt 関数と衝突するが、名前空間を区別
print(math.sqrt(16))  # 【7】外部モジュール math の sqrt 関数を使用(出力: 4.0)

my_math = MyMath()  # 【8】MyMath クラスのインスタンスを作成
print(my_math.sqrt(16))  # 【9】MyMath クラスの sqrt メソッドを使用(出力: 4.0)
実行結果
4.0
4.0

コードの解説

・ 外部モジュールとクラス内のメソッド名が衝突した場合
math.sqrt とクラス内の sqrt メソッド名が同じであるが、どちらを呼び出すかは名前空間を明示的に指定することで区別できる。ここでは math.sqrtmy_math.sqrt を使い分けて、異なる関数が呼び出されている。
・ Pythonでは、名前空間が異なれば同じ名前の関数を複数定義しても衝突を避けることができる。

衝突を避けるための対処法

・ 名前空間の明示的な区別
外部モジュールでインポートした関数やクラスと自クラス内の関数名が衝突しないように、名前空間を適切に区別する。例えば、math.sqrtmy_math.sqrt のように、モジュール名やインスタンス名で明示的に呼び出す。
・親クラスのメソッドのオーバーライド時の注意
親クラスと同じメソッド名を使う場合、子クラスでオーバーライドする意図がない場合は、別の名前に変更することを検討する。もしオーバーライドする場合は、super() を使って親クラスのメソッドを呼び出すことができる。
・名前の変更
同じ名前の関数やメソッドが衝突する場合は、関数名を変更することで衝突を回避する。

本問のまとめ

・ 名前解決の順序(MRO)に基づき、クラス内のメソッドと親クラスのメソッドが衝突した場合、親クラスのメソッドはオーバーライドされ、子クラスのメソッドが優先される。
・ 外部モジュールとの衝突が発生した場合は、名前空間を明示的に区別することで衝突を回避できる。
・ クラス内メソッド名の衝突を避けるためには、適切な名前付けや名前空間の管理が必要である。

Q.15-8

Pythonでは関数の再定義が静かに受け入れられる仕様であることを前提に、動的再定義が意図せず起こるリスクを論ぜよ。

問題背景

【1】Pythonにおける関数の再定義

Pythonでは、関数の再定義が許容される。つまり、関数名が既に存在する場合でも、後から同じ名前の関数を定義し直すことができる。新たに定義された関数は、既存の関数を上書きすることになる。

例えば、次のように関数を再定義することが可能である。

def greet():
    print("Hello, World!")

# 既存の greet 関数を再定義する
def greet():
    print("Hi there!")

【2】動的再定義のリスク

動的再定義が意図せず行われると、プログラムの挙動が予期しないものになる可能性がある。例えば、関数の再定義によって、意図していた動作が変更され、後続のコードで異なる結果を招くことがある。これにより、バグや不具合が発生しやすくなる。

再定義が意図せず発生するリスクとしては、以下のようなケースが考えられる。

  1. 意図せず関数の動作が変わる:関数名を変更せずに再定義してしまった場合、既存のコードが新しい関数を呼び出し、意図しない動作を引き起こす。
  2. 再定義が原因でエラーが発生する:後から再定義された関数が、元々の関数の引数や戻り値の仕様と異なり、呼び出し元でエラーが発生する場合がある。
  3. 名前空間の汚染:関数が頻繁に再定義されると、同じ名前の関数が異なる意図で使われ、名前空間が汚染されてしまう。

【3】動的再定義が発生しやすい状況

・ インタラクティブな開発環境:インタラクティブなシェル(例えば、IPythonやJupyter Notebook)では、コードを一部ずつ実行することが多いため、意図せず関数が再定義されるリスクが高まる。
・ モジュールの再読み込み:モジュールを再読み込んだ際に、既に定義されていた関数が再定義されることがある。特に、開発中にモジュールの修正を繰り返すと、再定義が行われ、バグが発生しやすくなる。

解答例とコードによる実践

# 【1】最初の関数定義
def greet():
    print("Hello, World!")  # 【2】最初に定義した挨拶

# 【3】greet 関数を再定義
def greet():
    print("Hi there!")  # 【4】再定義した挨拶

# 【5】関数を呼び出す
greet()  # 【6】再定義後の greet 関数が呼ばれる
実行結果
Hi there!

コードの解説

・ 関数の再定義
最初に greet() 関数を定義した後、同じ名前で再定義を行った。そのため、再定義後の greet() が呼ばれることになり、「Hi there!」と表示される。最初に定義した挨拶「Hello, World!」はもはや存在しない。
・ 意図せず再定義が起こるリスク
関数の再定義が意図せず行われると、プログラムの挙動が変更される可能性がある。例えば、最初の greet() 関数の挨拶を変更する意図がない場合、意図せず挨拶を変えてしまうことになる。

動的再定義が意図せず起こるリスク

・ 関数の意図しない変更
再定義によって、既存の関数の挙動が変更され、後続のコードに影響を与える。特に、再定義された関数が元々の関数と異なる動作をすると、プログラム全体の挙動が予期せぬものになる。
例えば、元々の関数で「ユーザーに挨拶」を行っていたが、再定義された関数で「異なる処理」を行ってしまった場合、アプリケーションの挙動が異なり、バグが発生する可能性がある。

・ 意図しない依存関係
再定義された関数が他のコードに依存している場合、予期せぬエラーが発生することがある。関数の再定義を行う際は、依存関係が切れることがないように注意が必要である。

デバッグの難易度
動的に関数を再定義することで、デバッグが難しくなる。再定義が行われるたびに、関数の挙動が変わるため、どの段階の関数が実行されているか追跡が困難になる。

インタラクティブ開発環境でのリスク
インタラクティブシェルやJupyter Notebookのような開発環境では、コードを一部ずつ実行するため、関数が意図せず再定義されやすい。再定義を繰り返すことで、最終的に望んでいない動作が行われることがある。

動的再定義を防ぐための対策

・ 関数名の一意性
再定義を避けるために、関数名が他の関数やモジュールと衝突しないように名前付け規則を徹底することが重要である。関数名のプレフィックスやサフィックスを使って名前の衝突を避けることが有効である。

・ 関数の再定義に注意
コードを書いている途中で関数を再定義しないように注意する。また、関数が再定義された場合、意図していない動作が起こらないように、関数の挙動をテストし続けることが重要である。

・ インタラクティブ開発環境での管理
インタラクティブな開発環境で作業している場合、コードを変更する際には注意が必要である。定義済みの関数を再定義する場合は、事前に既存の関数が上書きされることを確認し、必要であれば名前を変更して衝突を避ける。

・ コードのモジュール化と分割
関数が再定義されるリスクを最小限に抑えるために、コードをモジュール化し、関数を適切に分割することが有効である。これにより、関数が他のコードと衝突する可能性を減らすことができる。

本問のまとめ

・ Pythonでは関数の再定義が静かに受け入れられるため、意図せず関数が上書きされるリスクがある。

・ 動的再定義が発生すると、関数の挙動が予期せぬ形で変わるため、バグやエラーが発生する可能性がある。

・ 再定義を防ぐためには、関数名の一意性を保ち、再定義に注意することが重要である。

・ インタラクティブ開発環境での作業やコードのモジュール化も、再定義を避けるための効果的な方法である。

あとがき

今回は「ユーザー定義関数の基礎」について扱いました。個人的にはもっと扱いたかった問題も多いのですが、盲点となりがちな話や重要そうに感じる話題を中心に選んでみました。次回は、「ユーザー定義関数における引数」について扱う予定です。

参考文献

[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)
[3] 【Python 猛特訓】100本ノックで基礎力を向上させよう!プログラミング初心者向けの厳選100問を出題!(Youtube, https://youtu.be/v5lpFzSwKbc?si=PEtaPNdD1TNHhnAG)

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?