Jakie są dobre zastosowania „Adnotacji funkcji” języka Python3

159

Adnotacje funkcji: PEP-3107

Natrafiłem na fragment kodu demonstrujący adnotacje funkcji Python3. Koncepcja jest prosta, ale nie mam pojęcia, dlaczego zostały one zaimplementowane w Pythonie3 ani w jakimkolwiek dobrym zastosowaniu. Może SO może mnie oświecić?

Jak to działa:

def foo(a: 'x', b: 5 + 6, c: list) -> max(2, 9):
    ... function body ...

Wszystko, co następuje po dwukropku po argumencie to „adnotacja”, a informacje następujące po nim ->to adnotacja dotycząca wartości zwracanej przez funkcję.

foo.func_annotations zwróci słownik:

{'a': 'x',
 'b': 11,
 'c': list,
 'return': 9}

Jakie jest znaczenie posiadania tego dostępnego?

agscala
źródło
6
@SilentGhost: niestety wiele linków z rzeczywistymi przypadkami użycia jest uszkodzonych. Czy jest miejsce, w którym zawartość mogłaby zostać przechowywana lub zniknęła na zawsze?
maksymalnie
16
nie powinno foo.func_annotations być foo.__annotations__w python3?
zhangxaochen
2
Adnotacje nie mają specjalnego znaczenia. Jedyne, co robi Python, to umieszczanie ich w słowniku adnotacji . Wszelkie inne działania należy do Ciebie.
N Randhawa,
co to def foo(a: 'x', b: 5 + 6, c: list) -> max(2, 9):znaczy
Ali SH

Odpowiedzi:

90

Myślę, że to jest naprawdę świetne.

Mając doświadczenie akademickie, mogę powiedzieć, że adnotacje okazały się nieocenione we włączaniu inteligentnych analizatorów statycznych dla języków takich jak Java. Na przykład, możesz zdefiniować semantykę, taką jak ograniczenia stanu, wątki, do których można uzyskać dostęp, ograniczenia architektury itp., I istnieje wiele narzędzi, które mogą je następnie odczytać i przetworzyć, aby zapewnić gwarancje wykraczające poza to, co otrzymujesz od kompilatorów. Możesz nawet napisać rzeczy, które sprawdzają warunki wstępne / warunki końcowe.

Wydaje mi się, że coś takiego jest szczególnie potrzebne w Pythonie z powodu słabszego pisania, ale tak naprawdę nie było konstrukcji, które uczyniłyby to prostym i częścią oficjalnej składni.

Istnieją inne zastosowania adnotacji, których nie można zagwarantować. Widzę, jak mogę zastosować moje narzędzia Java do Pythona. Na przykład mam narzędzie, które pozwala przypisać specjalne ostrzeżenia do metod i daje wskazówki, kiedy je wywołujesz, że powinieneś przeczytać ich dokumentację (np. Wyobraź sobie, że masz metodę, której nie wolno wywoływać z wartością ujemną, ale nie intuicyjnie z nazwy). Z adnotacjami mógłbym technicznie napisać coś takiego dla Pythona. Podobnie, narzędzie, które organizuje metody w dużej klasie na podstawie tagów, można napisać, jeśli istnieje oficjalna składnia.

Uri
źródło
34
ISTM są to teoretyczne korzyści, które można osiągnąć tylko wtedy, gdy standardowa biblioteka i moduły innych firm używają adnotacji funkcji i używają ich w spójnym znaczeniu oraz używają dobrze przemyślanych systemów adnotacji. Do tego dnia (który nigdy nie nadejdzie), głównymi zastosowaniami adnotacji funkcji Pythona będą jednorazowe zastosowania opisane w innych odpowiedziach. Na razie można zapomnieć o inteligentnych analizatorach statycznych, gwarancjach kompilatorów, łańcuchach narzędzi opartych na Javie itp.
Raymond Hettinger.
4
Nawet bez tego wszystkiego, używając adnotacji funkcji, nadal możesz ich używać do analizy statycznej w kodzie, który ma je na swoich danych wejściowych i wywołuje inny kod, który ma podobne adnotacje. W przypadku większego projektu lub bazy kodu może to być nadal bardzo przydatna część kodu do wykonywania analizy statycznej opartej na adnotacjach.
GPS
1
ODPOWIEDŹ, możesz to wszystko zrobić za pomocą dekoratorów, które poprzedzają adnotacje; dlatego nadal nie widzę korzyści. Mam nieco inne podejście do tego pytania: stackoverflow.com/questions/13784713/ ...
allyourcode
9
Przewijając do 2015 roku, python.org/dev/peps/pep-0484 i mypy-lang.org zaczynają udowadniać, że wszyscy mylą się.
Mauricio Scheffer
1
Jeszcze bardziej ujawnia wpływ Pythona na Swift.
uchuugaka
92

