Penamaan paksa parameter dengan Python

111

Di Python Anda mungkin memiliki definisi fungsi:

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

yang dapat dipanggil dengan salah satu cara berikut:

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

terima kasih kepada Python yang mengizinkan argumen urutan apa pun, asalkan diberi nama.

Masalah yang kami hadapi adalah karena beberapa fungsi kami yang lebih besar berkembang, orang mungkin menambahkan parameter di antara spacingdan collapse, yang berarti bahwa nilai yang salah mungkin menuju ke parameter yang tidak dinamai. Selain itu terkadang tidak selalu jelas tentang apa yang perlu dimasukkan. Kami mencari cara untuk memaksa orang memberi nama parameter tertentu - bukan hanya standar pengkodean, tetapi idealnya bendera atau plugin pydev?

Sehingga dalam 4 contoh di atas, hanya yang terakhir yang lolos pemeriksaan karena semua parameter diberi nama.

Kemungkinan besar kami hanya akan mengaktifkannya untuk fungsi tertentu, tetapi saran apa pun tentang cara menerapkan ini - atau bahkan jika memungkinkan akan dihargai.

Mark Mayo
sumber

Jawaban:

214

Dalam Python 3 - Ya, Anda dapat menentukannya *di daftar argumen.

Dari dokumen :

Parameter setelah "*" atau "* pengenal" adalah parameter khusus kata kunci dan hanya boleh diberikan argumen kata kunci bekas.

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

Ini juga dapat dikombinasikan dengan **kwargs:

def foo(pos, *, forcenamed, **kwargs):
Eli Bendersky
sumber
32

Anda dapat memaksa orang untuk menggunakan argumen kata kunci di Python3 dengan mendefinisikan fungsi dengan cara berikut.

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

Dengan membuat argumen pertama sebagai argumen posisi tanpa nama, Anda memaksa setiap orang yang memanggil fungsi untuk menggunakan argumen kata kunci yang menurut saya Anda tanyakan. Di Python2, satu-satunya cara untuk melakukan ini adalah dengan mendefinisikan fungsi seperti ini

def foo(**kwargs):
    pass

Itu akan memaksa penelepon untuk menggunakan kwargs tetapi ini bukan solusi yang bagus karena Anda kemudian harus memberi tanda centang untuk hanya menerima argumen yang Anda butuhkan.

martega
sumber
11

Benar, kebanyakan bahasa pemrograman menjadikan urutan parameter sebagai bagian dari kontrak pemanggilan fungsi, tetapi ini tidak harus demikian. Mengapa demikian? Pemahaman saya tentang pertanyaan ini adalah, jika Python berbeda dengan bahasa pemrograman lain dalam hal ini. Selain jawaban bagus lainnya untuk Python 2, harap pertimbangkan yang berikut:

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

Satu-satunya cara pemanggil dapat memberikan argumen spacingdan secara collapseposisional (tanpa pengecualian) adalah:

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

Konvensi untuk tidak menggunakan elemen privat milik modul lain sudah sangat mendasar di Python. Seperti pada Python itu sendiri, konvensi untuk parameter ini hanya akan diterapkan secara semi-diterapkan.

Jika tidak, panggilan harus dalam bentuk:

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

Sebuah panggilan

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

akan menetapkan nilai 11 ke parameter _pdan pengecualian yang muncul dengan instruksi pertama fungsi.

Karakteristik:

  • Parameter sebelum _p=__named_only_startditerima secara posisional (atau dengan nama).
  • Parameter setelah _p=__named_only_startharus diberikan hanya dengan nama (kecuali jika pengetahuan tentang objek sentinel khusus __named_only_startdiperoleh dan digunakan).

