Wymuszone nazewnictwo parametrów w Pythonie

111

W Pythonie możesz mieć definicję funkcji:

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

które można wywołać w dowolny z następujących sposobów:

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

dzięki zezwoleniu Pythona na argumenty dowolnej kolejności, o ile są nazwane.

Problem polega na tym, że gdy niektóre z naszych większych funkcji rosną, ludzie mogą dodawać parametry między spacingi collapse, co oznacza, że ​​niewłaściwe wartości mogą trafiać do parametrów, które nie są nazwane. Ponadto czasami nie zawsze jest jasne, co należy wprowadzić. Szukamy sposobu, aby zmusić ludzi do nazywania określonych parametrów - nie tylko standardu kodowania, ale idealnie flagi lub wtyczki pydev?

tak więc w powyższych 4 przykładach tylko ostatni przejdzie kontrolę, ponieważ wszystkie parametry są nazwane.

Możliwe, że włączymy go tylko dla niektórych funkcji, ale wszelkie sugestie, jak to zaimplementować - lub jeśli to w ogóle możliwe, będą mile widziane.

Mark Mayo
źródło

Odpowiedzi:

214

W Pythonie 3 - tak, możesz określić * na liście argumentów.

Z dokumentów :

Parametry występujące po „*” lub „* identyfikatorze” są parametrami zawierającymi tylko słowa kluczowe i mogą być do nich przekazywane tylko używane argumenty słów kluczowych.

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

Można to również połączyć z **kwargs:

def foo(pos, *, forcenamed, **kwargs):
Eli Bendersky
źródło
32

Możesz zmusić ludzi do używania argumentów słów kluczowych w Pythonie3, definiując funkcję w następujący sposób.

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

Sprawiając, że pierwszy argument jest argumentem pozycyjnym bez nazwy, zmuszasz każdego, kto wywołuje funkcję, do używania argumentów słów kluczowych, o co myślę, że pytałeś. W Pythonie2 jedynym sposobem na to jest zdefiniowanie takiej funkcji

def foo(**kwargs):
    pass

To zmusi wywołującego do używania kwargs, ale nie jest to takie świetne rozwiązanie, ponieważ wtedy musiałbyś umieścić czek, aby zaakceptować tylko potrzebny argument.

martega
źródło
11

To prawda, większość języków programowania sprawia, że ​​kolejność parametrów jest częścią kontraktu wywołania funkcji, ale nie musi tak być. Dlaczego miałoby to robić? Rozumiem zatem, czy Python różni się pod tym względem od innych języków programowania. Oprócz innych dobrych odpowiedzi dotyczących języka Python 2, weź pod uwagę następujące kwestie:

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

Jedynym sposobem, w jaki wywołujący mógłby podać argumenty spacingi collapsepozycjonować (bez wyjątku), byłoby:

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

Konwencja nie używania prywatnych elementów należących do innych modułów jest już bardzo podstawowa w Pythonie. Podobnie jak w przypadku samego Pythona, ta konwencja parametrów byłaby tylko częściowo wymuszona.

W przeciwnym razie wezwania musiałyby mieć postać:

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

Wezwanie

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

przypisze parametrowi wartość 11 _p i wyjątek podniesiony przez pierwszą instrukcję funkcji.

Charakterystyka:

  • Parametry przed _p=__named_only_start są dopuszczane pozycyjnie (lub według nazwy).
  • Parametry po _p=__named_only_startmuszą być podane tylko z nazwy (chyba że __named_only_startuzyskano i wykorzystano wiedzę o specjalnym obiekcie wartowniczym ).

Plusy:

  • Parametry są jednoznaczne co do liczby i znaczenia (oczywiście później, jeśli wybrano również dobre nazwy).
  • Jeśli wartownik jest określony jako pierwszy parametr, wszystkie argumenty muszą być określone według nazwy.
  • Podczas wywoływania funkcji można przełączyć się do trybu pozycyjnego przy użyciu obiektu wartownika __named_only_start w odpowiedniej pozycji.
  • Można oczekiwać lepszej wydajności niż inne alternatywy.

Cons:

  • Sprawdzanie odbywa się w czasie wykonywania, a nie w czasie kompilacji.
  • Użycie dodatkowego parametru (choć nie argumentu) i dodatkowej kontroli. Niewielkie pogorszenie wydajności w stosunku do zwykłych funkcji.
  • Funkcjonalność to hack bez bezpośredniego wsparcia ze strony języka (patrz uwaga poniżej).
  • Wywołując funkcję, można przejść do trybu pozycyjnego, używając obiektu wartownika __named_only_startw odpowiedniej pozycji. Tak, to również może być postrzegane jako profesjonalista.

Pamiętaj, że ta odpowiedź dotyczy tylko Pythona 2. Python 3 implementuje podobny, ale bardzo elegancki, obsługiwany przez język mechanizm opisany w innych odpowiedziach.

Odkryłem, że kiedy otwieram umysł i myślę o tym, żadne pytanie ani decyzja innej osoby nie wydaje się głupia, głupia lub po prostu głupia. Wręcz przeciwnie: zazwyczaj dużo się uczę.

