In che modo le funzioni Python gestiscono i tipi di parametri che passi?


305

A meno che non mi sbagli, la creazione di una funzione in Python funziona in questo modo:

def my_func(param1, param2):
    # stuff

Tuttavia, in realtà non si forniscono i tipi di tali parametri. Inoltre, se ricordo, Python è un linguaggio fortemente tipizzato, in quanto tale, sembra che Python non dovrebbe lasciarti passare un parametro di un tipo diverso da quello previsto dal creatore della funzione. Tuttavia, come fa Python a sapere che l'utente della funzione sta passando nei tipi corretti? Il programma morirà se è del tipo sbagliato, supponendo che la funzione utilizzi effettivamente il parametro? Devi specificare il tipo?


15
Penso che la risposta accettata in questa domanda dovrebbe essere aggiornata per essere più in linea con le attuali capacità offerte da Python. Penso che questa risposta funzioni.
code_dredd,

Risposte:


173

Python è fortemente tipizzato perché ogni oggetto ha un tipo, ogni oggetto ne conosce il tipo, è impossibile usare accidentalmente o deliberatamente un oggetto di un tipo "come se" fosse un oggetto di un tipo diverso e tutte le operazioni elementari sull'oggetto sono delegato al suo tipo.

Questo non ha nulla a che fare con i nomi . Un nome in Python non "ha un tipo": se e quando viene definito un nome, il nome si riferisce a un oggetto e l' oggetto ha un tipo (ma che in realtà non forza un tipo sul nome : a il nome è un nome).

Un nome in Python può fare perfettamente riferimento a diversi oggetti in momenti diversi (come nella maggior parte dei linguaggi di programmazione, anche se non in tutti) - e non esiste alcun vincolo sul nome tale che, se una volta si è riferito a un oggetto di tipo X, è poi per sempre costretto a riferirsi solo ad altri oggetti di tipo Vincoli X. su nomi non sono parte del concetto di "tipizzazione forte", anche se alcuni appassionati di statica battitura (in cui i nomi non vengono vincolati, e in una statica, AKA di compilazione anche il tempo, la moda) usano il termine in questo modo.


71
Quindi sembra che la digitazione forte non sia così forte, in questo caso particolare, è più debole della digitazione statica.IMHO, il vincolo di digitazione del tempo di compilazione su nome / variabile / riferimento è in realtà abbastanza importante, quindi sostengo coraggiosamente che Python non è buono come la digitazione statica su questo aspetto. Per favore correggimi se sbaglio.
liang,

19
@liang Questa è un'opinione, quindi non puoi avere ragione o torto. È certamente anche la mia opinione e ho provato molte lingue. Il fatto che non riesca a usare il mio IDE per scoprire il tipo (e quindi i membri) dei parametri è un grave svantaggio di Python. Se questo inconveniente è più importante dei vantaggi della tipizzazione anatra dipende dalla persona che si chiede.
Maarten Bodewes,

6
Ma questo non risponde a nessuna delle domande: "Tuttavia, come fa Python a sapere che l'utente della funzione sta passando i tipi corretti? Il programma morirà solo se è del tipo sbagliato, supponendo che la funzione utilizzi effettivamente il parametro? Devi specificare il tipo? " oppure ..
qPCR4vir

4
@ qPCR4vir, qualsiasi oggetto può essere passato come argomento. L'errore (un'eccezione, il programma non "morirà" se è codificato per catturarlo, vedere try/ except) si verificherà quando e se si tenta di eseguire un'operazione che l'oggetto non supporta. In Python 3.5 ora puoi facoltativamente "specificare i tipi" di argomenti, ma di per sé non si verificano errori se le specifiche sono violate; la notazione di battitura ha solo lo scopo di aiutare a separare gli strumenti che eseguono analisi ecc., non altera il comportamento di Python stesso.
Alex Martelli

2
@AlexMartelli. Grazie! Per me questa è la risposta giusta: "L'errore (un'eccezione, il programma non" morirà "se è codificato per catturarlo, vedi prova / tranne) .."
qPCR4vir

753

Le altre risposte hanno fatto un buon lavoro nello spiegare la tipizzazione delle anatre e la semplice risposta di tzot :

Python non ha variabili, come altre lingue in cui le variabili hanno un tipo e un valore; ha nomi che indicano oggetti, che ne conoscono il tipo.

