【Python】引数における値渡しと参照渡し

Pythonでプログラムを書くとき、関数にデータを渡す方法には「値渡し」と「参照渡し」という2つの基本的な概念があります。

この違いを理解することで、プログラムがどのように動作するかを予測しやすくなります。

この記事では、これらの基本概念を説明し、Pythonでの具体的な引数の渡し方について詳しく解説します。

さらに、実際のコード例を通じて、どのように引数が渡されるのかを確認し、予期しない副作用を避けるためのベストプラクティスも紹介します。

目次から探す

基本概念の理解

Pythonにおける引数の渡し方を理解するためには、まず「値渡し」と「参照渡し」という基本的な概念を理解することが重要です。

これらの概念は、関数に引数を渡す際にどのようにデータが扱われるかを説明するものです。

値渡しとは

定義と基本的な説明

値渡し(Call by Value)とは、関数に引数を渡す際に、その引数の「値」をコピーして渡す方法です。

関数内で引数の値を変更しても、元の変数には影響を与えません。

これは、関数が引数として受け取るのは元の変数のコピーであり、元の変数とは別のメモリ領域に保存されるためです。

例えば、次のようなコードを考えてみましょう。

def modify_value(x):
    x = 10
    print("関数内のx:", x)
a = 5
modify_value(a)
print("関数外のa:", a)

このコードを実行すると、以下のような出力が得られます。

関数内のx: 10
関数外のa: 5

この例では、関数内で引数xの値を変更しても、関数外の変数aには影響がありません。

これは、aの値が関数に渡される際にコピーされ、関数内での変更が元の変数に影響を与えないためです。

他のプログラミング言語での例

値渡しの概念は、Python以外の多くのプログラミング言語でも見られます。

例えば、C言語やJavaなどがその代表例です。

C言語の例を見てみましょう。

#include <stdio.h>
void modify_value(int x) {
    x = 10;
    printf("関数内のx: %d\n", x);
}
int main() {
    int a = 5;
    modify_value(a);
    printf("関数外のa: %d\n", a);
    return 0;
}

このコードを実行すると、Pythonの例と同様に以下のような出力が得られます。

関数内のx: 10
関数外のa: 5

このように、値渡しは多くのプログラミング言語で共通の概念です。

参照渡しとは

定義と基本的な説明

参照渡し(Call by Reference)とは、関数に引数を渡す際に、その引数の「参照」を渡す方法です。

関数内で引数の値を変更すると、元の変数にもその変更が反映されます。

これは、関数が引数として受け取るのは元の変数の参照であり、元の変数と同じメモリ領域を指しているためです。

例えば、次のようなコードを考えてみましょう。

def modify_list(lst):
    lst.append(4)
    print("関数内のlst:", lst)
my_list = [1, 2, 3]
modify_list(my_list)
print("関数外のmy_list:", my_list)

このコードを実行すると、以下のような出力が得られます。

関数内のlst: [1, 2, 3, 4]
関数外のmy_list: [1, 2, 3, 4]

この例では、関数内で引数lstに対して行った変更が、関数外の変数my_listにも反映されています。

これは、my_listの参照が関数に渡され、関数内での変更が元の変数に影響を与えるためです。

他のプログラミング言語での例

参照渡しの概念も、Python以外の多くのプログラミング言語で見られます。

例えば、C++やJavaScriptなどがその代表例です。

C++の例を見てみましょう。

#include <iostream>
#include <vector>
void modify_list(std::vector<int>& lst) {
    lst.push_back(4);
    std::cout << "関数内のlst: ";
    for (int i : lst) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
}
int main() {
    std::vector<int> my_list = {1, 2, 3};
    modify_list(my_list);
    std::cout << "関数外のmy_list: ";
    for (int i : my_list) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    return 0;
}

このコードを実行すると、Pythonの例と同様に以下のような出力が得られます。

関数内のlst: 1 2 3 4
関数外のmy_list: 1 2 3 4

このように、参照渡しも多くのプログラミング言語で共通の概念です。

Pythonにおける引数の渡し方

