Bagaimana memeriksa apakah parameter fungsi opsional diatur

92

Apakah ada cara mudah dengan Python untuk memeriksa apakah nilai parameter opsional berasal dari nilai defaultnya, atau karena pengguna telah menyetelnya secara eksplisit pada pemanggilan fungsi?

Matthias
sumber
12
Karena saya ingin memeriksanya di fungsi itu tentunya :)
Matthias
2
Cukup gunakan Nonesebagai default dan periksa itu. Jika Anda benar-benar dapat menyiapkan pengujian ini, Anda juga akan mengecualikan kemungkinan apa pun bagi pengguna untuk secara eksplisit meneruskan nilai yang memanggil perilaku default.
Michael J. Barber
3
Itu bisa dilakukan dengan cara yang jauh lebih dapat digunakan kembali dan indah daripada dalam jawaban yang Anda terima, setidaknya untuk CPython. Lihat jawaban saya di bawah.
Ellioh
2
@Volatility: penting jika Anda memiliki dua set default. Pertimbangkan kelas rekursif: Class My(): def __init__(self, _p=None, a=True, b=True, c=False) Pengguna menyebutnya dengan x=My(b=False). Metode kelas dapat memanggil dirinya sendiri dengan x=My(_p=self, c=True)jika fungsi dapat mendeteksi bahwa b tidak disetel secara eksplisit dan variabel yang tidak disetel akan diturunkan dari tingkat atas. Tetapi jika mereka tidak bisa, panggilan rekursif harus melewati setiap variabel secara eksplisit: x=My(a=self.a, b=self.b, c=True, d=self.d, ...).
Dave
@ Dave tapi apakah itu pertanyaannya? Dalam pemahaman saya, pertanyaannya adalah menanyakan bagaimana membedakan x=My()dan x=My(a=True). Skenario Anda melibatkan penetapan parameter opsional nilai selain nilai defaultnya.
Volatilitas

Jawaban:

16

Banyak jawaban memiliki potongan-potongan kecil dari info lengkap, jadi saya ingin menggabungkan semuanya dengan pola favorit saya.

nilai default adalah mutabletipe

Jika nilai default adalah objek yang bisa berubah, Anda beruntung: Anda dapat mengeksploitasi fakta bahwa argumen default Python dievaluasi satu kali ketika fungsi didefinisikan (lebih banyak lagi tentang ini di akhir jawaban di bagian terakhir)

Ini berarti Anda dapat dengan mudah membandingkan nilai default yang dapat berubah menggunakan isuntuk melihat apakah nilai itu diteruskan sebagai argumen atau dibiarkan secara default, seperti dalam contoh berikut sebagai fungsi atau metode:

def f(value={}):
    if value is f.__defaults__[0]:
        print('default')
    else:
        print('passed in the call')

dan

class A:
    def f(self, value={}):
        if value is self.f.__defaults__[0]:
            print('default')
        else:
            print('passed in the call')

Argumen default yang tidak dapat diubah

Sekarang, agak kurang elegan jika default Anda diharapkan menjadi immutablenilai (dan ingat bahwa bahkan string tidak dapat diubah!) Karena Anda tidak dapat mengeksploitasi trik apa adanya, tetapi masih ada sesuatu yang dapat Anda lakukan, masih mengeksploitasi mutable Tipe; pada dasarnya Anda menempatkan default "palsu" yang bisa berubah di tanda tangan fungsi, dan nilai default "nyata" yang diinginkan di badan fungsi.

def f(value={}):
    """
    my function
    :param value: value for my function; default is 1
    """
    if value is f.__defaults__[0]:
        print('default')
        value = 1
    else:
        print('passed in the call')
    # whatever I want to do with the value
    print(value)

Rasanya sangat lucu jika Anda benar-benar default None, tetapi Nonetetap jadi ... Anda masih perlu secara eksplisit menggunakan mutable sebagai parameter default fungsi, dan beralih ke None dalam kode.

Menggunakan Defaultkelas untuk default yang tidak dapat diubah

atau, mirip dengan saran @cz, jika dokumen python tidak cukup :-), Anda dapat menambahkan objek di antaranya untuk membuat API lebih eksplisit (tanpa membaca dokumen); instance kelas used_proxy_ Default bisa berubah, dan akan berisi nilai default sebenarnya yang ingin Anda gunakan.

class Default:
    def __repr__(self):
        return "Default Value: {} ({})".format(self.value, type(self.value))

    def __init__(self, value):
        self.value = value