Tuttavia , una cosa interessante è cambiata dal 2010 (quando è stata posta la prima domanda), ovvero l'implementazione di PEP 3107 (implementata in Python 3). Ora puoi effettivamente specificare il tipo di un parametro e il tipo del tipo restituito di una funzione come questa:

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

Possiamo qui vedere che pickaccetta 2 parametri, un elenco le un numero intero index. Dovrebbe anche restituire un numero intero.

Quindi qui è implicito che lsia un elenco di numeri interi che possiamo vedere senza molto sforzo, ma per funzioni più complesse può essere un po 'confuso su ciò che l'elenco dovrebbe contenere. Vogliamo anche che il valore predefinito indexsia 0. Per risolvere questo problema, puoi scegliere di scrivere in pickquesto modo:

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

Si noti che ora inseriamo una stringa come tipo di l, che è sintatticamente consentita, ma non è utile per l'analisi a livello di codice (a cui torneremo più avanti).

È importante notare che Python non rilascerà a TypeErrorse passi un float index, la ragione di ciò è uno dei punti principali nella filosofia di progettazione di Python: "Siamo tutti consenzienti adulti qui" , il che significa che ci si aspetta che sii consapevole di cosa puoi passare a una funzione e cosa non puoi. Se vuoi davvero scrivere il codice che genera TypeErrors puoi usare la isinstancefunzione per verificare che l'argomento passato sia del tipo corretto o di una sua sottoclasse in questo modo:

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

Maggiori informazioni sul motivo per cui raramente dovresti fare questo e su cosa dovresti fare invece sono discussi nella sezione successiva e nei commenti.

PEP 3107 non solo migliora la leggibilità del codice, ma ha anche diversi casi d'uso adatti che puoi leggere qui .


L'annotazione dei tipi ha suscitato molta più attenzione in Python 3.5 con l'introduzione di PEP 484 che introduce un modulo standard per i suggerimenti sul tipo.

Questi suggerimenti sul tipo provengono dal mypy di controllo del tipo ( GitHub ), che ora è conforme a PEP 484 .

Con il modulo di digitazione viene fornita una raccolta piuttosto completa di suggerimenti sul tipo, tra cui:

  • List, Tuple, Set, Map- per list, tuple, sete map, rispettivamente.
  • Iterable - utile per i generatori.
  • Any - quando potrebbe essere qualsiasi cosa.
  • Union- quando potrebbe essere qualcosa all'interno di un determinato set di tipi, al contrario di Any.
  • Optional- quando potrebbe essere Nessuno. Stenografia per Union[T, None].
  • TypeVar - usato con generici.
  • Callable - Utilizzato principalmente per le funzioni, ma potrebbe essere utilizzato per altri callable.

Questi sono i suggerimenti di tipo più comuni. Un elenco completo è disponibile nella documentazione del modulo di battitura .

Ecco il vecchio esempio che utilizza i metodi di annotazione introdotti nel modulo di digitazione:

from typing import List

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

Una potente funzionalità è quella Callableche consente di digitare metodi annotati che accettano una funzione come argomento. Per esempio:

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

L'esempio sopra potrebbe diventare più preciso con l'uso di TypeVarinvece diAny , ma questo è stato lasciato come un esercizio al lettore poiché credo di aver già riempito la mia risposta con troppe informazioni sulle meravigliose nuove funzionalità abilitate dal suggerimento del tipo.


In precedenza, quando si documentava un codice Python con ad esempio Sphinx alcune delle funzionalità sopra menzionate potevano essere ottenute scrivendo docstring formattati in questo modo:

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]

Come puoi vedere, questo richiede un numero di righe extra (il numero esatto dipende da quanto esplicitamente vuoi essere e da come formattare la tua docstring). Ma ora ti dovrebbe essere chiaro come PEP 3107 offra un'alternativa che è in molti (tutti?) Modi superiori. Ciò è particolarmente vero in combinazione con PEP 484 che, come abbiamo visto, fornisce un modulo standard che definisce una sintassi per questi suggerimenti / annotazioni di tipo che possono essere utilizzati in modo che sia chiaro e preciso ma flessibile, rendendo combinazione potente.