Pythonのオブジェクトモデル

オブジェクトとメモリ管理

Pythonでは、すべてのデータはオブジェクトとして扱われます。

整数、文字列、リスト、辞書など、すべてがオブジェクトです。

これらのオブジェクトはメモリ上に配置され、各オブジェクトには一意の識別子(ID)が割り当てられます。

この識別子は、オブジェクトがメモリ上のどこに存在するかを示します。

Pythonのメモリ管理は自動的に行われ、ガベージコレクションと呼ばれる仕組みを使って不要になったオブジェクトを自動的に解放します。

これにより、プログラマはメモリ管理を意識せずにプログラムを書くことができます。

可変オブジェクトと不変オブジェクト

Pythonのオブジェクトは大きく分けて「可変オブジェクト」と「不変オブジェクト」に分類されます。

  • 可変オブジェクト: その内容を変更できるオブジェクト。

例としてリスト、辞書、セットなどがあります。

  • 不変オブジェクト: その内容を変更できないオブジェクト。

例として整数、浮動小数点数、文字列、タプルなどがあります。

可変オブジェクトと不変オブジェクトの違いは、引数の渡し方に大きな影響を与えます。

Pythonの引数渡しの仕組み

値渡しと参照渡しの違い

プログラミング言語における引数の渡し方には「値渡し」と「参照渡し」の2つの方法があります。

  • 値渡し: 関数に引数を渡す際に、引数の値そのものを渡す方法。

関数内で引数の値を変更しても、元の値には影響を与えません。

  • 参照渡し: 関数に引数を渡す際に、引数の参照(メモリ上のアドレス)を渡す方法。

関数内で引数の値を変更すると、元の値にも影響を与えます。

Pythonにおける「オブジェクトの参照渡し」

Pythonでは、引数は「オブジェクトの参照」として渡されます。

これは、引数として渡されたオブジェクトの参照(メモリ上のアドレス)が関数に渡されることを意味します。

具体的には、以下のような動作をします。

  • 不変オブジェクト: 関数内で不変オブジェクトの引数を変更しようとすると、新しいオブジェクトが作成され、元のオブジェクトには影響を与えません。
  • 可変オブジェクト: 関数内で可変オブジェクトの引数を変更すると、元のオブジェクトにも影響を与えます。

以下に具体的な例を示します。

def modify_list(lst):
    lst.append(4)
    print("関数内のリスト:", lst)
my_list = [1, 2, 3]
modify_list(my_list)
print("関数外のリスト:", my_list)

このコードを実行すると、以下のような出力が得られます。

関数内のリスト: [1, 2, 3, 4]
関数外のリスト: [1, 2, 3, 4]

この例では、リスト my_list が関数 modify_list に渡され、関数内でリストが変更されると、関数外のリストにもその変更が反映されます。

これは、リストが可変オブジェクトであり、参照渡しが行われているためです。

一方、不変オブジェクトの場合は以下のようになります。

def modify_integer(x):
    x = x + 1
    print("関数内の整数:", x)
my_integer = 10
modify_integer(my_integer)
print("関数外の整数:", my_integer)

このコードを実行すると、以下のような出力が得られます。

関数内の整数: 11
関数外の整数: 10

この例では、整数 my_integer が関数 modify_integer に渡され、関数内で整数が変更されても、関数外の整数には影響がありません。

これは、整数が不変オブジェクトであり、新しいオブジェクトが作成されるためです。

このように、Pythonでは引数の渡し方がオブジェクトの可変性に依存しているため、引数の扱い方に注意が必要です。

実際のコード例

不変オブジェクトの引数渡し

整数、文字列、タプルの例

Pythonでは、整数(int)、文字列(str)、タプル(tuple)などの不変オブジェクトは、関数に渡された際にその値が変更されることはありません。

以下の例を見てみましょう。

def modify_int(x):
    x = 10
    print(f"関数内のx: {x}")
a = 5
modify_int(a)
print(f"関数外のa: {a}")

このコードの実行結果は以下の通りです。

関数内のx: 10
関数外のa: 5