Kelebihan:

  • Parameternya eksplisit dalam jumlah dan artinya (nanti jika nama baik juga dipilih, tentunya).
  • Jika sentinel ditentukan sebagai parameter pertama, maka semua argumen harus ditentukan dengan nama.
  • Saat memanggil fungsi, dimungkinkan untuk beralih ke mode posisi dengan menggunakan objek sentinel __named_only_startdi posisi yang sesuai.
  • Sebuah kinerja yang lebih baik dari alternatif lain dapat diantisipasi.

Kekurangan:

  • Pemeriksaan terjadi selama waktu proses, bukan waktu kompilasi.
  • Penggunaan parameter tambahan (meskipun bukan argumen) dan pemeriksaan tambahan. Penurunan kinerja kecil sehubungan dengan fungsi reguler.
  • Fungsionalitas adalah hack tanpa dukungan langsung oleh bahasa (lihat catatan di bawah).
  • Saat memanggil fungsi, dimungkinkan untuk beralih ke mode posisi dengan menggunakan objek sentinel __named_only_startdi posisi yang benar. Ya, ini juga bisa dilihat sebagai seorang profesional.

Harap diingat bahwa jawaban ini hanya berlaku untuk Python 2. Python 3 menerapkan mekanisme yang didukung bahasa yang serupa, tetapi sangat elegan, yang dijelaskan dalam jawaban lain.

Saya telah menemukan bahwa ketika saya membuka pikiran dan memikirkannya, tidak ada pertanyaan atau keputusan orang lain yang tampak bodoh, bodoh, atau konyol. Justru sebaliknya: Saya biasanya belajar banyak.

Mario Rossi
sumber
"Pemeriksaan terjadi selama run-time, bukan waktu kompilasi." - Saya pikir itu benar untuk semua pemeriksaan argumen fungsi. Sampai Anda benar-benar mengeksekusi baris pemanggilan fungsi, Anda tidak selalu tahu fungsi mana yang sedang dieksekusi. Juga, +1 - ini pintar.
Eric
@ Eric: Hanya saja saya lebih suka pemeriksaan statis. Tapi Anda benar: itu sama sekali bukan Python. Meskipun bukan poin yang menentukan, konstruksi "*" Python 3 juga diperiksa secara dinamis. Terima kasih atas komentar Anda.
Mario Rossi
Juga, jika Anda memberi nama variabel modul _named_only_start, menjadi tidak mungkin untuk merujuknya dari modul eksternal, yang mengeluarkan pro dan kontra. (garis bawah utama tunggal pada cakupan modul bersifat pribadi, IIRC)
Eric
Mengenai penamaan sentinel, kami juga dapat memiliki a __named_only_startdan a named_only_start(tanpa garis bawah awal), yang kedua untuk menunjukkan bahwa mode bernama "direkomendasikan", tetapi tidak untuk tingkat "dipromosikan secara aktif" (karena salah satunya bersifat publik dan lainnya tidak). Mengenai "privasi" yang _namesdimulai dengan garis bawah, hal itu tidak terlalu ditegakkan oleh bahasa: hal ini dapat dengan mudah dielakkan dengan penggunaan impor (non- *) tertentu atau nama yang memenuhi syarat. Inilah sebabnya mengapa beberapa dokumen Python lebih suka menggunakan istilah "non-publik" daripada "pribadi".
Mario Rossi
6

Anda dapat melakukannya dengan cara yang bekerja pada Python 2 dan Python 3 , dengan membuat argumen kata kunci pertama yang "palsu" dengan nilai default yang tidak akan muncul "secara alami". Argumen kata kunci tersebut dapat diawali dengan satu atau beberapa argumen tanpa nilai:

_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")

Ini akan memungkinkan:

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

tapi tidak:

info(odbchelper, 12)                

Jika Anda mengubah fungsinya menjadi:

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

maka semua argumen harus memiliki kata kunci dan info(odbchelper)tidak akan berfungsi lagi.

Ini akan memungkinkan Anda untuk menempatkan argumen kata kunci tambahan di sembarang tempat _kw, tanpa memaksa Anda untuk meletakkannya setelah entri terakhir. Ini sering kali masuk akal, misalnya mengelompokkan sesuatu secara logis atau menyusun kata kunci menurut abjad dapat membantu pemeliharaan dan pengembangan.

