Pythonの関数引数は、一見シンプルに見えますが、実は「オブジェクト」「参照」「ミュータブル/イミュータブル」などの概念が絡み合う少し奥深いテーマです。
本記事では、Pythonの引数がどのように渡され、どんなときに元のデータが書き換わるのかを、図解とサンプルコードを使って丁寧に解説します。
さらに、可変長引数やデフォルト引数の落とし穴まで、実践的なテクニックもまとめて理解できるように進めていきます。
Pythonの関数引数の基本
Pythonにおける「値渡し」「参照渡し」の考え方

Pythonの引数まわりでよく耳にするのが「値渡しなのか参照渡しなのか」という話題です。
しかし、PythonはC言語のような「値渡し」やC++のような「参照渡し」という用語だけではきれいに説明しきれません。
Pythonでは、関数に渡されるのは「オブジェクトそのもの」ではなく「オブジェクトへの参照」です。
もう少し噛み砕いて言うと、関数呼び出しのときに「変数が指している先」をそのまま渡しているイメージです。
ただし、ここで混乱しやすいポイントがあります。
それは「参照を渡しているからといって、常に元の値が書き換わるわけではない」ということです。
書き換えられるかどうかは、そのオブジェクトがミュータブルかイミュータブルかに依存します。
この点については後半で詳しく見ていきます。
変数は参照(オブジェクトへのポインタ)を持つ仕組み

Pythonでは変数そのものが値を持っているわけではなく、オブジェクトへの参照を持っていると考えると理解しやすくなります。
例えば、次のコードを見てください。
x = 10
y = x
print(x, y)
ここで起きていることを分解すると、次のようになります。
- 整数オブジェクト(10)がメモリ上に作られる
- 変数
xが、その整数オブジェクトを指す参照を持つ y = xにより、変数yも同じオブジェクトを指す参照を持つ
この状態でxもyも10を指していますが、次のように書き換えるとどうなるでしょうか。
x = 20
print(x, y)
このとき、xは新たに作られた整数オブジェクト20を指すようになり、yは依然として10を指し続けます。
つまり、「変数を書き換える」とは、「その変数が指す参照を付け替える」ことだと捉えるのがポイントです。
ミュータブルとイミュータブルで挙動が変わる理由

Pythonのオブジェクトは、大きくミュータブル(変更可能)とイミュータブル(変更不可能)に分けられます。
代表的なものは次のとおりです。
| 種類 | ミュータブル(変更可能) | イミュータブル(変更不可能) |
|---|---|---|
| 代表例 | list, dict, set, bytearray | int, float, str, tuple, frozenset, bytes |
ミュータブルなオブジェクトは、同じオブジェクトを指している変数が複数あっても、そのオブジェクトの中身を変更できるため、片方からの変更がもう片方にも影響します。
一方、イミュータブルなオブジェクトは中身を変更できないので、変更しようとしたときには「新しいオブジェクトを作って参照を差し替える」という動きになります。
この違いが、関数引数の挙動に大きな影響を与えます。
次の章では、実際のコードを使ってその違いを確かめていきます。
ミュータブルとイミュータブルの引数挙動
イミュータブル(int・str・tuple)引数の挙動と注意点

まずは、整数や文字列などのイミュータブルなオブジェクトを引数に渡した場合を見てみます。
def change_number(n):
# 関数に渡された引数nの値を表示
print("関数内(変更前) n:", n)
# nを書き換えようとする
n = 20
print("関数内(変更後) n:", n)
# 関数の外側
x = 10
print("関数呼び出し前 x:", x)
change_number(x)
print("関数呼び出し後 x:", x)
関数呼び出し前 x: 10
関数内(変更前) n: 10
関数内(変更後) n: 20
関数呼び出し後 x: 10
この例では、change_number関数の中でnを20に変更しても、外側のxは10のままです。
これはイミュータブルなオブジェクトは中身を変更できず、「n = 20」という代入が「nという変数の参照先を変えただけ」だからです。
文字列でも同じことが起こります。
def append_str(s):
print("関数内(変更前) s:", s)
# 文字列の結合は新しい文字列オブジェクトを作る
s = s + " World"
print("関数内(変更後) s:", s)
msg = "Hello"
print("関数呼び出し前 msg:", msg)
append_str(msg)
print("関数呼び出し後 msg:", msg)
関数呼び出し前 msg: Hello
関数内(変更前) s: Hello
関数内(変更後) s: Hello World
関数呼び出し後 msg: Hello
このように、イミュータブルなオブジェクトを引数に渡した場合、関数内で元の変数の値が変わることはありません。
ただし、関数の戻り値を受け取って外側の変数に代入し直すことで、結果的に値を更新するのが一般的な使い方になります。
ミュータブル(list・dict・set)引数が書き換わる仕組み