def f(default=Default(1)):
    if default is f.__defaults__[0]:
        print('default')
        print(default)
        default = default.value
    else:
        print('passed in the call')
    print("argument is: {}".format(default))

sekarang:

>>> f()
default
Default Value: 1 (<class 'int'>)
argument is: 1

>>> f(2)
passed in the call
argument is: 2

Di atas bekerja dengan baik juga untuk Default(None).

Pola lainnya

Jelas sekali pola di atas terlihat lebih jelek dari yang seharusnya karena semua printyang ada hanya untuk menunjukkan bagaimana mereka bekerja. Kalau tidak, saya menganggapnya singkat dan cukup berulang.

Anda dapat menulis dekorator untuk menambahkan __call__pola yang disarankan oleh @dmg dengan cara yang lebih efisien, tetapi ini tetap mengharuskan penggunaan trik aneh dalam definisi fungsi itu sendiri - Anda perlu membagi valuedan value_defaultjika kode Anda perlu membedakannya, jadi Saya tidak melihat banyak keuntungan dan saya tidak akan menulis contohnya :-)

Jenis yang dapat berubah sebagai nilai default dengan Python

Sedikit lebih banyak tentang # 1 python gotcha! , disalahgunakan untuk kesenangan Anda sendiri di atas. Anda dapat melihat apa yang terjadi karena evaluasi pada definisi dengan melakukan:

def testme(default=[]):
    print(id(default))

Anda dapat menjalankan testme()sebanyak yang Anda inginkan, Anda akan selalu melihat referensi ke instance default yang sama (jadi pada dasarnya default Anda tidak dapat diubah :-)).

Ingat bahwa dalam Python hanya ada 3 bisa berubah built-in jenis : set, list, dict; yang lainnya - bahkan string! - tidak bisa diubah.

Stefano
sumber
Contoh yang Anda miliki di "Argumen default yang tidak dapat diubah" sebenarnya tidak memiliki argumen default yang tidak dapat diubah. Jika ya, itu tidak akan berhasil.
Karol
@Karol, peduli rumit? Nilai default dalam contoh itu adalah 1, yang seharusnya tidak berubah ...
Stefano
Saya melihat tanda tangan fungsinya sebagai def f(value={}).
Karol
@Karol itu tandatangannya, tapi nilai default yang diinginkan adalah 1; maaf jika itu tidak jelas dalam penjelasannya, tetapi inti dari bagian tanggapan itu adalah agar dapat memiliki default yang tidak dapat diubah ( 1). Jika Anda memeriksa contoh, Anda akan melihatnya mengatakan print('default'); value = 1value={}
Stefano
1
Ha, saya mengerti sekarang, terima kasih. Ini tidak mudah untuk diikuti kecuali seseorang membaca teks Anda dengan sangat hati-hati yang mungkin tidak sering terjadi di SO. Pertimbangkan untuk merekam ulang.
Karol
56

Tidak juga. Cara standar adalah dengan menggunakan nilai default yang tidak diharapkan oleh pengguna untuk dilewati, misalnya sebuah objectcontoh:

DEFAULT = object()
def foo(param=DEFAULT):
    if param is DEFAULT:
        ...

Biasanya Anda hanya dapat menggunakan Nonesebagai nilai default, jika tidak masuk akal sebagai nilai yang ingin diteruskan oleh pengguna.

Alternatifnya adalah dengan menggunakan kwargs:

def foo(**kwargs):
    if 'param' in kwargs:
        param = kwargs['param']
    else:
        ...

Namun ini terlalu bertele-tele dan membuat fungsi Anda lebih sulit digunakan karena dokumentasinya tidak akan secara otomatis menyertakan paramparameter.

ecatmur.dll
sumber
9
Saya juga telah melihat beberapa orang menggunakan Ellipsis builtin untuk tempat-tempat di mana ini diperlukan dan Tidak Ada yang dianggap masukan yang valid. Ini pada dasarnya sama dengan contoh pertama.
GrandOpener
Jika Anda ingin mengimplementasikan perilaku khusus jika Tidak Ada yang diteruskan, tetapi masih membutuhkan cara untuk menguji apakah argumen diberikan oleh pengguna, Anda dapat menggunakan Ellipsissingleton sebagai default, yang secara eksplisit dirancang untuk digunakan sebagai pengabaian nilai ini . ...adalah alias untuk Ellipsis, jadi pengguna yang ingin menggunakan argumen posisi dapat memanggil your_function(p1, ..., p3)yang membuatnya jelas dan bagus untuk dibaca.
Bachsau
However this is overly verbose and makes your function more difficult to use as its documentation will not automatically include the param parameter.Ini sebenarnya tidak benar, karena Anda dapat mengatur deskripsi fungsi dan parameternya menggunakan inspectmodul. Itu tergantung pada IDE Anda apakah itu akan berfungsi atau tidak.
EZLearner
15

