Jak funkcje Pythona radzą sobie z typami przekazywanych parametrów?

305

O ile się nie mylę, tworzenie funkcji w Pythonie działa w następujący sposób:

def my_func(param1, param2):
    # stuff

Jednak nie podajesz typów tych parametrów. Ponadto, o ile pamiętam, Python jest silnie typowanym językiem, dlatego wydaje się, że Python nie powinien przekazywać parametru innego typu niż oczekiwany przez twórcę funkcji. Skąd jednak Python wie, że użytkownik funkcji przekazuje odpowiednie typy? Czy program po prostu umrze, jeśli jest niewłaściwy, zakładając, że funkcja faktycznie używa parametru? Czy musisz podać typ?

Leif Andersen
źródło
15
Myślę, że zaakceptowana odpowiedź na to pytanie powinna zostać zaktualizowana, aby była bardziej zgodna z obecnymi możliwościami oferowanymi przez Python. Myślę, że ta odpowiedź zadziała.
code_dredd

Odpowiedzi:

172

Python jest silnie typowany, ponieważ każdy obiekt ma typ, każdy obiekt zna swój typ, niemożliwe jest przypadkowe lub celowe użycie obiektu typu „tak jakby” był to obiekt innego typu, a wszystkie podstawowe operacje na obiekcie są przekazane do tego typu.

To nie ma nic wspólnego z nazwami . Nazwa w Pythonie nie „ma typ”: czy i kiedy zdefiniowana nazwa, nazwa odnosi się do obiektu , a obiekt ma typ (ale w rzeczywistości nie życie typ o nazwie : A imię to imię).

Nazwa w Pythonie może doskonale odnosić się do różnych obiektów w różnych momentach (jak w większości języków programowania, choć nie we wszystkich) - i nie ma żadnych ograniczeń co do nazwy, więc jeśli kiedyś odwoływała się do obiektu typu X, jest wtedy na zawsze ograniczone, aby odnosić się tylko do innych obiektów typu X. Ograniczenia dotyczące nazw nie są częścią koncepcji „silnego pisania”, chociaż niektórzy entuzjaści pisania statycznego (gdzie nazwy ograniczone, a przy statycznej kompilacji AKA- czas, moda też) niewłaściwie używaj tego terminu.

Alex Martelli
źródło
71
Wydaje się więc, że mocne pisanie nie jest tak mocne, w tym konkretnym przypadku jest słabsze niż pisanie statyczne. IMHO, ograniczenie czasowe kompilacji w nazwie / zmiennej / referencji jest w rzeczywistości dość ważne, dlatego śmiało twierdzę, że python nie jest tak dobry jak pisanie statyczne w tym aspekcie. Proszę popraw mnie jeżeli się mylę.
liang
19
@liang To jest opinia, więc nie możesz mieć racji. Z pewnością jest to również moja opinia i próbowałem wielu języków. Fakt, że nie mogę użyć mojego IDE do znalezienia typu (a więc i członków) parametrów, jest główną wadą Pythona. Jeśli ta wada jest ważniejsza, to zalety pisania kaczego zależą od osoby, o którą pytasz.
Maarten Bodewes
6
Ale to nie odpowiada na żadne pytanie: „Skąd jednak Python wie, że użytkownik funkcji przekazuje odpowiednie typy? Czy program po prostu umrze, jeśli jest niewłaściwy, zakładając, że funkcja faktycznie używa parametru? Czy musisz podać typ? ” lub ..
qPCR4vir
4
@ qPCR4vir, dowolny obiekt można przekazać jako argument. Błąd (wyjątek, program nie „umrze”, jeśli jest zakodowany, aby go złapać, patrz try/ except) pojawi się, gdy i jeśli zostanie podjęta operacja, której obiekt nie obsługuje. W Pythonie 3.5 możesz teraz opcjonalnie „określić typy” argumentów, ale sam błąd nie wystąpi, jeśli specyfikacja zostanie naruszona; notacja typowania ma na celu jedynie pomoc w oddzieleniu narzędzi wykonujących analizy itp. Nie zmienia ona zachowania samego Pythona.
Alex Martelli,
2
@AlexMartelli. Podziękować! Dla mnie jest to prawidłowa odpowiedź: „Błąd (wyjątek, program nie„ umrze ”, jeśli jest zakodowany, aby go złapać, patrz spróbuj / wyjątek).”
qPCR4vir
753

Inne odpowiedzi wykonały dobrą robotę wyjaśniając pisanie kaczek i prostą odpowiedź tzot :

