Pythonでのパラメータの強制命名


111

Pythonでは、関数定義がある場合があります。

def info(object, spacing=10, collapse=1)

これは、次のいずれかの方法で呼び出すことができます。

info(odbchelper)                    
info(odbchelper, 12)                
info(odbchelper, collapse=0)        
info(spacing=15, object=odbchelper)

名前が付けられている限り、Pythonでは任意の順序の引数を使用できるため、

私たちが抱えている問題は、いくつかの大きな関数が成長するにつれて、spacingとの間にユーザーがパラメーターを追加collapseしている可能性があります。つまり、名前が付けられていないパラメーターに誤った値が渡される可能性があります。さらに、何を入れる必要があるかが常に明確であるとは限らない場合もあります。コーディング基準だけでなく、フラグまたはpydevプラグインだけでなく、特定のパラメーターに名前を付けるようにユーザーを強制する方法が必要ですか?

上記の4つの例では、すべてのパラメーターに名前が付けられているため、最後のものだけがチェックに合格します。

奇妙なことに、特定の機能に対してのみオンにするつもりですが、これを実装する方法に関する提案や、それが可能かどうかさえあれば、喜んでいただけます。

回答:


214

Python 3では、はい、*引数リストで指定できます。

ドキュメントから:

「*」または「* identifier」の後のパラメーターはキーワードのみのパラメーターであり、使用されたキーワード引数にのみ渡すことができます。

>>> def foo(pos, *, forcenamed):
...   print(pos, forcenamed)
... 
>>> foo(pos=10, forcenamed=20)
10 20
>>> foo(10, forcenamed=20)
10 20
>>> foo(10, 20)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() takes exactly 1 positional argument (2 given)

これは以下と組み合わせることもできます**kwargs

def foo(pos, *, forcenamed, **kwargs):

31

次のように関数を定義することで、Python3でキーワード引数を使用するように強制できます。

def foo(*, arg0="default0", arg1="default1", arg2="default2"):
    pass

最初の引数を名前のない位置引数にすると、関数を呼び出すすべての人がキーワード引数を使用するように強制されます。Python2でこれを行う唯一の方法は、このような関数を定義することです

def foo(**kwargs):
    pass

これにより、呼び出し側はkwargsを使用するように強制されますが、必要な引数のみを受け入れるようにチェックを行う必要があるため、これはそれほど優れた解決策ではありません。


11

確かに、ほとんどのプログラミング言語では、パラメーターの順序を関数呼び出しコントラクトの一部にしていますが、そうである必要ありません。なぜでしょうか?私の質問に対する理解は、Pythonがこの点で他のプログラミング言語と異なるかどうかです。Python 2に関する他の適切な回答に加えて、次の点を考慮してください。

__named_only_start = object()

def info(param1,param2,param3,_p=__named_only_start,spacing=10,collapse=1):
    if _p is not __named_only_start:
        raise TypeError("info() takes at most 3 positional arguments")
    return str(param1+param2+param3) +"-"+ str(spacing) +"-"+ str(collapse)

呼び出し元が引数を指定spacingしてcollapse(例外なしで)位置を指定できる唯一の方法は次のとおりです。

info(arg1, arg2, arg3, module.__named_only_start, 11, 2)

他のモジュールに属するプライベート要素を使用しないという慣習は、Pythonではすでに非常に基本的です。Python自体と同様に、このパラメーターの規則は半強制的です。

それ以外の場合、呼び出しは次の形式である必要があります。

info(arg1, arg2, arg3, spacing=11, collapse=2)

電話

info(arg1, arg2, arg3, 11, 2)

値11をパラメーターに割り当て_p、関数の最初の命令によって例外が発生します。

特徴:

  • 以前のパラメーター_p=__named_only_startは、位置的に(または名前によって)許可されます。
  • 後のパラメーター_p=__named_only_startは、名前のみで提供する必要があります(特別な標識オブジェクトに関する知識を__named_only_start取得して使用する場合を除く)。

長所:

  • パラメータの数と意味は明確です(もちろん、適切な名前も選択されている場合)。
  • 番兵が最初のパラメーターとして指定されている場合、すべての引数は名前で指定する必要があります。
  • 関数を呼び出すとき__named_only_startに、対応する位置にある番兵オブジェクトを使用して位置モードに切り替えることができます。
  • 他の選択肢よりも優れたパフォーマンスが期待できます。

短所:

  • チェックはコンパイル時ではなく実行時に行われます。
  • 追加のパラメーター(引数ではない)と追加のチェックの使用。通常の機能に比べてパフォーマンスが少し低下します。
  • 機能は、言語による直接サポートのないハックです(下記の注を参照)。
  • 関数を呼び出すとき__named_only_startに、正しい位置にある番兵オブジェクトを使用して位置モードに切り替えることができます。はい、これはプロと見なすこともできます。

この回答はPython 2に対してのみ有効であることに注意してください。Python3は、他の回答で説明されている、非常に洗練された、言語に対応した同様のメカニズムを実装しています。