Penghias fungsi berikut explicit_checker,, membuat satu set nama parameter dari semua parameter yang diberikan secara eksplisit. Ini menambahkan hasil sebagai parameter tambahan ( explicit_params) ke fungsi. Lakukan saja 'a' in explicit_paramsuntuk memeriksa apakah parameter adiberikan secara eksplisit.

def explicit_checker(f):
    varnames = f.func_code.co_varnames
    def wrapper(*a, **kw):
        kw['explicit_params'] = set(list(varnames[:len(a)]) + kw.keys())
        return f(*a, **kw)
    return wrapper

@explicit_checker
def my_function(a, b=0, c=1, explicit_params=None):
    print a, b, c, explicit_params
    if 'b' in explicit_params:
        pass # Do whatever you want


my_function(1)
my_function(1, 0)
my_function(1, c=1)
Ellioh
sumber
Kode ini hanya berfungsi di python2. Untuk python 3, lihat jawaban saya di bawah ini: stackoverflow.com/questions/14749328/…
R. Yang
1
Ini cukup keren, tetapi lebih baik untuk menghindari masalah dengan desain yang lebih baik, jika memungkinkan.
Karol
@Karol, saya setuju. Dalam kebanyakan kasus orang tidak perlu itu jika desainnya masuk akal.
Ellioh
4

Saya terkadang menggunakan string unik universal (seperti UUID).

import uuid
DEFAULT = uuid.uuid4()
def foo(arg=DEFAULT):
  if arg is DEFAULT:
    # it was not passed in
  else:
    # it was passed in

Dengan cara ini, tidak ada pengguna yang bisa menebak default jika mereka mencoba sehingga saya bisa sangat yakin bahwa ketika saya melihat nilai argitu, itu tidak diteruskan.

Jesse B Miller
sumber
4
Objek Python adalah referensi, Anda bisa menggunakan object()sebagai gantinya uuid4()- ini masih contoh unik , itulah yang isdiperiksa
cz
3

Aku pernah melihat pola ini beberapa kali (misalnya perpustakaan unittest, py-flags, jinja):

class Default:
    def __repr__( self ):
        return "DEFAULT"

DEFAULT = Default()

... atau satu baris yang setara ...:

DEFAULT = type( 'Default', (), { '__repr__': lambda x: 'DEFAULT' } )()

Tidak seperti DEFAULT = object(), ini membantu pengecekan tipe dan memberikan informasi ketika terjadi kesalahan - seringkali representasi string ( "DEFAULT") atau nama kelas ( "Default") digunakan dalam pesan kesalahan.

cz
sumber
3

@ Jawaban Ellioh berfungsi di python 2. Di python 3, kode berikut harus berfungsi:

import inspect
def explicit_checker(f):
    varnames = inspect.getfullargspec(f)[0]
    def wrapper(*a, **kw):
        kw['explicit_params'] = set(list(varnames[:len(a)]) + list(kw.keys()))
        return f(*a, **kw)
    return wrapper

@explicit_checker
def my_function(a, b=0, c=1, explicit_params=None):
    print a, b, c, explicit_params
    if 'b' in explicit_params:
        pass # Do whatever you want

Metode ini dapat menyimpan nama argumen dan nilai default (bukan ** kwargs) dengan keterbacaan yang lebih baik.

R. Yang
sumber
3

Anda dapat memeriksanya dari foo.__defaults__danfoo.__kwdefaults__

lihat contoh sederhana di bawah ini

def foo(a, b, c=123, d=456, *, e=789, f=100):
    print(foo.__defaults__)
    # (123, 456) 
    print(foo.__kwdefaults__)
    # {'e': 789, 'f': 100}
    print(a, b, c, d, e, f)

#and these variables are also accessible out of function body
print(foo.__defaults__)    
# (123, 456)  
print(foo.__kwdefaults__)  
# {'e': 789, 'f': 100}

foo.__kwdefaults__['e'] = 100500

foo(1, 2) 
#(123, 456)
#{'f': 100, 'e': 100500}
#1 2 123 456 100500 100

kemudian dengan menggunakan operator =dan isAnda dapat membandingkannya