Jadi tidak perlu kembali menggunakan def(**kwargs)dan kehilangan informasi tanda tangan di editor pintar Anda. Kontrak sosial Anda adalah memberikan informasi tertentu, dengan memaksa (beberapa di antaranya) meminta kata kunci, urutan penyajiannya, menjadi tidak relevan.

Anthon
sumber
2

Memperbarui:

Saya menyadari bahwa menggunakan **kwargstidak akan menyelesaikan masalah. Jika programmer Anda mengubah argumen fungsi sesuai keinginan, seseorang dapat, misalnya, mengubah fungsinya menjadi ini:

def info(foo, **kwargs):

dan kode lama akan rusak lagi (karena sekarang setiap pemanggilan fungsi harus menyertakan argumen pertama).

Itu benar-benar tergantung pada apa yang dikatakan Bryan.


(...) orang mungkin menambahkan parameter antara spacingdan collapse(...)

Secara umum, saat mengubah fungsi, argumen baru harus selalu berakhir. Jika tidak, itu akan merusak kode. Harus jelas.
Jika seseorang mengubah fungsi sehingga kode rusak, perubahan ini harus ditolak.
(Seperti yang dikatakan Bryan, ini seperti kontrak)

(...) terkadang tidak selalu jelas tentang apa yang perlu dimasukkan.

Dengan melihat tanda tangan dari fungsi (yaitu def info(object, spacing=10, collapse=1)), seseorang harus segera melihat bahwa setiap argumen yang tidak memiliki nilai default, adalah wajib.
Untuk apa argumennya, harus masuk ke docstring.


Jawaban lama (disimpan untuk kelengkapan) :

Ini mungkin bukan solusi yang baik:

Anda dapat mendefinisikan fungsi dengan cara ini:

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

kwargsadalah kamus yang berisi argumen kata kunci. Anda dapat memeriksa apakah ada argumen wajib dan jika tidak, buat pengecualian.

Sisi negatifnya adalah, itu mungkin tidak terlalu jelas lagi, argumen mana yang mungkin, tetapi dengan docstring yang tepat, itu akan baik-baik saja.

Felix Kling
sumber
3
Saya lebih menyukai jawaban lama Anda. Beri komentar mengapa Anda hanya menerima ** kwargs dalam fungsi tersebut. Bagaimanapun, siapa pun dapat mengubah apa pun dalam kode sumber - Anda memerlukan dokumentasi untuk menjelaskan maksud dan tujuan di balik keputusan Anda.
Brandon
Tidak ada jawaban sebenarnya dalam jawaban ini!
Phil
2

Argumen hanya kata kunci python3 ( *) dapat disimulasikan di python2.x dengan**kwargs

Perhatikan kode python3 berikut:

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

dan perilakunya:

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

Ini dapat disimulasikan dengan menggunakan yang berikut, perhatikan bahwa saya telah mengambil kebebasan untuk beralih TypeErrorke KeyErrordalam kasus "argumen bernama yang diperlukan", tidak akan terlalu sulit untuk membuat jenis pengecualian yang sama itu juga

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)

Dan perilaku:

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

Resepnya bekerja dengan baik di python3.x, tetapi harus dihindari jika Anda hanya python3.x

Anthony Sottile
sumber
Ah, jadi kwargs.pop('foo')apakah idiom Python 2? Saya perlu memperbarui gaya pengkodean saya. Saya masih menggunakan pendekatan ini di Python 3 🤔
Neil
0

Anda dapat mendeklarasikan fungsi Anda sebagai **argshanya menerima . Itu akan mengamanatkan argumen kata kunci tetapi Anda memiliki beberapa pekerjaan tambahan untuk memastikan hanya nama yang valid yang diteruskan.

def foo(**args):
   print args