Adnotacje funkcji są tym, co z nich zrobisz.

Mogą służyć do dokumentacji:

def kinetic_energy(mass: 'in kilograms', velocity: 'in meters per second'):
     ...

Mogą być używane do sprawdzania stanu wstępnego:

def validate(func, locals):
    for var, test in func.__annotations__.items():
        value = locals[var]
        msg = 'Var: {0}\tValue: {1}\tTest: {2.__name__}'.format(var, value, test)
        assert test(value), msg


def is_int(x):
    return isinstance(x, int)

def between(lo, hi):
    def _between(x):
            return lo <= x <= hi
    return _between

def f(x: between(3, 10), y: is_int):
    validate(f, locals())
    print(x, y)


>>> f(0, 31.1)
Traceback (most recent call last):
   ... 
AssertionError: Var: y  Value: 31.1 Test: is_int

Zobacz także http://www.python.org/dev/peps/pep-0362/, aby dowiedzieć się, jak zaimplementować sprawdzanie typów.

Raymond Hettinger
źródło
18
Jak to jest lepsze niż dokumentacja do dokumentacji lub jawne sprawdzanie typu w funkcji? Wydaje się, że bez powodu komplikuje to język.
endolith
10
@endolith Z pewnością możemy obejść się bez adnotacji funkcji. Po prostu zapewniają standardowy sposób dostępu do adnotacji. To sprawia, że ​​są dostępni do pomocy () i wskazówek narzędzi oraz do introspekcji.
Raymond Hettinger
4
Zamiast przekazywać liczby, możesz tworzyć typy Massi Velocityzamiast tego.
prawy
1
aby w pełni to zademonstrować, musiałbym def kinetic_energy(mass: 'in kilograms', velocity: 'in meters per second') -> float:również pokazać typ zwrotu. To moja ulubiona odpowiedź tutaj.
Tommy
Używając swojego kodu, czy istnieje sposób na zweryfikowanie returnadnotacji? Wydaje się, że nie pojawia się wlocals
user189728
46

To dość późna odpowiedź, ale ODPOWIEDŹ, obecnie najlepszym zastosowaniem adnotacji funkcji jest PEP-0484 i MyPy .

Mypy jest opcjonalnym statycznym narzędziem do sprawdzania typów dla Pythona. Możesz dodawać wskazówki dotyczące typów do programów w języku Python, korzystając z nadchodzącego standardu adnotacji typu wprowadzonego w Pythonie 3.5 beta 1 (PEP 484) i używać mypy do statycznego sprawdzania typów.

Używane tak:

from typing import Iterator

def fib(n: int) -> Iterator[int]:
    a, b = 0, 1
    while a < n:
        yield a
        a, b = b, a + b
Dustin Wyatt
źródło
2
Więcej przykładów tutaj Moje przykłady i tutaj Jak możesz skorzystać z podpowiedzi dotyczących typów
El Ruso
Zobacz także pytype - inny analizator statyczny budowany z myślą o PEP-0484.
GPS
Niestety typ nie jest wymuszany. Jeśli piszę za list(fib('a'))pomocą Twojej przykładowej funkcji, Python 3.7 z radością przyjmuje argument i narzeka, że ​​nie ma możliwości porównania ciągu i int.
Denis de Bernardy
@DenisdeBernardy Jak wyjaśnia PEP-484, Python udostępnia tylko adnotacje typu. Aby wymusić typy, musisz użyć mypy.
Dustin Wyatt
23