関数内でxの値を変更しても、関数外のaの値には影響がありません。

同様に、文字列やタプルも同じように動作します。

def modify_str(s):
    s = "変更後"
    print(f"関数内のs: {s}")
b = "元の文字列"
modify_str(b)
print(f"関数外のb: {b}")

実行結果は以下の通りです。

関数内のs: 変更後
関数外のb: 元の文字列

タプルの場合も同様です。

def modify_tuple(t):
    t = (10, 20, 30)
    print(f"関数内のt: {t}")
c = (1, 2, 3)
modify_tuple(c)
print(f"関数外のc: {c}")

実行結果は以下の通りです。

関数内のt: (10, 20, 30)
関数外のc: (1, 2, 3)

変更が反映されない理由

不変オブジェクトは、その名の通り一度作成されると変更できないオブジェクトです。

関数に渡された不変オブジェクトの引数は、新しい値を割り当てると新しいオブジェクトが作成され、元のオブジェクトには影響を与えません。

これが、関数内での変更が関数外に反映されない理由です。

可変オブジェクトの引数渡し

リスト、辞書、セットの例

一方、リスト(list)、辞書(dict)、セット(set)などの可変オブジェクトは、関数内で変更が行われるとその変更が関数外にも反映されます。

以下の例を見てみましょう。

def modify_list(lst):
    lst.append(4)
    print(f"関数内のlst: {lst}")
d = [1, 2, 3]
modify_list(d)
print(f"関数外のd: {d}")

実行結果は以下の通りです。

関数内のlst: [1, 2, 3, 4]
関数外のd: [1, 2, 3, 4]

辞書の場合も同様です。

def modify_dict(dic):
    dic["新しいキー"] = "新しい値"
    print(f"関数内のdic: {dic}")
e = {"元のキー": "元の値"}
modify_dict(e)
print(f"関数外のe: {e}")

実行結果は以下の通りです。

関数内のdic: {'元のキー': '元の値', '新しいキー': '新しい値'}
関数外のe: {'元のキー': '元の値', '新しいキー': '新しい値'}

セットの場合も同様です。

def modify_set(s):
    s.add(4)
    print(f"関数内のs: {s}")
f = {1, 2, 3}
modify_set(f)
print(f"関数外のf: {f}")

実行結果は以下の通りです。

関数内のs: {1, 2, 3, 4}
関数外のf: {1, 2, 3, 4}

変更が反映される理由

可変オブジェクトは、その内容を変更することができます。

関数に渡された可変オブジェクトの引数は、関数内でその内容が変更されると、元のオブジェクトにもその変更が反映されます。

これは、関数に渡されるのがオブジェクトの参照であり、その参照を通じてオブジェクトの内容が直接変更されるためです。

実践的な応用

関数内でのオブジェクトの変更

Pythonでは、関数内でオブジェクトを変更することができます。

特に可変オブジェクト(リスト、辞書、セットなど)は、関数内で変更を加えると、その変更が関数外にも反映されます。

以下に具体的な例を示します。

関数内でのリストの操作

リストは可変オブジェクトの一例です。

関数内でリストを変更すると、その変更は関数外にも反映されます。

def modify_list(lst):
    lst.append(4)
    print("関数内のリスト:", lst)
my_list = [1, 2, 3]
print("関数呼び出し前のリスト:", my_list)
modify_list(my_list)
print("関数呼び出し後のリスト:", my_list)

このコードを実行すると、以下のような出力が得られます。

関数呼び出し前のリスト: [1, 2, 3]
関数内のリスト: [1, 2, 3, 4]
関数呼び出し後のリスト: [1, 2, 3, 4]

この例からわかるように、関数内でリストに要素を追加すると、その変更が関数外のリストにも反映されます。

関数内での辞書の操作

辞書も可変オブジェクトの一例です。

関数内で辞書を変更すると、その変更は関数外にも反映されます。

def modify_dict(dct):
    dct["new_key"] = "new_value"
    print("関数内の辞書:", dct)
