[Python]常識ですよ?と言われないための引数入門

耳が痛いですね。というか記事のタイトルってこんな感じでいいんでしょうかね?

今回は引数の入門記事です。これからの勉強する方の下地になればいいなっていう感じで書きました。

スコープ


だいたいどの言語でも同じな気がしますが、関数の引数は定義した関数のローカルスコープに所属します。そのため外側の変数名と同じであっても書き換えてしまうということはありません。

>>> a = 1
>>> b = 2
>>> # 外側の変数と同じ引数を定義して
>>> def scopetest(a, b=3):
...     print a, b
... 
>>> # 呼び出しても
>>> scopetest(0)
0 3
>>> # 外側の変数は書き換わっていない
>>> print(a, b)
(1, 2)

定義

関数で定義できる引数は4つの種類があります。

>>> def test(start, end=None, *scores, **mapping):
...     s = sum(scores[start:end])
...     return mapping[s]

(相変わらず状況は謎ですが)上記の例だけでも雰囲気はわかっていただけるんじゃないでしょうか。

以下で説明していきます。定義順はこの通りでなければSyntaxErrorとなります。

通常引数

呼び出し時に指定が必須な引数です。足りない場合はTypeErrorが発生します。動作に必須な引数はこれで定義するべきです。
特別な呼び方がわからなくて適当に通常引数とか言ってたら、初めてのPythonにも通常の引数って書いてあったので安心。

デフォルト引数

イコールのついた引数をキーワード引数と呼びます。

このキーワード引数を関数定義に使用すると呼び出し時に指定した値をデフォルト値とすることができます。
デフォルト引数で指定した値は関数実行ごとに初期化されないことに注意してください。

>>> def test(default=[]):
...     return default
... 
>>> l = test()
>>> print l
[]
 
>>> # lに変更を加える
>>> l.append(1)
>>> m = test()
 
>>> # オブジェクトが同じ
>>> l is m
 
>>> # だから加えた変更が残ってる
>>> print m
[1]

可変(ミュータブル)な値をデフォルト引数としないことが一番ですが、もし必要がある場合は変更を加えたり関数外に持ち出さない等の対策が必要となります。

可変長引数

「通常引数が足りないとTypeError」ですが、定義した以上の引数が与えられた場合もTypeErrorとなります。
可変長引数はこの「過剰な引数」の受け皿となります。(受け皿って何気にいい表現だと思いませんか?)
期待する引数の数が不定なときに使用しましょう。
定義するには引数の前に「*(アスタリスク)」を1つ付けます。受け取った引数は指定した順序でタプルに格納されます。

キーワード可変長引数

(呼び方が正しいかは自信がないので突っ込まないでくださいね!)
定義されていない引数をキーワード引数で指定した場合もTypeErrorとなります。
キーワード可変長引数は「過剰なキーワード引数」の受け皿となります。(受け皿って(ry))

定義するには引数の前に「*(アスタリスク)」を2つ付けます。引数は受け取った時点で辞書となるため順序は無視されます。OrderedDictで指定した順番に引数が入らないのはこのためです。

>>> from collections import OrderedDict
>>> # この指定方法は
>>> OrderedDict(a=1, b=2, c=3)
OrderedDict([('a', 1), ('c', 3), ('b', 2)])
>>> # OrderedDictからすれば辞書を指定しているのと同じ
>>> OrderedDict({'a': 1, 'b': 2, 'c': 3})
OrderedDict([('a', 1), ('c', 3), ('b', 2)])
 
>>> # 話 脱線中
>>> # 順序を保持したければ順番に入れるか
>>> od = OrderedDict()
>>> od['a'] = 1
>>> od['b'] = 2
>>> od['c'] = 3
>>> od
OrderedDict([('a', 1), ('b', 2), ('c', 3)])
 
>>> # 1行で書きたければタプルのリストで(リストのリストでも可)
>>> OrderedDict([('a', 1), ('b', 2), ('c', 3)])                                 
OrderedDict([('a', 1), ('b', 2), ('c', 3)])
 
>>> # 上記はdict.items()と同じ形式です
>>> od.items()
[('a', 1), ('b', 2), ('c', 3)]
 
>>> # 辞書にすることもできるんです(どうでもいい)
>>> dict([('a', 1), ('b', 2), ('c', 3)])
{'a': 1, 'c': 3, 'b': 2}

(脱線終わり)

また、デフォルト引数では指定した値が初期化されない問題があると言ったため「もしかしたら」と思ったかもしれません。確認してみると面白い挙動をしてますがキーワード可変長引数では同じような問題は発生しないことがわかりました。

>>> i = 0
>>> def kwtest(**kw):
...     global i
...     print(id(kw))
...     kw[str(i)] = i
...     i += 1
...     return kw
... 
>>> kwtest()
139915519212072
{'0': 0}
>>> kwtest()
139915519220544
{'1': 1}
 
>>> # 1回目と同じアドレスが使われている(139915519212072 == 139915519212072)
>>> kwtest()
139915519212072
{'2': 2}
 