Aby dodać konkretny przykład dobrego wykorzystania z mojej odpowiedzi tutaj , w połączeniu z dekoratorami, można zrobić prosty mechanizm dla wielu metod.

# This is in the 'mm' module

registry = {}
import inspect

class MultiMethod(object):
    def __init__(self, name):
        self.name = name
        self.typemap = {}
    def __call__(self, *args):
        types = tuple(arg.__class__ for arg in args) # a generator expression!
        function = self.typemap.get(types)
        if function is None:
            raise TypeError("no match")
        return function(*args)
    def register(self, types, function):
        if types in self.typemap:
            raise TypeError("duplicate registration")
        self.typemap[types] = function

def multimethod(function):
    name = function.__name__
    mm = registry.get(name)
    if mm is None:
        mm = registry[name] = MultiMethod(name)
    spec = inspect.getfullargspec(function)
    types = tuple(spec.annotations[x] for x in spec.args)
    mm.register(types, function)
    return mm

i przykład użycia:

from mm import multimethod

@multimethod
def foo(a: int):
    return "an int"

@multimethod
def foo(a: int, b: str):
    return "an int and a string"

if __name__ == '__main__':
    print("foo(1,'a') = {}".format(foo(1,'a')))
    print("foo(7) = {}".format(foo(7)))

Można to zrobić, dodając typy do dekoratora jako oryginalny post Guido pokazuje , ale adnotacji do samych parametrów jest lepsze, ponieważ pozwala uniknąć niewłaściwego dopasowania parametrów i typów.

Uwaga : w Pythonie możesz uzyskać dostęp do adnotacji, function.__annotations__a nie function.func_annotationstak, jak func_*styl został usunięty w Pythonie 3.

Muhammad Alkarouri
źródło
2
Ciekawa aplikacja, choć obawiam się, function = self.typemap.get(types)że nie zadziała, gdy w grę wchodzą podklasy. W takim przypadku prawdopodobnie będziesz musiał przejść przez typemapusing isinnstance. Zastanawiam się, czy dobrze @overloadsobie z tym radzi
Tobias Kienzler
Myślę, że to jest zepsute, jeśli funkcja ma typ powrotu
zenna
1
__annotations__ To dict, że nie zapewnia argumenty porządku, więc ten fragment czasami zawodzą. Polecam zmieniając types = tuple(...)się spec = inspect.getfullargspec(function)potem types = tuple([spec.annotations[x] for x in spec.args]).
xoolive,
Masz całkowitą rację, @xoolive. Dlaczego nie zmienisz odpowiedzi, aby dodać swoją poprawkę?
Muhammad Alkarouri
@xoolive: Zauważyłem. Czasami redaktorzy używają ciężkiej ręki w zarządzaniu zmianami. Zmodyfikowałem pytanie, aby uwzględnić twoją poprawkę. Właściwie dyskutowałem na ten temat, ale nie ma sposobu, aby odrzucić poprawkę. Przy okazji dzięki za pomoc.
Muhammad Alkarouri
22

Uri udzielił już właściwej odpowiedzi, więc oto mniej poważna odpowiedź: możesz więc skrócić swoje dokumenty.

UKŁUCIE
źródło
2
kocham to. +1. jednak ostatecznie pisanie ciągów dokumentów jest nadal numerem jeden, dzięki któremu mój kod jest czytelny, jednak jeśli miałbyś zaimplementować jakiekolwiek statyczne lub dynamiczne sprawdzanie, dobrze jest mieć to. Może znajdę dla niego zastosowanie.
Warren P
8
Nie polecam używania adnotacji jako zamiennika dla Args: section, @param lines lub podobnych w twoich dokumentach (niezależnie od wybranego formatu). Chociaż adnotacje w dokumentacji stanowią ładny przykład, szkodzą one potencjalnej mocy adnotacji, ponieważ mogą przeszkadzać innym, potężniejszym zastosowaniom. Ponadto nie można pominąć adnotacji w czasie wykonywania, aby zmniejszyć zużycie pamięci (python -OO), tak jak w przypadku ciągów dokumentów i instrukcji assert.
GPS
2
@gps: Jak powiedziałem, była to mniej poważna odpowiedź.
JAB
2
Mówiąc poważnie, jest to znacznie lepszy sposób dokumentowania typów, których się spodziewasz, przy jednoczesnym przestrzeganiu DuckTyping.
Marc
1
@gps Nie jestem pewien, czy zużycie pamięci przez dokumentację jest powodem do zmartwień w 99,999% przypadków.
Tommy
13