Secondo la mia opinione personale, questa è una delle più grandi funzionalità di Python di sempre. Non vedo l'ora che le persone inizino a sfruttarne il potere. Ci scusiamo per la lunga risposta, ma questo è ciò che accade quando mi eccito.


Un esempio di codice Python che utilizza pesantemente i suggerimenti sul tipo può essere trovato qui .


2
@rickfoosusa: sospetto che non stia eseguendo Python 3 in cui è stata aggiunta la funzione.
erb,

26
Apetta un minuto! Se la definizione del parametro e il tipo restituito non generano a TypeError, qual è il punto di usare pick(l: list, index: int) -> intcome la definizione di una riga? O ho sbagliato, non lo so.
Erdin Eray,

24
@Eray Erdin: Questo è un malinteso comune e per niente una cattiva domanda. Può essere utilizzato a scopo di documentazione, aiuta gli IDE a completare meglio il completamento automatico e a trovare errori prima del runtime utilizzando l'analisi statica (proprio come il mypy che ho citato nella risposta). Ci sono speranze che il runtime possa trarre vantaggio dalle informazioni e velocizzare effettivamente i programmi, ma probabilmente ci vorrà molto tempo per essere implementati. Si potrebbe anche essere in grado di creare un decoratore che getta le TypeErrors per voi (le informazioni sono memorizzate nel __annotations__attributo dell'oggetto funzioni).
Erb,

2
@ErdinEray Dovrei aggiungere che lanciare TypeErrors è una cattiva idea (il debug non è mai divertente, indipendentemente dalle eccezioni ben intenzionate). Ma non temere, il vantaggio delle nuove funzionalità descritte nella mia risposta consente un modo migliore: non fare affidamento su alcun controllo in fase di runtime, fai tutto prima del runtime con mypy o usa un editor che esegue l'analisi statica per te come PyCharm .
Erb,

2
@Tony: quando ritorni due o più oggetti in realtà restituisci una tupla, quindi dovresti usare l'annotazione del tipo Tuple, cioèdef f(a) -> Tuple[int, int]:
erb

14

Non si specifica un tipo. Il metodo fallirà (in fase di runtime) se tenta di accedere ad attributi che non sono definiti sui parametri che vengono passati.

Quindi questa semplice funzione:

def no_op(param1, param2):
    pass

... non mancherà, indipendentemente da cosa siano passati due argomenti.

Tuttavia, questa funzione:

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

... fallirà in fase di esecuzione se param1e param2non hanno entrambi attributi richiamabili denominati quack.


+1: gli attributi e i metodi non sono determinati staticamente. Il concetto di come questo "tipo corretto" o "tipo sbagliato" viene stabilito se il tipo funziona correttamente nella funzione.
S. Lott

11

Molte lingue hanno variabili, che sono di un tipo specifico e hanno un valore. Python non ha variabili; ha oggetti e usi i nomi per riferirti a questi oggetti.

In altre lingue, quando dici:

a = 1

quindi una variabile (generalmente intera) cambia il suo contenuto nel valore 1.

In Python,

a = 1

significa "usa il nome a per riferirti all'oggetto 1 ". Puoi fare quanto segue in una sessione interattiva di Python:

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

La funzione typeviene chiamata con l'oggetto 1; poiché ogni oggetto conosce il suo tipo, è facile pertype scoprire quel tipo e restituirlo.

Allo stesso modo, ogni volta che si definisce una funzione

def funcname(param1, param2):