次に、リストのようなミュータブルなオブジェクトを引数に渡した場合を見てみます。
def add_item(lst):
print("関数内(変更前) lst:", lst)
# 渡されたリストに要素を追加(中身を書き換える操作)
lst.append(3)
print("関数内(変更後) lst:", lst)
numbers = [1, 2]
print("関数呼び出し前 numbers:", numbers)
add_item(numbers)
print("関数呼び出し後 numbers:", numbers)
関数呼び出し前 numbers: [1, 2]
関数内(変更前) lst: [1, 2]
関数内(変更後) lst: [1, 2, 3]
関数呼び出し後 numbers: [1, 2, 3]
ここでは、関数内でlst.append(3)を呼び出した結果、関数の外側のnumbersも書き換わっています。
これはnumbersとlstが同じリストオブジェクトを参照しているため、そのオブジェクトの中身を変更すると両方に影響するからです。
同じようなことは辞書や集合でも起こります。
def update_dict(d):
# 渡された辞書に新しいキーを追加
d["new"] = 100
data = {"a": 1}
print("呼び出し前:", data)
update_dict(data)
print("呼び出し後:", data)
呼び出し前: {'a': 1}
呼び出し後: {'a': 1, 'new': 100}
「引数として渡したリストや辞書が、関数の後で勝手に変わってしまった」というトラブルは、このミュータブルの性質を理解していないと起きやすいポイントです。
関数内で元のオブジェクトを変更しない書き方

もし「渡されたオブジェクトを元のままにしておきたい(副作用を避けたい)」場合は、関数内でコピーを作ってから操作するのが定石です。
def safe_add_item(lst):
# 渡されたリストのシャローコピー(浅いコピー)を作成
copied = lst.copy()
# コピーの方にのみ要素を追加
copied.append(3)
# 元のlstは変更しない
return copied
numbers = [1, 2]
print("呼び出し前 numbers:", numbers)
new_numbers = safe_add_item(numbers)
print("呼び出し後 numbers:", numbers)
print("戻り値 new_numbers:", new_numbers)
呼び出し前 numbers: [1, 2]
呼び出し後 numbers: [1, 2]
戻り値 new_numbers: [1, 2, 3]
このように、コピーを作ってから変更し、その結果を戻り値として返すスタイルにしておくと、副作用によるバグを防ぎやすくなります。
特にライブラリや共通関数を設計するときには、「引数で受け取ったデータを勝手に書き換えない」という方針を基本にすると安心です。
Pythonの関数引数の種類
ここからは、引数の「渡し方」そのものに焦点を当てて、Pythonで用意されているさまざまな種類の引数を順番に見ていきます。
位置引数(positional arguments)の基本と使い方

もっとも基本的な引数が位置引数(positional arguments)です。
呼び出し時の「順番」によって、どの引数にどの値が渡されるかが決まります。
def greet(name, message):
# nameとmessageを単純に組み合わせて表示する
print(message + ", " + name + "!")
# 位置引数での呼び出し
greet("Alice", "Hello")
greet("Bob", "Hi")
Hello, Alice!
Hi, Bob!
ここでは、最初の引数がname、2番目の引数がmessageに対応します。
位置引数はシンプルですが、引数の順番を間違えると、意図しない値が渡されてしまうという問題もあります。
この問題を解決する一つの手段が、次に説明するキーワード引数です。
デフォルト引数(default arguments)と注意すべき落とし穴

デフォルト引数は、引数が省略されたときに使われる既定値を定義できます。
def greet(name, message="Hello"):
# messageを省略した場合は"Hello"が使われる
print(message + ", " + name + "!")
greet("Alice") # messageは省略 → "Hello"
greet("Bob", "Hi") # messageを指定
Hello, Alice!
Hi, Bob!
このように、「たいていは同じ値を使うが、たまに変えたい」というパラメータにデフォルト引数はとても便利です。
ただし、ミュータブルなオブジェクト(リストなど)をデフォルト引数にすることは避けるべきという有名な落とし穴があります。
この点は後の「ミュータブルなデフォルト引数を避ける定石パターン」で詳しく説明します。
キーワード引数(keyword arguments)で可読性を上げる

