Czy można wpisać podpowiedź do funkcji lambda?

93

Obecnie w Pythonie parametry funkcji i typy zwracane mogą mieć następujące wskazówki:

def func(var1: str, var2: str) -> int:
    return var1.index(var2)

Co oznacza, że ​​funkcja przyjmuje dwa ciągi i zwraca liczbę całkowitą.

Jednak ta składnia jest bardzo myląca z lambdami, które wyglądają następująco:

func = lambda var1, var2: var1.index(var2)

Próbowałem umieścić wskazówki dotyczące typów zarówno dla parametrów, jak i typów zwracanych, ale nie mogę znaleźć sposobu, który nie powoduje błędu składniowego.

Czy można wpisać podpowiedź do funkcji lambda? Jeśli nie, czy istnieją plany dotyczące wyrażeń lambda podpowiadających typ lub z jakiegoś powodu (poza oczywistym konfliktem składni), dlaczego nie?

Nathan Merrill
źródło
Myślę, że zwykły przypadek użycia lambdy jest zagnieżdżony w innej funkcji (w przeciwieństwie do zwykłych funkcji, które są zdefiniowane w jednym miejscu i nazywane gdzie indziej). Jeśli znasz już typy w tej funkcji, to (w zasadzie) powinno być dość łatwo dowiedzieć się, jakie typy otrzyma lambda. Ponadto zmodyfikowanie składni w celu obsługi adnotacji utrudniłoby odczytanie lambdy .
mgilson
Dlaczego chcesz to zrobić? Składnia lambda służy do „wyrzucania” funkcji do użycia w bardzo ograniczonych kontekstach, na przykład jako keyargument funkcji sortedwbudowanej. Naprawdę nie widzę sensu w dodawaniu wskazówek dotyczących typów w tak ograniczonych kontekstach. Ponadto użycie adnotacji zmiennych PEP-526 w celu dodania wskazówek dotyczących typu lambdacałkowicie mija się z celem, IMHO. lambdaSkładnia ma na celu zdefiniowanie anonimowych funkcje. Jaki jest sens używania lambdai natychmiastowego wiązania go ze zmienną? Po prostu użyj def!
Luciano Ramalho

Odpowiedzi:

101

W pewnym sensie w Pythonie 3.6 i nowszych można używać adnotacji zmiennych PEP 526 . Możesz dodać adnotację do zmiennej, do której przypisujesz lambdawynik, za pomocą typing.Callableogólnego :

from typing import Callable

func: Callable[[str, str], int] = lambda var1, var2: var1.index(var2)

Nie dołącza to informacji wskazujących na typ do samego obiektu funkcji, tylko do przestrzeni nazw, w której przechowywany jest obiekt, ale zwykle jest to wszystko, czego potrzebujesz do celów podpowiadania typu.

Jednak równie dobrze możesz zamiast tego po prostu użyć instrukcji funkcyjnej; jedyną zaletą lambdaoferty jest to, że można umieścić definicję funkcji dla prostego wyrażenia wewnątrz większego wyrażenia. Ale powyższa lambda nie jest częścią większego wyrażenia, jest zawsze tylko częścią instrukcji przypisania, wiążącej ją z nazwą. Dokładnie to def func(var1: str, var2: str): return var1.index(var2)osiągnie stwierdzenie.

Zwróć uwagę, że nie możesz osobno dodawać adnotacji *argsani **kwargsargumentów, ponieważ dokumentacja Callablestwierdza:

Nie ma składni wskazującej argumenty opcjonalne lub słowa kluczowe; takie typy funkcji są rzadko używane jako typy wywołań zwrotnych.

To ograniczenie nie dotyczy protokołu PEP 544 z metodą__call__ ; użyj tego, jeśli potrzebujesz wyrazistej definicji tego, jakie argumenty powinny być akceptowane. Potrzebujesz Pythona 3.8 lub zainstaluj typing-extensionsprojekt dla backport:

from typing-extensions import Protocol

class SomeCallableConvention(Protocol):
    def __call__(var1: str, var2: str, spam: str = "ham") -> int:
        ...

func: SomeCallableConvention = lambda var1, var2, spam="ham": var1.index(var2) * spam

Dla lambdaekspresji samego , nie można stosować żadnych adnotacji (składni Pythona, na którym zbudowana jest rodzaj podpowiedzi). Składnia jest dostępna tylko dla definstrukcji funkcyjnych.

Z PEP 3107 - Adnotacje funkcji :