心を開いて考えてみると、質問や他人の決定は、ばかげている、ばかげている、またはばかげているように見えます。それどころか、私は通常多くのことを学びます。


「チェックは、コンパイル時ではなく実行時に行われます。」-それはすべての関数の引数チェックに当てはまると思います。関数呼び出しの行を実際に実行するまでは、実行されている関数が常にわかるとは限りません。また、+ 1-これは賢いです。
Eric

@エリック:それは私が静的チェックを好んだというだけのことです。しかし、あなたの言うとおりです。それは、Pythonではまったくなかったでしょう。決定的なポイントではありませんが、Python 3の「*」構造も動的にチェックされます。コメントありがとうございます。
マリオロッシ

また、モジュール変数_named_only_startに名前を付けると、外部モジュールから参照できなくなり、賛否両論が出てきます。(モジュールスコープでの単一のアンダースコアはプライベート、IIRC)
Eric

センチネルの命名に関しては、a __named_only_startとa named_only_start(最初の下線なし)の両方を指定することもできます。2番目は、名前付きモードが「推奨」されていることを示しますが、「積極的に推進される」レベルではありません(1つはパブリックであり、他はありません)。_namesアンダースコアで始まる"プライベート"については、言語によって強く強制されているわけではありません。特定の(*以外の)インポートまたは修飾名を使用することで簡単に回避できます。これが、いくつかのPythonドキュメントが「非公開」ではなく「非公開」という用語の使用を好む理由です。
マリオロッシ

6

「自然に」発生しないデフォルト値で「偽の」最初のキーワード引数を作成することにより、Python 2とPython 3の両方で機能する方法でそれを行うことができます 。そのキーワード引数の前に、値のない1つ以上の引数を付けることができます。

_dummy = object()

def info(object, _kw=_dummy, spacing=10, collapse=1):
    if _kw is not _dummy:
        raise TypeError("info() takes 1 positional argument but at least 2 were given")

これにより、次のことが可能になります。

info(odbchelper)        
info(odbchelper, collapse=0)        
info(spacing=15, object=odbchelper)

だがしかし:

info(odbchelper, 12)                

関数を次のように変更した場合:

def info(_kw=_dummy, spacing=10, collapse=1):

その場合、すべての引数にはキーワードが必要であり、機能しinfo(odbchelper)なくなります。

これにより_kw、最後のエントリの後にそれらを置くことを強制せずに、の後の任意の場所に追加のキーワード引数を配置できます。たとえば、論理的にグループ化したり、キーワードをアルファベット順に並べたりすると、メンテナンスと開発に役立ちます。

そのためdef(**kwargs)、スマートエディターで署名情報を使用したり、失ったりする必要はありません。あなたの社会的契約は、特定の情報を提供することであり、それらの一部を強制的にキーワードに要求することによって、それらが提示される順序は無関係になりました。


2

更新:

を使用して**kwargsも問題が解決しないことに気づきました。プログラマが望みどおりに関数の引数を変更した場合、たとえば、関数を次のように変更できます。

def info(foo, **kwargs):

そして、古いコードは再び壊れます(すべての関数呼び出しに最初の引数を含める必要があるため)。

それは本当にブライアンが言うことに帰着します。


(...)spacingcollapse(...)の間にパラメータを追加している可能性があります

一般に、関数を変更する場合、新しい引数は常に最後に移動する必要があります。それ以外の場合は、コードが破損します。明らかなはずです。
コードが壊れるように誰かが関数を変更した場合、この変更は拒否される必要があります。
(ブライアンが言うように、それは契約のようなものです)

(...)場合によっては、何を入れる必要があるかが常に明確ではないことがあります。

関数のシグネチャ(つまりdef info(object, spacing=10, collapse=1))を見ると、デフォルト値を持たないすべての引数が必須であることがすぐにわかります。
どのような引数があるため、ドキュメンテーション文字列に行く必要があります。


古い回答(完全性を保つため)

これはおそらく良い解決策ではありません:

次の方法で関数を定義できます。

def info(**kwargs):
    ''' Some docstring here describing possible and mandatory arguments. '''
    spacing = kwargs.get('spacing', 15)
    obj = kwargs.get('object', None)
    if not obj:
       raise ValueError('object is needed')

kwargsキーワード引数を含む辞書です。必須の引数が存在するかどうかを確認し、存在しない場合は例外を発生させることができます。

欠点は、どの引数が可能であるかはもはやそれほど明白ではないかもしれませんが、適切なdocstringがあれば、それは問題ないはずです。


3
私はあなたの以前の答えがより好きでした。関数で** kwargsのみを受け入れる理由についてコメントを入力してください。結局のところ、誰もがソースコードのすべてを変更できます。意思決定の背後にある意図と目的を説明するドキュメントが必要です。
Brandon

この答えには実際の答えはありません!
フィル

2

python3キーワードのみの引数(*)はpython2.xでシミュレートできます**kwargs

次のpython3コードを考えてみましょう:

def f(pos_arg, *, no_default, has_default='default'):
    print(pos_arg, no_default, has_default)

そしてその行動:

>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes 1 positional argument but 3 were given
>>> f(1, no_default='hi')
1 hi default
>>> f(1, no_default='hi', has_default='hello')
1 hi hello
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() missing 1 required keyword-only argument: 'no_default'
>>> f(1, no_default=1, wat='wat')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() got an unexpected keyword argument 'wat'

これは以下を使用してシミュレートできます。私は切り替えの自由を取っていることに注意してください TypeErrorKeyErrorする場合、「引数という名前のREQUIRED」で、それも同じ例外タイプことを確認するためにあまりにも多くの仕事ではないでしょうが

def f(pos_arg, **kwargs):
    no_default = kwargs.pop('no_default')
    has_default = kwargs.pop('has_default', 'default')
    if kwargs:
        raise TypeError('unexpected keyword argument(s) {}'.format(', '.join(sorted(kwargs))))

    print(pos_arg, no_default, has_default)

そして行動:

>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes exactly 1 argument (3 given)
>>> f(1, no_default='hi')
(1, 'hi', 'default')
>>> f(1, no_default='hi', has_default='hello')
(1, 'hi', 'hello')
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
KeyError: 'no_default'
>>> f(1, no_default=1, wat='wat')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in f
TypeError: unexpected keyword argument(s) wat

レシピはpython3.xでも同様に機能しますが、python3.xのみの場合は避けてください。


ああ、それでkwargs.pop('foo')Python 2のイディオムは?コーディングスタイルを更新する必要があります。私はまだ🤔のPython 3に、このアプローチを使用していた
ニール・

0

関数を受信**argsのみとして宣言できます。これはキーワード引数を義務付けますが、有効な名前のみが渡されるようにするために、追加の作業が必要になります。

def foo(**args):
   print args

foo(1,2) # Raises TypeError: foo() takes exactly 0 arguments (2 given)
foo(hello = 1, goodbye = 2) # Works fine.

1
キーワードチェックを追加する必要があるだけでなく、署名付きのメソッドを呼び出さなければならないことを知っているコンシューマーについて考えてfoo(**kwargs)ください。私はそれに何を渡しますか?foo(killme=True, when="rightnowplease")
ダグルームズ

それは本当に依存します。考えてくださいdict
Noufal Ibrahim 2017

0

他の回答が言うように、関数のシグネチャを変更することは悪い考えです。最後に新しいパラメーターを追加するか、引数が挿入されている場合はすべての呼び出し元を修正してください。

それでもやりたい場合は、関数デコレータinspect.getargspec関数を使用します。これは次のように使用されます。

@require_named_args
def info(object, spacing=10, collapse=1):
    ....

の実装はrequire_named_args、読者のための演習として残されています。

気にしません。関数が呼び出されるたびに遅くなり、コードをより慎重に記述することにより、より良い結果が得られます。


-1

あなたは**演算子を使うことができます:

def info(**kwargs):

この方法では、名前付きパラメーターを使用する必要があります。


2
また、コードを読み取らずにメソッドを呼び出す方法がわからないため、コンシューマーの認知負荷が増大します:(
Dagrooms

前述の理由により、これは非常に悪い習慣であり、回避する必要があります。
David S.

-1
def cheeseshop(kind, *arguments, **keywords):

Pythonで* argsを使用する場合、これは、このパラメーターにn個の引数を渡すことができることを意味します-これは、アクセスする関数内のリストになります

そして、キーワード引数を意味する** kwを使用する場合、これはdictとしてアクセスできます-n-numberのkw引数を渡すことができ、そのユーザーがシーケンスと引数を順番に入力する必要がある場合は使用しないでください。 *および**-(大規模なアーキテクチャ向けの一般的なソリューションを提供するためのPythonの方法...)

関数をデフォルト値で制限したい場合は、関数内をチェックできます

def info(object, spacing, collapse)
  spacing = spacing or 10
  collapse = collapse or 1

間隔を0にしたい場合はどうなりますか?(答え、あなたは10を得る)。この答えは、同じ理由で他のすべての** kwargsの答えと同じくらい間違っています。
Phil

-2

そもそもプログラマが他の2つの間にパラメータを追加する理由がわかりません。

関数パラメーターを名前で使用する場合(例: info(spacing=15, object=odbchelper))する場合は、それらが定義されている順序は重要ではないため、新しいパラメーターを最後に配置することもできます。

順序を問題にしたい場合は、変更しても何も期待できません。


2
これは質問の答えにはなりません。それが良い考えかどうかは関係ありません-誰かがとにかくそれをするかもしれません。
Graeme Perrow、2015年

1
Graemeが述べたように、誰かがとにかくそれをします。また、他の人が使用するライブラリを作成している場合は、(Python 3のみ)キーワードのみの引数を渡すことで、APIをリファクタリングする必要があるときに柔軟性が高まります。
s0undt3ch 2016
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.