la funzione riceve due oggetti, li nomina param1e param2, indipendentemente dai loro tipi. Se vuoi assicurarti che gli oggetti ricevuti siano di un tipo specifico, codifica la tua funzione come se fosse del tipo o dei tipi necessari e prendi le eccezioni che vengono generate se non lo sono. Le eccezioni generate sono in genere TypeError(hai utilizzato un'operazione non valida) e AttributeError(hai tentato di accedere a un membro inesistente (anche i metodi sono membri)).


8

Python non è fortemente tipizzato nel senso del controllo del tipo in fase di compilazione o statico.

La maggior parte del codice Python rientra nel cosiddetto "Duck Typing" - ad esempio, cerchi un metodo readsu un oggetto - non ti importa se l'oggetto è un file sul disco o su un socket, vuoi solo leggere N byte da esso.


21
Python è fortemente tipizzato. Inoltre è digitato in modo dinamico.
Daniel Newby,

1
Ma questo non risponde a nessuna delle domande: "Tuttavia, come fa Python a sapere che l'utente della funzione sta passando i tipi corretti? Il programma morirà solo se è del tipo sbagliato, supponendo che la funzione utilizzi effettivamente il parametro? Devi specificare il tipo? " oppure ..
qPCR4vir

6

Come spiega Alex Martelli ,

La normale soluzione preferita di Pythonic è quasi invariabilmente la "tipizzazione anatra": prova a usare l'argomento come se fosse di un certo tipo desiderato, fallo in un'istruzione try / tranne catturando tutte le eccezioni che potrebbero sorgere se l'argomento non fosse in realtà di quel tipo (o qualsiasi altro tipo che lo imita piacevolmente ;-), e nella clausola tranne, prova qualcos'altro (usando l'argomento "come se" fosse di qualche altro tipo).

Leggi il resto del suo post per informazioni utili.


5

A Python non importa cosa passi alle sue funzioni. Quando si chiama my_func(a,b), le variabili param1 e param2 conterranno quindi i valori di a e b. Python non sa che stai chiamando la funzione con i tipi corretti e si aspetta che il programmatore se ne occupi. Se la tua funzione verrà chiamata con diversi tipi di parametri, puoi racchiudere il codice accedendo ad essi con blocchi try / tranne e valutare i parametri come preferisci.


11
Python non ha variabili, come altre lingue in cui le variabili hanno un tipo e un valore; ha nomi che indicano oggetti , che ne conoscono il tipo.
dal

2

Non si specifica mai il tipo; Python ha il concetto di scrivere l'anatra ; fondamentalmente il codice che elabora i parametri farà alcune ipotesi su di essi - forse chiamando determinati metodi che un parametro dovrebbe implementare. Se il parametro è di tipo errato, verrà generata un'eccezione.

In generale, spetta al tuo codice accertarti di passare in giro oggetti del tipo corretto: non esiste un compilatore che lo imponga in anticipo.


2

C'è una nota eccezione alla tipizzazione delle anatre che vale la pena menzionare in questa pagina.

Quando la strfunzione chiama il __str__metodo class, controlla attentamente il suo tipo:

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

Come se Guido ci suggerisse quale eccezione dovrebbe sollevare un programma se incontra un tipo inaspettato.


1

In Python ogni cosa ha un tipo. Una funzione Python farà tutto ciò che viene richiesto se il tipo di argomenti lo supporta.

Esempio: fooaggiungerà tutto ciò che può essere __add__ed;) senza preoccuparsi molto del suo tipo. Ciò significa che, per evitare il fallimento, è necessario fornire solo quelle cose che supportano l'aggiunta.

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

1

Non l'ho visto menzionato in altre risposte, quindi lo aggiungerò al piatto.

Come altri hanno già detto, Python non impone il tipo sui parametri di funzione o metodo. Si presume che tu sappia cosa stai facendo e che se hai davvero bisogno di sapere il tipo di qualcosa che è stato passato, lo controllerai e deciderai cosa fare per te stesso.

Uno degli strumenti principali per farlo è la funzione isinstance ().

Ad esempio, se scrivo un metodo che prevede di ottenere dati di testo binari non elaborati, piuttosto che le normali stringhe codificate utf-8, potrei verificare il tipo di parametri durante il passaggio e adattarmi a ciò che trovo, oppure aumentare un valore eccezione da rifiutare.

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 fornisce anche tutti i tipi di strumenti per scavare negli oggetti. Se sei coraggioso, puoi persino usare importlib per creare i tuoi oggetti di classi arbitrarie, al volo. L'ho fatto per ricreare oggetti dai dati JSON. Una cosa del genere sarebbe un incubo in un linguaggio statico come il C ++.


1

Per utilizzare efficacemente il modulo di digitazione (nuovo in Python 3.5) includere all ( *).

from typing import *

E sarai pronto per l'uso:

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.

Tuttavia, ancora si può utilizzare nomi di tipo come int, list, dict, ...


1

Ho implementato un wrapper se qualcuno desidera specificare i tipi di variabili.

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

Usalo come:

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

MODIFICARE

Il codice sopra riportato non funziona se non viene dichiarato alcun tipo di argomento (o di ritorno). La seguente modifica può aiutare, invece, funziona solo per kwargs e non controlla args.

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.