Gdy po raz pierwszy zobaczyłem adnotacje, pomyślałem: „Świetnie! Wreszcie mogę włączyć sprawdzanie typów!” Oczywiście nie zauważyłem, że adnotacje nie są w rzeczywistości wymuszane.

Postanowiłem więc napisać prosty dekorator funkcji, aby je egzekwować :

def ensure_annotations(f):
    from functools import wraps
    from inspect import getcallargs
    @wraps(f)
    def wrapper(*args, **kwargs):
        for arg, val in getcallargs(f, *args, **kwargs).items():
            if arg in f.__annotations__:
                templ = f.__annotations__[arg]
                msg = "Argument {arg} to {f} does not match annotation type {t}"
                Check(val).is_a(templ).or_raise(EnsureError, msg.format(arg=arg, f=f, t=templ))
        return_val = f(*args, **kwargs)
        if 'return' in f.__annotations__:
            templ = f.__annotations__['return']
            msg = "Return value of {f} does not match annotation type {t}"
            Check(return_val).is_a(templ).or_raise(EnsureError, msg.format(f=f, t=templ))
        return return_val
    return wrapper

@ensure_annotations
def f(x: int, y: float) -> float:
    return x+y

print(f(1, y=2.2))

>>> 3.2

print(f(1, y=2))

>>> ensure.EnsureError: Argument y to <function f at 0x109b7c710> does not match annotation type <class 'float'>

Dodałem go do biblioteki Zapewnij .

tkacz
źródło
Mam to samo rozczarowanie, gdy wyszedłem, wierząc, że Python wreszcie ma sprawdzanie typów. W końcu będzie musiał przejść do domowej kontroli typu.
Hibou57,
3

Minęło dużo czasu, odkąd o to zadano, ale przykładowy fragment podany w pytaniu pochodzi (jak tam również stwierdzono) z PEP 3107 i na końcu przykładu PEP Podano również przypadki użycia, które mogą odpowiedzieć na pytanie z punktu PEP zobacz;)

Poniższy cytat pochodzi z PEP3107

Przypadków użycia

W trakcie omawiania adnotacji podniesiono szereg przypadków użycia. Niektóre z nich zostały tutaj przedstawione, pogrupowane według rodzaju przekazywanych informacji. Uwzględniono również przykłady istniejących produktów i pakietów, w których można by użyć adnotacji.

  • Dostarczanie informacji dotyczących pisania
    • Kontrola typu ([3], [4])
    • Niech IDE pokażą, jakich typów funkcja oczekuje i zwraca ([17])
    • Przeciążanie funkcji / funkcje ogólne ([22])
    • Mosty obcojęzyczne ([18], [19])
    • Adaptacja ([21], [20])
    • Funkcje logiczne predykatów
    • Mapowanie zapytań do bazy danych
    • Organizowanie parametrów RPC ([23])
  • Inne informacje
    • Dokumentacja parametrów i zwracanych wartości ([24])

Więcej informacji na temat konkretnych punktów (wraz z odniesieniami) można znaleźć w PEP

klaas
źródło
Byłbym bardzo wdzięczny, gdyby osoby negatywne zostawiały przynajmniej krótki komentarz, który spowodował głos przeciw. To naprawdę pomogłoby (przynajmniej mi) w poprawie.
klaas
2

Python 3.X (tylko) również uogólnia definicję funkcji, aby umożliwić przypisywanie argumentów i wartości zwracanych do wartości obiektu do użycia w rozszerzeniach .

Jego META-dane do wyjaśnienia, aby bardziej szczegółowo opisać wartości funkcji.

Adnotacje są kodowane :valuepo nazwie argumentu i przed wartością domyślną oraz ->valuepo liście argumentów.