Mario Rossi
źródło
„Sprawdzanie odbywa się w czasie wykonywania, a nie w czasie kompilacji”. - Myślę, że tak jest w przypadku sprawdzania wszystkich argumentów funkcji. Dopóki faktycznie nie wykonasz wiersza wywołania funkcji, nie zawsze wiesz, która funkcja jest wykonywana. Ponadto +1 - to jest sprytne.
Eric
@Eric: Po prostu wolałbym sprawdzanie statyczne. Ale masz rację: to wcale nie byłby Python. Chociaż nie jest to decydująca kwestia, konstrukcja „*” w Pythonie 3 jest również sprawdzana dynamicznie. Dzięki za komentarz.
Mario Rossi
Ponadto, jeśli nazwiesz zmienną modułu _named_only_start, niemożliwe będzie odwołanie się do niej z modułu zewnętrznego, który usuwa zalety i wady. (pojedyncze wiodące podkreślenia w zakresie modułu są prywatne, IIRC)
Eric
Jeśli chodzi o nazewnictwo wartownika, moglibyśmy również mieć zarówno a, jak __named_only_starti a named_only_start(bez początkowego podkreślenia), drugie oznaczające, że nazwany tryb jest „zalecany”, ale nie do poziomu „aktywnego promowania” (ponieważ jeden jest publiczny, a inne nie). Jeśli chodzi o „prywatność” _namesrozpoczynania od podkreślenia, nie jest ona zbyt silnie wymuszana przez język: można ją łatwo obejść, używając określonych (innych niż *) importu lub nazw kwalifikowanych. Z tego powodu kilka dokumentów Pythona woli używać terminu „niepubliczny” zamiast „prywatny”.
Mario Rossi
6

Możesz to zrobić w sposób, który działa zarówno w Pythonie 2, jak i Pythonie 3 , tworząc „fałszywy” argument pierwszego słowa kluczowego z wartością domyślną, która nie pojawi się „naturalnie”. Ten argument słowa kluczowego może być poprzedzony jednym lub większą liczbą argumentów bez wartości:

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

Pozwoli to na:

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

ale nie:

info(odbchelper, 12)                

Jeśli zmienisz funkcję na:

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

wtedy wszystkie argumenty muszą mieć słowa kluczowe i info(odbchelper)nie będą już działać.

Umożliwi to umieszczenie dodatkowych argumentów słów kluczowych w dowolnym miejscu po _kw, bez konieczności umieszczania ich po ostatnim wpisie. Często ma to sens, np. Logiczne grupowanie rzeczy lub alfabetyczne układanie słów kluczowych może pomóc w utrzymaniu i rozwoju.

Nie ma więc potrzeby powracania do używania def(**kwargs)i utraty informacji o podpisie w inteligentnym edytorze. Twoja umowa społeczna ma na celu dostarczenie pewnych informacji, zmuszając (niektóre z nich) do wymagania słów kluczowych, kolejność, w jakiej są one przedstawiane, stała się nieistotna.

Anthon
źródło
2

Aktualizacja:

Zdałem sobie sprawę, że użycie **kwargsnie rozwiąże problemu. Jeśli twoi programiści zmieniają argumenty funkcji według własnego uznania, można na przykład zmienić funkcję na następującą:

def info(foo, **kwargs):

a stary kod znowu by się zepsuł (ponieważ teraz każde wywołanie funkcji musi zawierać pierwszy argument).

To naprawdę sprowadza się do tego, co mówi Bryan.


(...) ludzie mogą dodawać parametry między spacinga collapse(...)

Ogólnie przy zmianie funkcji nowe argumenty powinny zawsze kończyć się na końcu. W przeciwnym razie łamie kod. Powinno być oczywiste.
Jeśli ktoś zmieni funkcję tak, że kod się zepsuje, to ta zmiana musi zostać odrzucona.
(Jak mówi Bryan, to jest jak umowa)

(...) czasami nie zawsze jest jasne, o co chodzi.

Patrząc na podpis funkcji (tj. def info(object, spacing=10, collapse=1)) Od razu widać, że każdy argument, który nie ma wartości domyślnej, jest obowiązkowy.
Po co ten argument powinien znaleźć się w dokumentacji.


Stara odpowiedź (zachowana dla kompletności) :

To prawdopodobnie nie jest dobre rozwiązanie:

Możesz zdefiniować funkcje w ten sposób:

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

kwargsto słownik zawierający dowolny argument ze słowa kluczowego. Możesz sprawdzić, czy obecny jest obowiązkowy argument, a jeśli nie, zgłosić wyjątek.

Wadą jest to, że może nie być już tak oczywiste, które argumenty są możliwe, ale przy odpowiednim dokumencie powinno być dobrze.