Składnia lambda nie obsługuje adnotacji. Składnię lambda można zmienić, aby obsługiwała adnotacje, wymagając nawiasów wokół listy parametrów. Jednak zdecydowano się nie wprowadzać tej zmiany, ponieważ:

  • To byłaby niekompatybilna zmiana.
  • Lambdy i tak są zneutralizowane.
  • Lambdę można zawsze zmienić na funkcję.

Nadal możesz dołączyć adnotacje bezpośrednio do obiektu, function.__annotations__atrybut jest słownikiem z możliwością zapisu:

>>> def func(var1: str, var2: str) -> int:
...     return var1.index(var2)
...
>>> func.__annotations__
{'var1': <class 'str'>, 'return': <class 'int'>, 'var2': <class 'str'>}
>>> lfunc = lambda var1, var2: var1.index(var2)
>>> lfunc.__annotations__
{}
>>> lfunc.__annotations__['var1'] = str
>>> lfunc.__annotations__['var2'] = str
>>> lfunc.__annotations__['return'] = int
>>> lfunc.__annotations__
{'var1': <class 'str'>, 'return': <class 'int'>, 'var2': <class 'str'>}

Oczywiście nie tak dynamiczne adnotacje, jak te, pomogą ci, gdy chcesz uruchomić statyczny analizator nad wskazówkami typu.

Martijn Pieters
źródło
1
Jeśli odpowiedź brzmi, func: Callable[[str, str], int] = lambda var1, var2: var1.index(var2)dlaczego nie jest lepsza odpowiedź def func(var1: str, var2: str) -> int: return var1.index(var2)???
Guido van Rossum,
@GuidovanRossum: ponieważ to jest odpowiedź na inne pytanie. :-) Tutaj chodziło o to, jak zastosować podpowiedź do typu dla lambdy, która daje taki sam wynik jak zdefiniowanie funkcji. Mimo to dodam sekcję opisującą, dlaczego możesz nie chcieć używać lambdy.
Martijn Pieters
2
Nie, odpowiedź powinna brzmieć dosłownie: „Nie, nie jest to możliwe, nie ma planów, aby to zmienić, a powody są głównie składniowe - i dość łatwo jest zdefiniować nazwaną funkcję i dodać do niej adnotacje”. Zwracam również uwagę, że proponowane obejście (utworzenie zmiennej o określonym typie Callable) nie jest zbyt pomocne, jeśli lambda jest ukryta w dużym wywołaniu lub strukturze danych, więc nie jest w żadnym sensie „lepsza” niż zdefiniowanie funkcji. (Twierdzę, że jest gorzej, ponieważ notacja Callable jest mało czytelna.)
Guido van Rossum
Rozwiązanie rozprasza również uwagę, wyglądając tak, jakby definiowało zmienną z pewnym typem wywoływanym, której można przypisać różne lambdy w czasie jej życia. To coś innego niż definiowanie funkcji (przynajmniej mypy balks, jeśli przedefiniujesz lub zmienisz definicję, ale nie zmienną typu Callable) i dalej od oryginalnego pytania „Chcę tylko lambda z adnotacjami typu”. Nadal twierdzę, że zachowanie formy lambda zamiast formy def w tym przypadku nie przynosi korzyści. (I powiedziałbym, że nawet gdyby nie było adnotacji typu.)
Guido van Rossum
1
@GuidovanRossum: Czy mogę w takim razie zaprosić cię do napisania własnej odpowiedzi? Z twoim szczególnym statusem jako ojca języka, odpowiedź przez ciebie z łatwością przewyższyłaby tę, z czasem, i możesz napisać dokładnie to, co uważasz, że powinno w niej być.
Martijn Pieters
47

Od Pythona 3.6 możesz (zobacz PEP 526 ):

from typing import Callable
is_even: Callable[[int], bool] = lambda x: (x % 2 == 0)
sty
źródło
Nie rozumiem tej odpowiedzi: gdzie ustawiasz rodzaj x?
Stenci
3
@stenci is_evenFunkcja to a, Callablektóra oczekuje jednego intargumentu, więc x jest int.
sty
Teraz rozumiem. Na pierwszy rzut oka pomyślałem, że opisujesz tylko typ zwracany przez lambdę, a nie typ jej argumentów.
Stenci
4
to właściwie nie jest adnotacja samej lambdy: nie możesz pobrać tych adnotacji z obiektu lambda, tak jak w przypadku funkcji z adnotacjami
cz
3
mypy 0.701 z Pythonem 3.7 poprawnie sprawdza typ: to: is_even('x')powoduje błąd typu.
Konrad Rudolph
-2

Nie, nie jest to możliwe, nie ma planów zmiany tego, a przyczyny są przede wszystkim syntaktyczne.

Bart Louwers
źródło