my_dict = {"key1": "value1", "key2": "value2"}
print("関数呼び出し前の辞書:", my_dict)
modify_dict(my_dict)
print("関数呼び出し後の辞書:", my_dict)

このコードを実行すると、以下のような出力が得られます。

関数呼び出し前の辞書: {'key1': 'value1', 'key2': 'value2'}
関数内の辞書: {'key1': 'value1', 'key2': 'value2', 'new_key': 'new_value'}
関数呼び出し後の辞書: {'key1': 'value1', 'key2': 'value2', 'new_key': 'new_value'}

この例からわかるように、関数内で辞書に新しいキーと値を追加すると、その変更が関数外の辞書にも反映されます。

関数内でのオブジェクトのコピー

関数内でオブジェクトを変更したくない場合、オブジェクトのコピーを作成することが有効です。

Pythonでは、シャローコピーとディープコピーの2種類のコピー方法があります。

シャローコピーとディープコピー

シャローコピーは、オブジェクトの最上位レベルのコピーを作成しますが、ネストされたオブジェクトは元のオブジェクトと同じ参照を持ちます。

一方、ディープコピーは、オブジェクト全体を再帰的にコピーし、ネストされたオブジェクトも新しいオブジェクトとして作成します。

シャローコピーの例:

import copy
def modify_list_shallow(lst):
    lst_copy = copy.copy(lst)
    lst_copy.append(4)
    print("関数内のリスト(シャローコピー):", lst_copy)
my_list = [1, 2, 3]
print("関数呼び出し前のリスト:", my_list)
modify_list_shallow(my_list)
print("関数呼び出し後のリスト:", my_list)

このコードを実行すると、以下のような出力が得られます。

関数呼び出し前のリスト: [1, 2, 3]
関数内のリスト(シャローコピー): [1, 2, 3, 4]
関数呼び出し後のリスト: [1, 2, 3]

ディープコピーの例:

import copy
def modify_list_deep(lst):
    lst_copy = copy.deepcopy(lst)
    lst_copy.append(4)
    print("関数内のリスト(ディープコピー):", lst_copy)
my_list = [1, 2, 3]
print("関数呼び出し前のリスト:", my_list)
modify_list_deep(my_list)
print("関数呼び出し後のリスト:", my_list)

このコードを実行すると、以下のような出力が得られます。

関数呼び出し前のリスト: [1, 2, 3]
関数内のリスト(ディープコピー): [1, 2, 3, 4]
関数呼び出し後のリスト: [1, 2, 3]

copyモジュールの使用例

Pythonのcopyモジュールを使用すると、簡単にシャローコピーとディープコピーを作成できます。

以下に、copyモジュールを使用した例を示します。

シャローコピーの作成:

import copy
original_list = [1, 2, [3, 4]]
shallow_copy = copy.copy(original_list)
print("オリジナルリスト:", original_list)
print("シャローコピー:", shallow_copy)
# シャローコピーの変更
shallow_copy[2].append(5)
print("オリジナルリスト(変更後):", original_list)
print("シャローコピー(変更後):", shallow_copy)

このコードを実行すると、以下のような出力が得られます。

オリジナルリスト: [1, 2, [3, 4]]
シャローコピー: [1, 2, [3, 4]]
オリジナルリスト(変更後): [1, 2, [3, 4, 5]]
シャローコピー(変更後): [1, 2, [3, 4, 5]]

ディープコピーの作成:

import copy
original_list = [1, 2, [3, 4]]
deep_copy = copy.deepcopy(original_list)
print("オリジナルリスト:", original_list)
print("ディープコピー:", deep_copy)
# ディープコピーの変更
deep_copy[2].append(5)
print("オリジナルリスト(変更後):", original_list)
print("ディープコピー(変更後):", deep_copy)

このコードを実行すると、以下のような出力が得られます。

オリジナルリスト: [1, 2, [3, 4]]
ディープコピー: [1, 2, [3, 4]]
オリジナルリスト(変更後): [1, 2, [3, 4]]
ディープコピー(変更後): [1, 2, [3, 4, 5]]

