2019-08-05

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

耳が痛いですね。記事のタイトルってこんな感じで煽ればいいんでしょうか? 今回は引数の入門記事です。これからの勉強する方の下地になればいいなって思って書きました。

info
  • この記事は もともと Python2 の動作をもとに書いていましたが、Python3で動作するように書き換えました。
  • いくつか Python2 では動作しない箇所があるので注意してください。

定義

用語自体は別に覚えなくてもいいですが、定義の際につかう引数を 仮引数 と呼びます。(一応ね)

この 仮引数 は4つの種類があります。次のように定義します。

>>> def test(start, end=None, *array, **mapping): ... print(start, end, array, mapping) >>> test(1, 10, 20, 30, 40, a=50, b=60, c=70) 1 10 (20, 30, 40) {'a': 50, 'b': 60, 'c': 70}

状況は謎ですが上記の例で雰囲気を感じ取ってください。

以下で説明していきます。

定義順が不正だと SyntaxError: invalid syntax となるので注意してください。

位置引数

呼び出し時に指定が必須な引数です。 =* がなく、単純に引数名だけが指定された引数とも言えます。 上記の例で言うと 1, 10, 20, 30, 40 が該当します。

挨拶をする関数を考えてみましょう。挨拶するフレーズを位置引数として定義してみます。

  • 定義
  • 呼び出し
  • def 挨拶(言葉): print(言葉)
  • >>> 挨拶('おはよう') おはよう >>> 挨拶() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 挨拶() missing 1 required positional argument: '言葉'

足りない場合はTypeErrorが発生します。動作に必須な引数はこれで定義するべきです。

参考

用語集>>>, インタラクティブシェルにおけるデフォルトの Python プロンプトです。インタプリタでインタラクティブに実行されるコード例でよく出てきます。,,..., 次のものが考えられます:- インタラクティブシェルにおいて、インデントされたコードブロック、対応する左右の区切り文字の組 (丸括弧、角括弧、波括弧、三重引用符) の内側、デコレーターの後に、コードを入力する際に表示されるデフォル...https://docs.python.org/3/glossary.html

デフォルト引数

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

このキーワード引数を関数定義に使用すると呼び出し時に指定した値をデフォルト値とすることができます。

「呼び出し時に指定しなくても普通はこの値を使ってほしい」という場合に利用するものです。

先程の挨拶する関数を少し改良してみます。 多くの場合、挨拶は朝にするものなので何も入力しない場合デフォルトで「おはよう」が出力されるようにしてみます。

  • 定義
  • 呼び出し
  • def 挨拶(言葉='おはよう'): print(言葉)
  • # 引数を指定しなくても呼び出せて便利! >>> 挨拶() おはよう # 別のフレーズで挨拶したい場合は 引数を指定する >>> 挨拶('こんばんは') こんばんは
info
  • Python3 からは平仮名や漢字のようなマルチバイト文字も変数名(関数名)として使えるようになりました。
warning
  • デフォルト引数で指定した値は関数実行ごとに初期化されないことに注意してください。

  • >>> def test(default=[]): ... return default ... >>> l = test() >>> print(l) [] >>> # lに変更を加える >>> l.append(1) >>> m = test() >>> # オブジェクトが同じ >>> l is m True >>> # だから加えた変更が残ってる >>> print(m) [1]
  • 可変(ミュータブル)な値をデフォルト引数としないことが一番ですが、 もし必要がある場合は変更を加えたり関数外に持ち出さない等の対策が必要となります。

可変長位置引数

位置引数が足りないとTypeError と説明しましたが、 本来は定義した以上の引数が与えられた場合もTypeErrorとなります。

可変長引数はこの 過剰な引数の受け皿 となります。(受け皿って何気にいい表現だと思いませんか?)

期待する引数の数が不定な場合に利用する引数と言えます。もちろん 0個でもOK です。

JavaScript に慣れている方はスプレッド構文と同じものと理解して概ね差し支えありません。

定義するには引数の前に *(アスタリスク) を1つ付けます。 受け取った引数は指定した順序でタプルに格納されるので、 for文などでループして利用することが多いです。

今度は 会話 という関数を定義してみます。 可変長引数だけでは面白くないので 挨拶のフレーズだけは必須というようにしてみます。

  • 定義
  • 呼び出し
  • def 会話(挨拶, *それ以降): print(挨拶) for 言葉 in それ以降: print(言葉)
  • # 少しそっけない >>> 会話('おはよう') おはよう # 第二フレーズ以上しゃべろう! >>> 会話('おはよう', '今日は天気いいね', 'え、なんで無視するの!?') おはよう 今日は天気いいね え、なんで無視するの!?

挨拶だけでなく会話のキャッチボールを楽しみましょう()

可変長キーワード引数

Pythonでは 定義されていない引数をキーワード引数で呼び出した場合もTypeErrorとなります。