Python nie ma zmiennych, podobnie jak inne języki, w których zmienne mają typ i wartość; ma nazwy wskazujące na obiekty, które znają ich typ.

Jednak jedna interesująca rzecz zmieniła się od 2010 roku (kiedy pytanie zostało zadane po raz pierwszy), a mianowicie implementacja PEP 3107 (zaimplementowana w Pythonie 3). Możesz teraz właściwie określić typ parametru i typ zwracanego typu funkcji:

def pick(l: list, index: int) -> int:
    return l[index]

Widzimy tutaj, że pickbierze 2 parametry, listę li liczbę całkowitą index. Powinien także zwrócić liczbę całkowitą.

Tutaj sugeruje się, że ljest to lista liczb całkowitych, które możemy zobaczyć bez większego wysiłku, ale w przypadku bardziej złożonych funkcji może być nieco mylące, co powinna zawierać lista. Chcemy również, aby domyślna wartość wynosiła index0. Aby rozwiązać ten problem, możesz pickzamiast tego napisać w następujący sposób:

def pick(l: "list of ints", index: int = 0) -> int:
    return l[index]

Zauważ, że teraz wstawiamy ciąg jako typ l, który jest składniowo dozwolony, ale nie nadaje się do parsowania programowego (do którego wrócimy później).

Ważne jest, aby pamiętać, że Python nie podniesie wartości, TypeErrorjeśli przejdziesz do float index, przyczyną tego jest jeden z głównych punktów filozofii projektowania Pythona: „Wszyscy zgadzamy się tutaj na dorosłych” , co oznacza, że ​​oczekuje się bądź świadomy tego, co możesz przekazać do funkcji, a czego nie. Jeśli naprawdę chcesz napisać kod, który wyrzuca błędy TypeErrors, możesz użyć isinstancefunkcji, aby sprawdzić, czy przekazany argument jest poprawnego typu lub jego podklasy w następujący sposób:

def pick(l: list, index: int = 0) -> int:
    if not isinstance(l, list):
        raise TypeError
    return l[index]

Więcej o tym, dlaczego tak rzadko powinieneś to robić, a o tym, co powinieneś zrobić, omówiono w następnej sekcji i w komentarzach.

PEP 3107 nie tylko poprawia czytelność kodu, ale ma także kilka pasujących przypadków użycia, o których możesz przeczytać tutaj .


Adnotacja typu zyskała dużo większą uwagę w Pythonie 3.5 dzięki wprowadzeniu PEP 484, który wprowadza standardowy moduł do wskazówek typu.

Te wskazówki dotyczące typów pochodzą od mypy sprawdzania typu ( GitHub ), która jest teraz zgodna z PEP 484 .

Z modułem do pisania dołączony jest dość obszerny zbiór wskazówek dotyczących typów, w tym:

  • List, Tuple, Set, Map- do list, tuple, seti mapodpowiednio.
  • Iterable - przydatne dla generatorów.
  • Any - kiedy może być cokolwiek.
  • Union- kiedy może to być cokolwiek w ramach określonego zestawu typów, w przeciwieństwie do Any.
  • Optional- kiedy może to być Brak. Stenografia dla Union[T, None].
  • TypeVar - stosowany z lekami generycznymi.
  • Callable - używany głównie do funkcji, ale może być wykorzystany do innych wywołań.

Są to najczęstsze wskazówki dotyczące typów. Pełną listę można znaleźć w dokumentacji modułu do pisania .

Oto stary przykład wykorzystujący metody adnotacji wprowadzone w module pisania:

from typing import List

def pick(l: List[int], index: int) -> int:
    return l[index]

Jedną z potężnych funkcji jest Callablemożliwość wpisywania metod adnotacji, które przyjmują funkcję jako argument. Na przykład:

from typing import Callable, Any, Iterable

def imap(f: Callable[[Any], Any], l: Iterable[Any]) -> List[Any]:
    """An immediate version of map, don't pass it any infinite iterables!"""
    return list(map(f, l))

Powyższy przykład może stać się bardziej precyzyjny z użyciem TypeVarzamiast Any, ale zostało to pozostawione jako ćwiczenie dla czytelnika, ponieważ uważam, że już wypełniłem moją odpowiedź zbyt dużą ilością informacji o cudownych nowych funkcjach włączonych przez podpowiedzi typu.


Wcześniej, gdy udokumentowano jeden kod Pythona, na przykład Sphinx, niektóre z powyższych funkcji można było uzyskać pisząc dokumenty w formacie takim jak ten:

def pick(l, index):
    """
    :param l: list of integers
    :type l: list
    :param index: index at which to pick an integer from *l*
    :type index: int
    :returns: integer at *index* in *l*
    :rtype: int
    """
    return l[index]