Są one gromadzone w __annotations__atrybucie funkcji, ale w innym przypadku nie są traktowane jako specjalne przez sam Python:

>>> def f(a:99, b:'spam'=None) -> float:
... print(a, b)
...
>>> f(88)
88 None
>>> f.__annotations__
{'a': 99, 'b': 'spam', 'return': <class 'float'>}

Źródło: Python Pocket Reference, Fifth Edition

PRZYKŁAD:

Plik typeannotationsModuł dostarcza zestaw narzędzi do sprawdzania typu i rodzaju wnioskowania kodu Pythona. Zawiera również zestaw typów przydatnych do opisywania funkcji i obiektów.

Narzędzia te są przeznaczone głównie do użytku w analizatorach statycznych, takich jak lintery, biblioteki uzupełniania kodu i środowiska IDE. Dodatkowo dostępne są dekoratory do przeprowadzania kontroli w czasie wykonywania. Sprawdzanie typów w czasie wykonywania nie zawsze jest dobrym pomysłem w Pythonie, ale w niektórych przypadkach może być bardzo przydatne.

https://github.com/ceronman/typeannotations

Jak pisanie pomaga pisać lepszy kod

Wpisywanie może pomóc w przeprowadzeniu statycznej analizy kodu w celu wykrycia błędów typu przed wysłaniem kodu do produkcji i uniknięcia niektórych oczywistych błędów. Istnieją narzędzia takie jak mypy, które możesz dodać do swojego zestawu narzędzi w ramach cyklu życia oprogramowania. mypy może sprawdzić poprawne typy, uruchamiając częściowo lub całkowicie w oparciu o Twój kod. mypy pomaga również wykrywać błędy, takie jak sprawdzanie typu None, gdy wartość jest zwracana przez funkcję. Wpisywanie pomaga uczynić kod bardziej przejrzystym. Zamiast dokumentować kod za pomocą komentarzy, w których określasz typy w łańcuchu dokumentów, możesz używać typów bez żadnych kosztów wydajności.

Czysty Python: eleganckie kodowanie w Pythonie ISBN: ISBN-13 (pbk): 978-1-4842-4877-5

PEP 526 - Składnia adnotacji zmiennych

https://www.python.org/dev/peps/pep-0526/

https://www.attrs.org/en/stable/types.html

Demz
źródło
@BlackJack, „do użytku w rozszerzeniach” nie było jasne?
Demz
To jasne, ale nie odpowiada na pytanie IMHO. To tak, jakby odpowiadać „Jakie są dobre zastosowania klas?” Za pomocą „Do użytku w programach.” Jest jasne, poprawne, ale strona pytająca nie jest tak naprawdę mądra, co do cholery, konkretnych zastosowań. Twoja jest odpowiedzią, która nie może być bardziej ogólna, z przykładem, który jest zasadniczo taki sam, jak ten już w pytaniu .
BlackJack
1

Pomimo wszystkich opisanych tutaj zastosowań, jedynym możliwym do wyegzekwowania i najprawdopodobniej wymuszonym użyciem adnotacji będzie wskazówki dotyczące typów .

Obecnie nie jest to w żaden sposób wymuszane, ale sądząc po PEP 484, przyszłe wersje Pythona będą zezwalać tylko na typy jako wartości dla adnotacji.

Cytowanie A co z obecnymi zastosowaniami adnotacji? :

Mamy nadzieję, że wskazówki dotyczące typów ostatecznie staną się wyłącznym zastosowaniem adnotacji, ale będzie to wymagało dodatkowej dyskusji i okresu przestarzałego po początkowym wdrożeniu modułu pisania w Pythonie 3.5. Obecny PEP będzie miał status tymczasowy (patrz PEP 411) do czasu wydania Pythona 3.6. Najszybszy możliwy schemat wprowadziłby ciche wycofanie adnotacji innych niż wskazujące na typ w 3.6, całkowite wycofanie w 3.7 i zadeklarowałoby wskazówki dotyczące typów jako jedyne dozwolone użycie adnotacji w Pythonie 3.8.

Chociaż nie widziałem jeszcze żadnych cichych wycofań w 3.6, zamiast tego można to z powodzeniem podnieść do 3.7.