キーワード可変長引数は 過剰なキーワード引数の受け皿 となります。受け皿って(ry

定義するには引数の前に **(アスタリスク2つ) を付けます。これはコマンド等のオプションを受け取るときなどによく使われます。

ラーメンを注文し、麺の硬さや油の量、トッピングなどをキーワード可変長引数で受け取る例で考えてみましょう

  • 定義
  • 呼び出し
  • def ラーメン(, **細かい注文): print(f'{}ラーメン') for key, value in 細かい注文.items(): print(f'{key}{value}') print('ほか全部普通で!')
  • >>> ラーメン('醤油',='硬め',='少なめ', チャシュー='あり', コーン='なし') 醤油ラーメン 麺硬め 油少なめ チャシューあり コーンなし ほか全部普通で!
warning
  • 可変長引数は複数定義することができません。どこが区切りなのかわからないですからね。
  • 可変長引数(*) とキーワード可変長引数(**) は混在できます
info
  • 可変長引数は関数外に持ち出しても問題ありません。

キーワード専用引数

4種類と言ったな、あれは嘘だ(5つめ)

info
  • 公式では キーワード専用 (3.5の用語集)とか キーワードのみ (3.7以降の用語集)と書かれていましたが、 なんか後者はしっくりこないのでここでは キーワード専用引数と呼ぶことにします。

* や 可変長位置引数の後ろに定義された引数は キーワード専用引数 と呼ばれ、キーワード引数として指定しなければ呼び出せないようになりました。(関数の呼び出しについては後述)

warning
  • * 自体が キーワード専用引数ではなく、 * の後に定義された引数が キーワード専用引数なので混同しないようにしてください。

もう少しだけ掘り下げて説明します。

可変長位置引数は * を前につけて宣言する引数でしたね。

これをつけるといくら位置引数を指定しても可変長位置引数が受け皿として吸収してしまうため この後ろに位置引数を宣言することはできないのです。

Python2系ではそのような引数を定義すると構文エラーになりましたが、 Python3系ではキーワード専用引数として扱われるようになりました。

そして、受け取る可変長位置引数がない場合は * だけを引数に指定できるというだけで * そのものに大した意味があるわけではありません。

いわば省略された可変長引数と言えるでしょうか。(おかしいかな?ツッコミ待ち)

関数の呼び出しについては次のセクションから説明したほうが混乱は少ないと思ったんですが、 まぁこれらを切り離して考えることはできませんし、上記で既に呼び出しはやっているので気にせず例を書こうと思います。

先程のラーメンの関数で2種類のトッピングに絞ってみます。

  • 定義
  • 呼び出し
  • def ラーメン(, *, メンマ, チャーシュー='なし'): print(f'{}ラーメン, メンマ{メンマ}, チャーシュー{チャーシュー}')
  • >>> ラーメン('みそ', メンマ='なし', チャーシュー='あり') みそラーメン, メンマなし, チャーシューあり >>> ラーメン('みそ', メンマ='なし') みそラーメン, メンマなし, チャーシューなし # メンマは必須 >>> ラーメン('みそ') Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: ラーメン() missing 1 required keyword-only argument: 'メンマ' # 位置引数指定はできません (可変長位置引数だったら指定できた) >>> ラーメン('みそ', 'なし', 'あり') Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: ラーメン() takes 1 positional argument but 3 were given

引数の後ろに キーワード専用の指定があるため、 それ以降のメンマ、チャーシュー引数を指定する場合はキーワード指定が必須となります。

あまり深く理解できなくても、 キーワード引数で明示したほうが何をしてるかわかりやすいな、くらいの感想は持ってもらえましたか? それでOKです。

info
  • ちなみに可変長位置引数 との同居はできません。 SyntaxError になります。
  • >>> def test(a, *d, *, c): print(a, c) File "<stdin>", line 1 def test(a, *d, *, c): print(a, c)

呼び出し

既に散々やってますが、関数の定義以外で () をつけると関数を実行できます。 呼び出しとも言います。(この記事では呼び出しといいます)

定義の際の引数を 仮引数 と呼ぶのに対し、 呼び出しの際の引数を 実引数 と呼びます。(一応ね)

位置引数の呼び出しは省略しても大丈夫だと思いますが、強いて言うなら ラーメン('みそ', 'なし', 'あり') みたいに引数名だけを指定して呼び出すことです

キーワード引数

定義でのキーワード引数は デフォルト引数(暗示) でしたが、 呼び出しでのキーワード引数は 指定(明示)を意味します。

明示的な指定では引数の定義順を無視することができます。 個人的には、省略可能な引数はキーワード引数として書くことが多いです。

今回は使い慣れた datetime.datetime の引数を指定してみます。

ちなみに datetime.datetime 関数の引数はこんな感じになっております。

Help on class datetime in module datetime: class datetime(date) | datetime(year, month, day[, hour[, minute[, second[, microsecond[,tzinfo]]]]]) | | The year, month and day arguments are required. tzinfo may be None, or an

左から , , , 時間, , , マイクロ秒, タイムゾーン を指定することになっていて、年,月,日 だけ必須です。

2022/5/22 00:00:30 を指すように引数を適当に指定してみます.

>>> from datetime import datetime # 必要な引数が指定できていれば順番は関係ない >>> datetime(day=22, month=5, year=2022, second=30) datetime.datetime(2022, 5, 22, 0, 0, 30) # このように入力したのと同じ >>> datetime(2000, 5, 22, 0, 0, 30) datetime.datetime(2022, 5, 22, 0, 0, 30) # キーワード引数より前に位置引数があってはならない >>> datetime(day=22, month=5, second=30, 2022) File "<stdin>", line 1 SyntaxError: positional argument follows keyword argument

また、ライブラリのアップデート等で、関数の引数順が入れ替わったりしてもキーワード引数で明示している場合は、 影響なく利用できたりするのでそういった背景で利用することもあるかもしれません。(状況によります)

引数の展開

呼び出しの場合は *, ** は引数の展開を意味します。アンパックとも言います。

リスト(タプル)や辞書の内容をそのまま引数として展開(適用)することができます。

  • *(アスタリスク1つ) はリストのような一次元のオブジェクトを仮引数に対して左から適用します。
    • 「位置引数」「デフォルト引数」「可変長引数」が対象
  • **(アスタリスク2つ) は辞書オブジェクトを キーと一致している引数に対して適用します。
    • 「位置引数」「デフォルト引数」「キーワード可変長引数」「キーワード専用引数」が対象

今回は 2022/5/22 00:45:30 を指す datetime オブジェクトを作るように 引数を展開してみましょう (datetime は引数が多くてわかりやすいのでこの例には最適だな〜)

>>> datetime(*[2022, 5, 22, 0, 45, 30]) datetime.datetime(2022, 5, 22, 0, 45, 30) >>> datetime(**{'year': 2022, 'month': 5, 'day': 22, 'minute': 45, 'second': 30}) datetime.datetime(2022, 5, 22, 0, 45, 30) >>> datetime(2022, *[5, 22], minute=45, **{'second': 30}) datetime.datetime(2022, 5, 22, 0, 45, 30) >>> datetime(2022, *[5, 22], **{'second': 30}, minute=45) datetime.datetime(2022, 5, 22, 0, 45, 30) # ``**`` (辞書の展開)は ``*`` (リストの展開)より前に来てはいけない >>> datetime(2022, minute=45, **{'second': 30}, *[5, 22]) File "<stdin>", line 2 SyntaxError: iterable argument unpacking follows keyword argument unpacking # 引数がかぶってはいけない(minuteを指す引数が2つある) >>> datetime(2022, *[5, 22], **{'second': 30, 'minute': 45}, minute=45) Traceback (most recent call last): File "<stdin>", line 2, in <module> TypeError: type object got multiple values for keyword argument 'minute' # リスト(タプル)を**で展開できない(あたりまえですね) >>> datetime(**[2022, 5, 22, 0, 45, 30]) Traceback (most recent call last): File "<stdin>", line 2, in <module> TypeError: type object argument after ** must be a mapping, not list
info
  • 引数の展開というと JavaScript には apply がありますが、実はPython2系までは apply関数 がありました

  • >>> apply <built-in function apply> >>> from datetime import datetime >>> apply(datetime, (2022, 5, 22)) datetime.datetime(2022, 5, 22, 0, 0)
  • これは *** ができるまで使われていた方法らしく、現状で必要になることはほとんどないでしょう。

  • Python3ではビルトインからなくなっていました。

書き方についてまとめてみます。

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

スコープ

info
  • スコープについては覚えなくても最低限の利用はできます。
  • まだ自分には難しいという人はスキップして大丈夫です。

関数は ローカルスコープ と呼ばれるPythonで最も狭いスコープを持ちます。

関数の中で定義した オブジェクト (関数、変数、クラスすべて) 及び、 受け取った 引数 は この ローカルスコープ に属します。

そのため 外側(グローバルスコープ) の変数名と重複したり、 他の関数の 変数名(あるいは引数名) と重複しても書き換える心配はありません。

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

スコープについてもっと知りたい方は Pythonのスコープについて をごらんください。

参照

このセクションはおまけです。

Pythonの引数には 値渡し という概念はなく、すべて参照が渡ります。

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

代表的なイミュータブルなオブジェクトを以下に列挙しました。

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

これらはオブジェクト自身を変更することができないため、 関数で値渡しが行われているように感じるかもしれませんが、単なるオブジェクトの性質によるものです。

warning
  • C や Golang の 参照渡しとは異なるので 参照渡しというと マサカリが飛んでくる恐れがあります。
  • 用法用量を守って正しくお使いください。

それでは関数内で同じオブジェクトを参照していることを確認してみます。

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

関数内でもアドレスが変わっていませんね。

この性質を利用して引数に指定した変数を編集するような破壊的な関数も簡単に定義することができます。

そのようなコードはバグの温床になるため乱用は禁物ですが、 Pythonista の一般教養として知っておくべきことだと思います。