Jak widać, wymaga to kilku dodatkowych wierszy (dokładna liczba zależy od tego, jak wyraźny chcesz być i jak formatujesz dokumenty). Ale teraz powinno być dla ciebie jasne, w jaki sposób PEP 3107 stanowi alternatywę, która jest na wiele (wszystkich?) Sposobów lepsza. Jest to szczególnie prawdziwe w połączeniu z PEP 484, który, jak widzieliśmy, zapewnia standardowy moduł, który definiuje składnię dla tego typu wskazówek / adnotacji, które mogą być używane w taki sposób, że są jednoznaczne i precyzyjne, a jednocześnie elastyczne, tworząc potężna kombinacja.

Moim osobistym zdaniem jest to jedna z największych funkcji Pythona w historii. Nie mogę się doczekać, aż ludzie zaczną wykorzystywać jego moc. Przepraszam za długą odpowiedź, ale tak się dzieje, kiedy się ekscytuję.


Przykład kodu Pythona, który intensywnie korzysta z podpowiedzi typu, można znaleźć tutaj .

erb
źródło
2
@rickfoosusa: Podejrzewam, że nie używasz Pythona 3, w którym dodano tę funkcję.
erb
26
Poczekaj minutę! Jeśli zdefiniowanie parametru i typu zwracanego znaku nie wywołuje a TypeError, jaki jest sens używania pick(l: list, index: int) -> inttakiego jak definiowanie jednowierszowe? Albo źle to zrozumiałem, nie wiem.
Erdin Eray
24
@Eray Erdin: To powszechne nieporozumienie i wcale nie złe pytanie. Może być używany do celów dokumentacji, pomaga IDE w lepszym autouzupełnianiu i znajdowaniu błędów przed czasem wykonania za pomocą analizy statycznej (podobnie jak mypy, o której wspomniałem w odpowiedzi). Istnieją nadzieje, że środowisko wykonawcze może skorzystać z informacji i faktycznie przyspieszyć programy, ale prawdopodobnie wdrożenie zajmie to dużo czasu. Państwo może również być w stanie stworzyć dekorator, który rzuca TypeErrors dla Ciebie (informacja jest przechowywana w __annotations__atrybucie obiektu funkcyjnego).
erb
2
@ErdinEray Powinienem dodać, że rzucanie TypeErrors jest złym pomysłem (debugowanie nigdy nie jest fajne, bez względu na to, jak dobrze są zgłaszane wyjątki). Ale nie obawiaj się, przewaga nowych funkcji opisanych w mojej odpowiedzi umożliwia lepszy sposób: nie polegaj na sprawdzaniu w czasie wykonywania, rób wszystko przed uruchomieniem z mypy lub użyj edytora, który wykonuje analizę statyczną dla Ciebie, taką jak PyCharm .
erb
2
@ Tony: Kiedy zwracasz dwa lub więcej obiektów, w rzeczywistości zwracasz krotkę, więc powinieneś użyć adnotacji typu Tuple, tj.def f(a) -> Tuple[int, int]:
erb
14

Nie określasz typu. Metoda zawiedzie (w czasie wykonywania) tylko wtedy, gdy spróbuje uzyskać dostęp do atrybutów, które nie są zdefiniowane w przekazywanych parametrach.

Więc ta prosta funkcja:

def no_op(param1, param2):
    pass

... nie zawiedzie, bez względu na to, jakie dwa argumenty zostaną przekazane.

Jednak ta funkcja:

def call_quack(param1, param2):
    param1.quack()
    param2.quack()

... nie powiedzie się, jeśli w czasie wykonywania param1i param2nie mają zarówno cechy wywołalnych nazwanych quack.

TM.
źródło
+1: Atrybuty i metody nie są ustalane statycznie. Pojęcie, w jaki sposób ten „właściwy typ” lub „zły typ” zostanie ustalony na podstawie tego, czy typ działa poprawnie w funkcji.
S.Lott,
11

Wiele języków ma zmienne, które są określonego typu i mają wartość. Python nie ma zmiennych; ma obiekty i używasz nazw, aby odwoływać się do tych obiektów.

W innych językach, kiedy mówisz:

a = 1

następnie zmienna (zwykle całkowita) zmienia swoją zawartość na wartość 1.

W Pythonie

a = 1

oznacza „użyj nazwy a, aby odnieść się do obiektu 1 ”. W interaktywnej sesji Pythona możesz wykonać następujące czynności:

>>> type(1)
<type 'int'>