>>> # 2回目と同じアドレスが使われている(139915519220544 == 139915519220544)
>>> a = kwtest()
139915519220544
 
>>> # 関数外に持ち出すと違うアドレスが発行される
>>> b = kwtest()
139915519167296
 
>>> # また違うアドレスが発行される
>>> c = kwtest()
139915519218304

関数内でのみ利用される場合は同じアドレスが使いまわされるようです。上記の例ではあえて毎回違うキーを設定しているのですが都度初期化されているんですかね。とりあえず大丈夫だってことだけ覚えとけば大丈夫だと思います。

また、可変長引数は複数定義することができません。どこが区切りなのかわからないですから。(可変長引数とキーワード可変長引数は混在できます)

呼び出し

普通の関数呼び出しは解説しなくても大丈夫だと思うので省略します。

キーワード引数

呼び出しでのキーワード引数は明示的な引数指定を意味します。
明示的な指定では引数の定義順を無視することができます。

>>> def kwtest(a, b, c):
...     print(a, b, c)                                                          
...
>>> # 通常の呼び出し 
>>> kwtest(1, 2, 3)
(1, 2, 3)
>>> # 定義した順番でなくてもいい
>>> kwtest(b=2, c=3, a=1)                                                  
(1, 2, 3)
>>> # キーワード引数は最後
>>> kwtest(b=2, c=1, 3)
  File "", line 1
SyntaxError: non-keyword arg after keyword arg

引数の展開

リスト(タプル)や辞書の内容をそのまま引数として展開(適用)することができます。
「*(アスタリスク1つ)」はリスト(タプル)を「通常引数」「デフォルト引数」「可変長引数」に対して展開します。
「**(アスタリスク2つ)」は辞書を「デフォルト引数」「キーワード可変長引数」に対して展開します。

>>> def apply_test(normal, default=1, *args, **kwargs):
...     print normal
...     print default
...     print args
...     print kwargs
... 
>>> args = [1, 2, 3, 4]
>>> # *での展開は「通常引数」「デフォルト引数」「可変長引数」に対して行われる
>>> apply_test(*args)
1
2
(3, 4)
{}
 
>>> kwargs = {'default': 5, 'etc': 100}
>>> # **での展開は「デフォルト引数」「キーワード可変長引数」に対して行われる
>>> apply_test(1, **kwargs)                                                     
1
5
()
{'etc': 100}
 
>>> # 「default」引数がダブったためエラー(既に2が指定されている)
>>> apply_test(1, 2, *args, **kwargs)
Traceback (most recent call last):
  File "", line 1, in 
TypeError: apply_test() got multiple values for keyword argument 'default'
 
>>> # 辞書を*で展開するとキーが展開される
>>> apply_test(*{'a': 1, 'b': 2, 'c': 3})
a
c
('b',)
{}
 
>>> # setもOK
>>> apply_test(*set([1, 2]))
1
2
()
{}
 
>>> # リスト(タプル)を**で展開できない(あたりまえですね)
>>> apply_test(**[1, 2])
Traceback (most recent call last):
  File "", line 1, in 
TypeError: apply_test() argument after ** must be a mapping, not list

引数の展開というとJavaScriptにはapplyがありますが、実はPythonにもapply関数があります。

>>> apply
<built-in function apply>
 
>>> # apply(対象の関数, リストorタプル, 辞書)
>>> apply(apply_test, (1,), kwargs)
1
5
()
{'etc': 100}

これは*と**ができるまで使われていた方法らしく、現状で必要になることはほとんどないでしょう。
Python3ではビルトインからなくなっていました。

まとめてみました。

書き方 定義 呼び出し
arg=value デフォルト引数 明示的な値の指定
*args 可変長引数 リストの展開
**kwargs キーワード可変長引数 辞書の展開

参照渡し

Pythonの引数はすべて「C言語でいう参照渡し」です。以下を見てください。

>>> def ref(a):
...     print(id(a))
... 
 
>>> # 27857240 == 27857240
>>> i = 1
>>> print(id(i))
27857240
>>> ref(i)
27857240
 
>>> # 140186227500816 == 140186227500816
>>> s = 'a'
>>> print(id(s))
140186227500816
>>> ref(s)
140186227500816
 
>>> # 140186226383256 == 140186226383256
>>> l = [1]
>>> print(id(l))
140186226383256
>>> ref(l)
140186226383256

関数内でもアドレスが変わっていません。リファレンスが渡っているということです。

参照渡し(リファレンス渡し)というと書換可能なイメージがあるかもしれませんが、Pythonでの書換可否は(基本的に)オブジェクトのミュータブル(可変)・イミュータブル(不可変)によって決まります。
代表的なイミュータブルなオブジェクトを以下に列挙しました。

  • int
  • str
  • bool
  • None
  • tuple
  • frozenset

これらはオブジェクト自身を変更することができないため、関数内で値渡しが行われているように見えるのです。