foo(1,2) # Raises TypeError: foo() takes exactly 0 arguments (2 given)
foo(hello = 1, goodbye = 2) # Works fine.
Noufal Ibrahim
sumber
1
Anda tidak hanya harus menambahkan pemeriksaan kata kunci, tetapi memikirkan tentang konsumen yang tahu bahwa mereka harus memanggil metode dengan tanda tangan foo(**kwargs). Apa yang saya lolos ke itu? foo(killme=True, when="rightnowplease")
Dagrooms
Itu benar-benar tergantung. Pertimbangkan dict.
Noufal Ibrahim
0

Seperti jawaban lain mengatakan, mengubah tanda tangan fungsi adalah ide yang buruk. Tambahkan parameter baru di akhir, atau perbaiki setiap pemanggil jika argumen dimasukkan.

Jika Anda masih ingin melakukannya, gunakan fungsi dekorator dan fungsi inspect.getargspec . Ini akan digunakan seperti ini:

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

Implementasi require_named_argsdibiarkan sebagai latihan bagi pembaca.

Saya tidak akan mengganggu. Ini akan menjadi lambat setiap kali fungsi dipanggil, dan Anda akan mendapatkan hasil yang lebih baik dengan menulis kode dengan lebih hati-hati.

Daniel Newby
sumber
-1

Anda bisa menggunakan **operator:

def info(**kwargs):

dengan cara ini orang-orang dipaksa untuk menggunakan parameter bernama.

Olivier Verdier
sumber
2
Dan tidak tahu bagaimana memanggil metode Anda tanpa membaca kode Anda, meningkatkan beban kognitif pada konsumen Anda :(
Dagrooms
Karena alasan yang disebutkan, ini adalah praktik yang sangat buruk dan harus dihindari.
David S.
-1
def cheeseshop(kind, *arguments, **keywords):

di python jika menggunakan * args itu berarti Anda dapat mengirimkan n-jumlah argumen untuk parameter ini - yang akan menjadi daftar di dalam fungsi untuk mengakses

dan jika menggunakan ** kw itu berarti argumen kata kuncinya, yang dapat diakses sebagai dict - Anda dapat memberikan n-number dari kw args, dan jika Anda ingin membatasi pengguna tersebut harus memasukkan urutan dan argumen secara berurutan maka jangan gunakan * dan ** - (cara pythonic untuk memberikan solusi umum untuk arsitektur besar ...)

jika Anda ingin membatasi fungsi Anda dengan nilai default maka Anda dapat memeriksa di dalamnya

def info(object, spacing, collapse)
  spacing = spacing or 10
  collapse = collapse or 1
shahjapan
sumber
apa yang terjadi jika jarak diinginkan menjadi 0? (jawaban, Anda mendapatkan 10). Jawaban ini sama salahnya dengan semua jawaban ** kwargs lainnya untuk semua alasan yang sama.
Phil
-2

Saya tidak mengerti mengapa seorang programmer akan menambahkan parameter di antara dua lainnya di tempat pertama.

Jika Anda ingin parameter fungsi digunakan dengan nama (mis. info(spacing=15, object=odbchelper)) Maka tidak masalah dalam urutan apa parameter tersebut didefinisikan, jadi sebaiknya Anda meletakkan parameter baru di bagian akhir.

Jika Anda benar-benar ingin urutan penting maka tidak dapat mengharapkan apa pun untuk bekerja jika Anda mengubahnya!

Umang
sumber
2
Ini tidak menjawab pertanyaan itu. Apakah itu ide yang bagus tidak relevan - seseorang mungkin akan melakukannya.
Graeme Perrow
1
Seperti yang dikatakan Graeme, seseorang akan tetap melakukannya. Selain itu, jika Anda menulis pustaka untuk digunakan oleh orang lain, memaksa (hanya python 3) meneruskan argumen hanya kata kunci memungkinkan fleksibilitas ekstra ketika Anda harus memfaktor ulang API Anda.
s0undt3ch