Tak więc, nawet jeśli mogą istnieć inne dobre przypadki użycia, najlepiej zachować je wyłącznie do podpowiedzi typu, jeśli nie chcesz zmieniać wszystkiego w przyszłości, w której to ograniczenie jest na miejscu.

Dimitris Fasarakis Hilliard
źródło
1

Jako nieco opóźnioną odpowiedź, kilka moich pakietów (marrow.script, WebCore itp.) Używa adnotacji, jeśli są dostępne, do deklarowania typowania (tj. Przekształcania wartości przychodzących z sieci, wykrywania, które argumenty są przełącznikami logicznymi itp.) jak wykonać dodatkowe znaczniki argumentów.

Marrow Script buduje kompletny interfejs wiersza poleceń dla dowolnych funkcji i klas oraz pozwala na definiowanie dokumentacji, rzutowania i wartości domyślnych pochodzących z wywołań zwrotnych za pośrednictwem adnotacji, z dekoratorem obsługującym starsze środowiska wykonawcze. Wszystkie moje biblioteki korzystające z adnotacji obsługują następujące formularze:

any_string  # documentation
any_callable  # typecast / callback, not called if defaulting
(any_callable, any_string)  # combination
AnnotationClass()  # package-specific rich annotation object
[AnnotationClass(), AnnotationClass(), …]  # cooperative annotation

Obsługa „gołych” ciągów dokumentów lub funkcji rzutowania typów umożliwia łatwiejsze mieszanie z innymi bibliotekami, które obsługują adnotacje. (To znaczy mam kontroler sieciowy używający rzutowania typów, który również jest ujawniany jako skrypt wiersza poleceń).

Edytowano, aby dodać: Zacząłem również korzystać z pakietu TypeGuard przy użyciu asercji w czasie programowania do walidacji. Korzyści: w przypadku uruchomienia z włączoną „optymalizacją” ( -O/ PYTHONOPTIMIZEenv var) pomija się kontrole, które mogą być kosztowne (np. Rekurencyjne), z założeniem, że aplikacja została poprawnie przetestowana w fazie rozwoju, więc kontrole powinny być niepotrzebne w środowisku produkcyjnym.

amcgregor
źródło
-2

Adnotacje mogą służyć do łatwego modularyzacji kodu. Np. Moduł dla programu, który obsługuję, mógłby po prostu zdefiniować metodę taką jak:

def run(param1: int):
    """
    Does things.

    :param param1: Needed for counting.
    """
    pass

i moglibyśmy poprosić użytkownika o rzecz o nazwie „param1”, która jest „potrzebna do liczenia” i powinna być wartością „int”. W końcu możemy nawet przekonwertować ciąg podany przez użytkownika na żądany typ, aby uzyskać jak najbardziej bezproblemowe doświadczenie.

Zobacz nasz obiekt metadanych funkcji dla klasy open source, która pomaga w tym i może automatycznie pobierać potrzebne wartości i konwertować je na dowolny pożądany typ (ponieważ adnotacja jest metodą konwersji). Nawet IDE poprawnie pokazują autouzupełnienia i zakładają, że typy są zgodne z adnotacjami - idealnie pasują.

Lasse Schuirmann
źródło
-2

Jeśli spojrzysz na listę zalet Cythona, główną z nich jest możliwość poinformowania kompilatora, jakiego typu jest obiekt Pythona.

Mogę wyobrazić sobie przyszłość, w której Cython (lub podobne narzędzia, które kompilują część twojego kodu Pythona) będą używać składni adnotacji do wykonywania swojej magii.

boardrider
źródło
RPython Annotator jest przykładem podejścia, które odczuwa odpowiednio pythonowy; po wygenerowaniu wykresu aplikacji może obliczyć typ każdej zmiennej i (w przypadku RPython) wymusić bezpieczeństwo pojedynczego typu. OTOH wymaga „boksu” lub innych rozwiązań / obejść, aby umożliwić dynamiczne bogate wartości. Kim jestem, aby zmusić moją multiplyfunkcję do działania tylko na liczbach całkowitych, kiedy 'na' * 8 + ' batman!'jest całkowicie poprawna? ;)
amcgregor