Funkcja typejest wywoływana z obiektem 1; ponieważ każdy obiekt zna swój typ, łatwo typejest znaleźć wspomniany typ i zwrócić go.

Podobnie za każdym razem, gdy definiujesz funkcję

def funcname(param1, param2):

funkcja otrzymuje dwa obiekty i nazywa je param1oraz param2niezależnie od ich typów. Jeśli chcesz się upewnić, że otrzymane obiekty są określonego typu, zakoduj swoją funkcję tak, jakby były wymaganych typów i wychwytuj wyjątki, które są zgłaszane, jeśli nie są. Zgłoszone wyjątki to zwykle TypeError(użyto niepoprawnej operacji) i AttributeError(próbowano uzyskać dostęp do nieistniejącego elementu (metody też są członkami)).

tzot
źródło
8

Python nie jest mocno wpisany w sensie statycznego lub sprawdzania typu podczas kompilacji.

Większość kodu Pythona podlega tak zwanemu „Typowaniu kaczkowatemu” - na przykład szukasz metody readna obiekcie - nie obchodzi cię, czy obiekt jest plikiem na dysku czy gnieździe, po prostu chcesz odczytać N bajty z tego.

Mark Rushakoff
źródło
21
Python jest silnie napisany. Jest także dynamicznie wpisywany.
Daniel Newby
1
Ale to nie odpowiada na żadne pytanie: „Skąd jednak Python wie, że użytkownik funkcji przekazuje odpowiednie typy? Czy program po prostu umrze, jeśli jest niewłaściwy, zakładając, że funkcja faktycznie używa parametru? Czy musisz podać typ? ” lub ..
qPCR4vir
6

Jak wyjaśnia Alex Martelli ,

Normalnym, Pythonicznym, preferowanym rozwiązaniem jest prawie zawsze „pisanie kaczką”: spróbuj użyć argumentu tak, jakby był z określonego pożądanego typu, zrób to w instrukcji try / wyjątek, wychwytując wszystkie wyjątki, które mogłyby powstać, gdyby argument nie był w rzeczywistości tego typu (lub dowolnego innego typu ładnie naśladującego kaczkę ;-), aw klauzuli wyjątku spróbuj czegoś innego (używając argumentu „jak gdyby” był innego typu).

Przeczytaj resztę jego postu, aby uzyskać przydatne informacje.

Nick Presta
źródło
5

Python nie przejmuje się tym, co przekazujesz jego funkcjom. Kiedy zadzwonisz my_func(a,b), zmienne param1 i param2 będą wówczas zawierać wartości aib. Python nie wie, że wywołujesz funkcję z odpowiednimi typami i oczekuje, że programista się tym zajmie. Jeśli twoja funkcja zostanie wywołana z różnymi typami parametrów, możesz owinąć kod uzyskując do nich dostęp blokami try / oprócz i ocenić parametry w dowolny sposób.

Kyle
źródło
11
Python nie ma zmiennych, podobnie jak inne języki, w których zmienne mają typ i wartość; ma nazwy wskazujące na obiekty , które znają ich typ.
tzot
2

Nigdy nie podajesz typu; Python ma pojęcie pisania kaczego ; w zasadzie kod, który przetwarza parametry, przyjmie pewne założenia na ich temat - być może przez wywołanie pewnych metod, które parametr ma zaimplementować. Jeśli parametr jest niewłaściwego typu, zostanie zgłoszony wyjątek.

Zasadniczo od twojego kodu zależy, czy przekazujesz obiekty odpowiedniego typu - nie ma kompilatora, który wymusiłby to z wyprzedzeniem.

Justin Ethier
źródło
2

Jest jeden notoryczny wyjątek od wpisywania kaczek, o których warto wspomnieć na tej stronie.

Gdy strfunkcja wywołuje __str__metodę class, subtelnie sprawdza jej typ:

>>> class A(object):
...     def __str__(self):
...         return 'a','b'
...
>>> a = A()
>>> print a.__str__()
('a', 'b')
>>> print str(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __str__ returned non-string (type tuple)

Jakby Guido podpowiadał nam, który wyjątek powinien zgłosić program, jeśli napotka nieoczekiwany typ.

Antony Hatchkins
źródło
1

W Pythonie wszystko ma swój typ. Funkcja Python zrobi wszystko, o co jest poproszony, jeśli obsługuje ją typ argumentów.

Przykład: foododa wszystko, co można __add__edytować;) nie martwiąc się zbytnio o jego typ. Oznacza to, że aby uniknąć niepowodzenia, powinieneś dostarczyć tylko te rzeczy, które obsługują dodawanie.