キーワード引数は、引数名を明示して渡すことで、順番に依存せずに値を指定できる方法です。
def create_user(name, age, admin=False):
print("name:", name)
print("age:", age)
print("admin:", admin)
# 位置引数だけで呼び出し
create_user("Alice", 30, True)
# キーワード引数を使った呼び出し
create_user(name="Bob", age=25, admin=False)
# 位置引数とキーワード引数を混ぜることも可能(位置引数が先)
create_user("Charlie", admin=True, age=40)
name: Alice
age: 30
admin: True
name: Bob
age: 25
admin: False
name: Charlie
age: 40
admin: True
キーワード引数を使うと、「このTrueは何を意味しているのか」「この数値はどのパラメータなのか」が一目で分かるようになります。
特に引数の数が多い関数や、同じ型の引数が並ぶ関数では、キーワード引数を積極的に使うと可読性が大きく向上します。
可変長引数(*args)で可変な個数の値を受け取る

可変長引数(*args)は、「0個以上の任意の数の位置引数」をまとめて受け取るときに使います。
def sum_all(*numbers):
# numbersはタプルとして受け取る
print("numbersの中身:", numbers)
total = 0
for n in numbers:
total += n
return total
print(sum_all(1, 2, 3))
print(sum_all(5, 10, 15, 20))
print(sum_all()) # 引数0個でもエラーにならない
numbersの中身: (1, 2, 3)
6
numbersの中身: (5, 10, 15, 20)
50
numbersの中身: ()
0
関数定義側で*numbersと書くことで、渡された位置引数がすべて1つのタプルにまとめられます。
タプルなので、関数内でnumbers自体を書き換えることはできませんが、中に入っているオブジェクトがミュータブルであれば、その中身を変更することは可能です。
キーワード可変長引数(**kwargs)で柔軟なAPIを設計する

キーワード可変長引数(**kwargs)は、任意のキーワード引数をまとめて辞書として受け取る機能です。
def show_config(**options):
# optionsは辞書として受け取る
print("設定内容:", options)
# 個別に取り出すこともできる
host = options.get("host", "localhost")
port = options.get("port", 3306)
debug = options.get("debug", False)
print("host:", host, "port:", port, "debug:", debug)
show_config(host="localhost", port=5432, debug=True)
show_config(host="example.com")
設定内容: {'host': 'localhost', 'port': 5432, 'debug': True}
host: localhost port: 5432 debug: True
設定内容: {'host': 'example.com'}
host: example.com port: 3306 debug: False
将来オプションが増える可能性がある関数や、外部ライブラリから柔軟にパラメータを受け取りたいAPIなどでは、**kwargsが役立ちます。
未知のキーワード引数をまるごと受け取って処理したり、別の関数に渡し直したりできるため、拡張性の高い設計がしやすくなります。
引数の組み合わせと定義順

Pythonでは、関数定義で指定できる引数の種類と順番にルールがあります。
細かく分類すると次のようになります。
| 種類 | 例 | 説明 |
|---|---|---|
| 位置専用引数 | def func(a, b, /) | Python 3.8以降で導入。キーワードでは渡せない引数 |
| 通常の位置・キーワード引数 | def func(a, b) | 位置でもキーワードでも渡せる一般的な引数 |
| 可変長位置引数 | def func(*args) | 追加の位置引数をタプルで受け取る |
| キーワード専用引数 | def func(*, x, y) | キーワードでしか渡せない引数 |
| 可変長キーワード引数 | def func(**kwargs) | 追加のキーワード引数を辞書で受け取る |
本記事の範囲では、基本的な順番として次の並びを押さえておくと十分です。
「位置引数 → デフォルト引数 → *args → キーワード専用引数 → **kwargs」
具体例を見てみます。
def example(a, b=10, *args, c, d=20, **kwargs):
print("a:", a)
print("b:", b)
print("args:", args)
print("c:", c)
print("d:", d)
print("kwargs:", kwargs)
# 呼び出し例
example(1, 2, 3, 4, c=5, x=100, y=200)
a: 1
b: 2
args: (3, 4)
c: 5
d: 20
kwargs: {'x': 100, 'y': 200}
このように、位置引数・キーワード引数・可変長引数を組み合わせることで、柔軟な関数インターフェースを設計できます。
ただし、複雑になりすぎると利用者が混乱しやすくなるので、実務では「シンプルで分かりやすい引数設計」を優先するのがおすすめです。
実践で役立つPython関数引数のテクニック
ミュータブルなデフォルト引数を避ける定石パターン