dan untuk beberapa kasus kode di bawah ini sudah cukup

Misalnya, Anda perlu menghindari perubahan nilai default, maka Anda dapat memeriksa kesetaraan dan menyalinnya jika demikian

    def update_and_show(data=Example):
        if data is Example:
            data = copy.deepcopy(data)
        update_inplace(data) #some operation
        print(data)

Juga, sangat nyaman untuk digunakan getcallargsdari inspectseperti mengembalikan argumen nyata dengan yang fungsi akan dipanggil. Anda meneruskan fungsi dan args dan kwargs ke it ( inspect.getcallargs(func, /, *args, **kwds)), itu akan mengembalikan argumen metode nyata yang digunakan untuk pemanggilan, dengan mempertimbangkan nilai default dan hal-hal lainnya. Lihat contoh di bawah ini.

from inspect import getcallargs

# we have a function with such signature
def show_params(first, second, third=3):
    pass

# if you wanted to invoke it with such params (you could get them from a decorator as example)
args = [1, 2, 5]
kwargs = {}
print(getcallargs(show_params, *args, **kwargs))
#{'first': 1, 'second': 2, 'third': 5}

# here we didn't specify value for d
args = [1, 2, 3, 4]
kwargs = {}

# ----------------------------------------------------------
# but d has default value =7
def show_params1(first, *second, d = 7):
    pass


print(getcallargs(show_params1, *args, **kwargs))
# it will consider b to be equal to default value 7 as it is in real method invocation
# {'first': 1, 'second': (2, 3, 4), 'd': 7}

# ----------------------------------------------------------
args = [1]
kwargs = {"d": 4}

def show_params2(first, d=3):
    pass


print(getcallargs(show_params2, *args, **kwargs))
#{'first': 1, 'd': 4}

https://docs.python.org/3/library/inspect.html

Alex
sumber
2

Saya setuju dengan komentar Volatility. Tetapi Anda dapat memeriksanya dengan cara berikut:

def function(arg1,...,**optional):
    if 'optional_arg' in optional:
        # user has set 'optional_arg'
    else:
        # user has not set 'optional_arg'
        optional['optional_arg'] = optional_arg_default_value # set default
isedev
sumber
Saya percaya parameter opsional adalah sesuatu seperti def func(optional=value)tidak**kwargs
Zaur Nasibov
Itu sesuatu yang agak terbuka untuk interpretasi. Apa perbedaan sebenarnya antara argumen dengan nilai default dan argumen kata kunci? Keduanya diekspresikan menggunakan sintaks yang sama "kata kunci = nilai".
isedev
Saya tidak setuju, karena tujuan parameter opsional dan **kwargssedikit berbeda. PS tidak masalah tentang -1 :) Dan -1 saya untuk Anda tidak disengaja :)
Zaur Nasibov
2

Ini adalah variasi dari jawaban Stefano, tetapi menurut saya sedikit lebih mudah dibaca:

not_specified = {}

def foo(x=not_specified):
    if x is not_specified:
            print("not specified")
    else:
            print("specified")
lazieburd
sumber
Satu suara positif ?? Saya paling suka ini. Sederhana, tanpa refleksi. Dapat dibaca.
vincent
1

Pendekatan yang sedikit aneh adalah:

class CheckerFunction(object):
    def __init__(self, function, **defaults):
        self.function = function
        self.defaults = defaults

    def __call__(self, **kwargs):
        for key in self.defaults:
            if(key in kwargs):
                if(kwargs[key] == self.defaults[key]):
                    print 'passed default'
                else:
                    print 'passed different'
            else:
                print 'not passed'
                kwargs[key] = self.defaults[key]

        return self.function(**kwargs)

def f(a):
    print a

check_f = CheckerFunction(f, a='z')
check_f(a='z')
check_f(a='b')
check_f()

Output mana:

passed default
z
passed different
b
not passed
z

Sekarang ini, seperti yang saya sebutkan, cukup aneh, tapi berhasil. Namun hal ini cukup dibaca dan juga untuk ecatmur 's saran tidak akan secara otomatis didokumentasikan.

dmg
sumber
1
Anda mungkin ingin memasukkan perilaku check_f('z'), yang juga, seperti yang Anda katakan, aneh.
Michael J. Barber
@ MichaelJ.Bagus poin. Anda juga harus melakukan "sihir" dengan * args. Namun, maksud saya adalah bahwa itu mungkin, tetapi perlu sekarang apakah nilai default dilewatkan atau tidak adalah desain yang buruk.
dmg