def foo(a,b):
    return a + b

class Bar(object):
    pass

class Zoo(object):
    def __add__(self, other):
        return 'zoom'

if __name__=='__main__':
    print foo(1, 2)
    print foo('james', 'bond')
    print foo(Zoo(), Zoo())
    print foo(Bar(), Bar()) # Should fail
Pratik Deoghare
źródło
1

Nie widziałem tego w innych odpowiedziach, więc dodam to do puli.

Jak powiedzieli inni, Python nie wymusza typu na parametrach funkcji lub metody. Zakłada się, że wiesz, co robisz, i jeśli naprawdę musisz wiedzieć, jaki rodzaj przekazu został przekazany, sprawdzisz to i zdecydujesz, co zrobić dla siebie.

Jednym z głównych narzędzi do tego celu jest funkcja isinstance ().

Na przykład, jeśli napiszę metodę, która spodziewa się uzyskać surowe binarne dane tekstowe, zamiast normalnych ciągów zakodowanych w utf-8, mógłbym sprawdzić typ parametrów po drodze i albo dostosować się do tego, co znajdę, albo podnieść wyjątek do odmowy.

def process(data):
    if not isinstance(data, bytes) and not isinstance(data, bytearray):
        raise TypeError('Invalid type: data must be a byte string or bytearray, not %r' % type(data))
    # Do more stuff

Python zapewnia również wszelkiego rodzaju narzędzia do kopania w obiektach. Jeśli jesteś odważny, możesz nawet użyć importlib do tworzenia własnych obiektów dowolnych klas w locie. Zrobiłem to, aby odtworzyć obiekty z danych JSON. Coś takiego byłoby koszmarem w statycznym języku, takim jak C ++.

Dread Quixadhal
źródło
1

Aby efektywnie korzystać z modułu pisania (nowość w Pythonie 3.5), należy zastosować all ( *).

from typing import *

I będziesz gotowy do użycia:

List, Tuple, Set, Map - for list, tuple, set and map respectively.
Iterable - useful for generators.
Any - when it could be anything.
Union - when it could be anything within a specified set of types, as opposed to Any.
Optional - when it might be None. Shorthand for Union[T, None].
TypeVar - used with generics.
Callable - used primarily for functions, but could be used for other callables.

Jednak nadal można używać nazw typu jak int, list, dict, ...

prosti
źródło
1

Zaimplementowałem opakowanie, jeśli ktoś chciałby określić typy zmiennych.

import functools

def type_check(func):

    @functools.wraps(func)
    def check(*args, **kwargs):
        for i in range(len(args)):
            v = args[i]
            v_name = list(func.__annotations__.keys())[i]
            v_type = list(func.__annotations__.values())[i]
            error_msg = 'Variable `' + str(v_name) + '` should be type ('
            error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ')'
            if not isinstance(v, v_type):
                raise TypeError(error_msg)

        result = func(*args, **kwargs)
        v = result
        v_name = 'return'
        v_type = func.__annotations__['return']
        error_msg = 'Variable `' + str(v_name) + '` should be type ('
        error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ')'
        if not isinstance(v, v_type):
                raise TypeError(error_msg)
        return result

    return check

Użyj go jako:

@type_check
def test(name : str) -> float:
    return 3.0

@type_check
def test2(name : str) -> str:
    return 3.0

>> test('asd')
>> 3.0

>> test(42)
>> TypeError: Variable `name` should be type (<class 'str'>) but instead is type (<class 'int'>)

>> test2('asd')
>> TypeError: Variable `return` should be type (<class 'str'>) but instead is type (<class 'float'>)

EDYTOWAĆ

Powyższy kod nie działa, jeśli żaden z typów argumentów (lub return) nie został zadeklarowany. Następująca edycja może pomóc, z drugiej strony, działa tylko dla kwargs i nie sprawdza argumentów.

def type_check(func):

    @functools.wraps(func)
    def check(*args, **kwargs):
        for name, value in kwargs.items():
            v = value
            v_name = name
            if name not in func.__annotations__:
                continue

            v_type = func.__annotations__[name]

            error_msg = 'Variable `' + str(v_name) + '` should be type ('
            error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ') '
            if not isinstance(v, v_type):
                raise TypeError(error_msg)

        result = func(*args, **kwargs)
        if 'return' in func.__annotations__:
            v = result
            v_name = 'return'
            v_type = func.__annotations__['return']
            error_msg = 'Variable `' + str(v_name) + '` should be type ('
            error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ')'
            if not isinstance(v, v_type):
                    raise TypeError(error_msg)
        return result

    return check
Gergely Papp
źródło