このように、copyモジュールを使用することで、オブジェクトのコピーを簡単に作成し、関数内での変更が元のオブジェクトに影響を与えないようにすることができます。

注意点とベストプラクティス

Pythonで引数を渡す際には、いくつかの注意点とベストプラクティスを守ることで、予期しない副作用を避け、パフォーマンスを向上させることができます。

ここでは、具体的な注意点とベストプラクティスについて解説します。

予期しない副作用の回避

可変オブジェクトの扱い方

可変オブジェクト(リスト、辞書、セットなど)を関数の引数として渡す際には、予期しない副作用が発生することがあります。

これは、関数内でオブジェクトが変更されると、呼び出し元のオブジェクトにもその変更が反映されるためです。

例えば、以下のコードを見てみましょう。

def modify_list(lst):
    lst.append(4)
my_list = [1, 2, 3]
modify_list(my_list)
print(my_list)  # 出力: [1, 2, 3, 4]

この例では、modify_list関数内でリストが変更され、その変更が呼び出し元のmy_listにも反映されています。

これを避けるためには、関数内でオブジェクトのコピーを作成することが有効です。

def modify_list(lst):
    lst_copy = lst.copy()
    lst_copy.append(4)
    return lst_copy
my_list = [1, 2, 3]
new_list = modify_list(my_list)
print(my_list)  # 出力: [1, 2, 3]
print(new_list)  # 出力: [1, 2, 3, 4]

このようにすることで、元のリストは変更されず、新しいリストが返されます。

関数設計のポイント

関数を設計する際には、以下のポイントに注意することで、予期しない副作用を避けることができます。

  1. 不変オブジェクトを使用する: 可能な限り不変オブジェクト(タプル、文字列など)を使用することで、副作用を避けることができます。
  2. オブジェクトのコピーを作成する: 可変オブジェクトを引数として受け取る場合は、関数内でそのコピーを作成し、コピーを操作するようにします。
  3. 関数の戻り値を活用する: 関数がオブジェクトを変更する場合は、その変更を反映した新しいオブジェクトを戻り値として返すようにします。

パフォーマンスの考慮

大きなデータ構造の渡し方

大きなデータ構造(リスト、辞書など)を関数に渡す際には、パフォーマンスに注意が必要です。

特に、データ構造のコピーを作成する場合は、メモリと処理時間が増加する可能性があります。

以下の例では、大きなリストを関数に渡す際のパフォーマンスを考慮した方法を示します。

import time
def process_list(lst):
    # リストのコピーを作成して処理
    lst_copy = lst.copy()
    # 何らかの処理を行う
    return lst_copy
large_list = list(range(1000000))
start_time = time.time()
processed_list = process_list(large_list)
end_time = time.time()
print(f"処理時間: {end_time - start_time}秒")

この例では、リストのコピーを作成することで、元のリストを変更せずに処理を行っています。

しかし、リストが非常に大きい場合は、コピーの作成に時間がかかることがあります。

メモリ効率の良い方法

メモリ効率を考慮する場合、以下の方法が有効です。

  1. ジェネレータを使用する: 大きなデータセットを処理する際には、ジェネレータを使用することでメモリ使用量を削減できます。

ジェネレータは、必要なデータを逐次生成するため、一度に全てのデータをメモリに保持する必要がありません。

def generate_numbers(n):
    for i in range(n):
        yield i
for number in generate_numbers(1000000):
    # 何らかの処理を行う
    pass
  1. インプレース操作を行う: 可能な限りインプレース操作(元のオブジェクトを直接変更する操作)を行うことで、メモリ使用量を削減できます。

ただし、これには副作用のリスクが伴うため、慎重に行う必要があります。

def modify_list_in_place(lst):
    for i in range(len(lst)):
        lst[i] *= 2
my_list = [1, 2, 3]
modify_list_in_place(my_list)
print(my_list)  # 出力: [2, 4, 6]

このように、Pythonで引数を渡す際には、予期しない副作用を避け、パフォーマンスを向上させるためのベストプラクティスを守ることが重要です。

これにより、より安全で効率的なコードを書くことができます。

目次から探す