デフォルト引数にミュータブルなオブジェクトを使うと、関数呼び出しごとにそのオブジェクトが「使い回されてしまう」という有名な問題があります。
まずは問題のある例を見てみます。
def add_item_bad(item, items=[]):
# itemsに要素を追加して、それを返す
items.append(item)
return items
print(add_item_bad(1))
print(add_item_bad(2))
print(add_item_bad(3))
[1]
[1, 2]
[1, 2, 3]
一見便利そうにも見えますが、関数を複数回呼び出すたびに、前回までのリストが引き継がれてしまっていることが問題です。
ほとんどの場合、これは意図しない挙動です。
Pythonでは、デフォルト引数は関数定義時に一度だけ評価され、そのオブジェクトが以後ずっと使われ続けるという仕様があるため、このような挙動になります。
安全なパターンは、デフォルト値としてNoneを使い、関数の中で必要に応じて新しいオブジェクトを生成する書き方です。
def add_item(item, items=None):
if items is None:
# 呼び出しごとに新しいリストを作る
items = []
items.append(item)
return items
print(add_item(1))
print(add_item(2))
print(add_item(3))
[1]
[2]
[3]
このように書いておけば、各呼び出しごとに独立したリストが使われるため、予期せぬ共有状態が生まれにくくなります。
辞書や集合など他のミュータブルなオブジェクトも、同じパターンで扱うのが定石です。
引数のコピー(copy・deepcopy)で副作用を防ぐ

ミュータブルなオブジェクトを引数として受け取りつつ、副作用を避けたい場面は多くあります。
その場合、コピーを作ってから操作するのが有効です。
コピーには浅いコピー(shallow copy)と深いコピー(deep copy)の2種類があります。
import copy
def modify_list_shallow(lst):
# 浅いコピーを作成
copied = lst.copy()
copied.append(100)
return copied
def modify_list_deep(nested_lst):
# 深いコピーを作成
copied = copy.deepcopy(nested_lst)
copied[0].append(999)
return copied
original = [1, 2, 3]
nested = [[1, 2], [3, 4]]
print("modify_list_shallowの例")
print("元:", original)
print("戻り値:", modify_list_shallow(original))
print("元(変更後):", original)
print("\nmodify_list_deepの例")
print("元:", nested)
print("戻り値:", modify_list_deep(nested))
print("元(変更後):", nested)
modify_list_shallowの例
元: [1, 2, 3]
戻り値: [1, 2, 3, 100]
元(変更後): [1, 2, 3]
modify_list_deepの例
元: [[1, 2], [3, 4]]
戻り値: [[1, 2, 999], [3, 4]]
元(変更後): [[1, 2], [3, 4]]
浅いコピーlst.copy()やlist(lst)は、「一段目のコンテナだけ新しくして、中に入っている要素は同じオブジェクトを指す」というコピーです。
一方、copy.deepcopyは、「入れ子になった中身も含めて、すべてを再帰的にコピー」するため、元のデータとは完全に独立した構造になります。
実務では、「どのレベルまで独立させたいか」を意識して、浅いコピーか深いコピーかを選ぶことが重要です。
型ヒントと引数設計でバグを減らす方法

Pythonは動的型付け言語ですが、型ヒント(type hints)を使うことで、引数の設計を明確にし、バグを減らすことができます。
from typing import Optional, List
def add_item_typed(item: int, items: Optional[List[int]] = None) -> List[int]:
"""整数itemをリストitemsに追加して返す関数"""
if items is None:
items = []
items.append(item)
return items
result = add_item_typed(1)
print(result)
# 静的型チェッカー(mypyなど)は、次のような誤用を検出できる:
# add_item_typed("not int") # 型エラーになる
[1]
型ヒントを付けると、次のようなメリットがあります。
- 関数の使い方がコードだけで分かる
どの引数にどの型を渡すべきか、戻り値は何かが明確になります。 - 静的解析ツールが早期にバグ候補を教えてくれる
mypyやpyrightなどのツールを使えば、「本来intを渡すべきところにstrを渡している」といったミスを実行前に検出できます。 - チーム開発での合意が取りやすい
関数の契約(この関数は何を受け取り、何を返すか)が明文化されるため、他の開発者が安心して関数を使えるようになります。
また、ミュータブルな引数が書き換えられるかどうかを明確にするために、ドキュメンテーション文字列(docstring)やコメントで「副作用の有無」を書いておくことも、品質の高いAPI設計には有効です。
まとめ
Pythonの関数引数は、「オブジェクトへの参照を渡す」という仕組みの上に、ミュータブルとイミュータブルの違い、位置引数・キーワード引数・可変長引数など多様な機能が重なっています。
本記事では、リストや辞書が書き換わる理由、ミュータブルなデフォルト引数の罠、コピーによる副作用回避、型ヒントによる設計明確化までを一通り解説しました。
日常的に使う関数でも、引数の性質と設計を意識することで、予期せぬバグを減らし、読みやすく安全なコードを書けるようになります。
今回学んだポイントを、自分のプロジェクトの関数定義に少しずつ取り入れてみてください。