Felix Kling
źródło
3
Bardziej podobała mi się twoja stara odpowiedź. Po prostu dodaj komentarz, dlaczego akceptujesz tylko ** kwargs w funkcji. W końcu każdy może zmienić wszystko w kodzie źródłowym - potrzebujesz dokumentacji, aby opisać zamiary i cel swoich decyzji.
Brandon
W tej odpowiedzi nie ma rzeczywistej odpowiedzi!
Phil
2

Argumenty ( *) zawierające tylko słowa kluczowe python3 można symulować w python2.x za pomocą**kwargs

Rozważmy następujący kod python3:

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

i jego zachowanie:

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

To może być symulowane przy użyciu następujących, uwaga Wziąłem wolność przełączania TypeErrorsię KeyErrorw „wymagane nazwany argument” sprawy, to nie byłoby zbyt wiele pracy, aby to tego samego typu, a także wyjątków

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)

I zachowanie:

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

Przepis działa równie dobrze w python3.x, ale należy go unikać, jeśli jesteś tylko python3.x

Anthony Sottile
źródło
Ach, więc kwargs.pop('foo')jest idiomem Pythona 2? Muszę zaktualizować mój styl kodowania. Nadal stosowałem to podejście w Pythonie 3 🤔
Neil
0

Możesz zadeklarować swoje funkcje jako **argstylko odbierające . Wymagałoby to stosowania argumentów słów kluczowych, ale wymagałoby to dodatkowej pracy, aby upewnić się, że przekazywane są tylko prawidłowe nazwy.

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
źródło
1
Nie tylko musisz dodać sprawdzanie słów kluczowych, ale także pomyśleć o kliencie, który wie, że musi wywołać metodę z podpisem foo(**kwargs). Co mam w tym przekazać? foo(killme=True, when="rightnowplease")
Dagrooms
To naprawdę zależy. Rozważ dict.
Noufal Ibrahim,
0

Jak mówią inne odpowiedzi, zmiana sygnatur funkcji to zły pomysł. Albo dodaj nowe parametry na końcu, albo napraw każdego wywołującego, jeśli zostaną wstawione argumenty.

Jeśli nadal chcesz to zrobić, użyj dekoratora funkcji i funkcji inspect.getargspec . Byłoby użyte coś takiego:

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

Wykonanie require_named_argspozostawiono czytelnikowi jako ćwiczenie.

Nie zawracałbym sobie głowy. Będzie to powolne za każdym razem, gdy wywoływana jest funkcja, a dokładniejsze pisanie kodu przyniesie lepsze wyniki.

Daniel Newby
źródło
-1

Możesz użyć **operatora:

def info(**kwargs):

w ten sposób ludzie są zmuszeni do używania nazwanych parametrów.

Olivier Verdier
źródło
2
I nie mam pojęcia, jak wywołać metodę bez czytania kodu, zwiększając obciążenie poznawcze klienta :(
Dagrooms
Z tego powodu jest to naprawdę zła praktyka i należy jej unikać.
David S.
-1
def cheeseshop(kind, *arguments, **keywords):

w Pythonie, jeśli używasz * args, co oznacza, że ​​możesz przekazać n-liczbę argumentów dla tego parametru - która będzie listą wewnątrz funkcji do uzyskania dostępu

a jeśli używasz ** kw, oznacza to, że jego argumenty słów kluczowych mogą być dostępne jako dict - możesz przekazać n-liczbę argumentów kw, a jeśli chcesz ograniczyć tego użytkownika, musisz wprowadzić sekwencję i argumenty w kolejności, a następnie nie używaj * i ** - (jest to pythonowy sposób dostarczania ogólnych rozwiązań dla dużych architektur ...)

jeśli chcesz ograniczyć swoją funkcję wartościami domyślnymi, możesz sprawdzić w niej

def info(object, spacing, collapse)
  spacing = spacing or 10
  collapse = collapse or 1
shahjapan
źródło
co się stanie, jeśli odstępy mają wynosić 0? (odpowiedź, otrzymasz 10). Ta odpowiedź jest tak samo błędna, jak wszystkie inne ** kwargs, z tych samych powodów.
Phil
-2

Nie rozumiem, dlaczego programista w pierwszej kolejności dodaje parametr między dwoma innymi.

Jeśli chcesz, aby parametry funkcji były używane z nazwami (np info(spacing=15, object=odbchelper) ), Nie powinno mieć znaczenia, w jakiej kolejności są zdefiniowane, więc równie dobrze możesz umieścić nowe parametry na końcu.

Jeśli chcesz, aby kolejność miała znaczenie, nie możesz oczekiwać, że cokolwiek zadziała, jeśli je zmienisz!

Umang
źródło
2
To nie odpowiada na pytanie. Nie ma znaczenia, czy to dobry pomysł - ktoś i tak może to zrobić.
Graeme Perrow
1
Jak wspomniał Graeme, ktoś i tak to zrobi. Ponadto, jeśli piszesz bibliotekę, która ma być używana przez innych, wymuszanie (tylko w Pythonie 3) przekazywania argumentów zawierających tylko słowa kluczowe zapewnia dodatkową elastyczność, gdy musisz refaktoryzować swoje API.
s0undt3ch