2013-07-05 18 views
9

Forse come un residuo dei miei giorni con un linguaggio fortemente tipizzato (Java), mi trovo spesso a scrivere funzioni e quindi a forzare i controlli di tipo. Ad esempio:Devo forzare il controllo del tipo Python?

def orSearch(d, query): 
    assert (type(d) == dict) 
    assert (type(query) == list) 

Devo continuare a farlo? quali sono i vantaggi nel fare/non farlo?

+5

Stai ancora lavorando con un linguaggio fortemente tipizzato ... dinamico! = Debole – Brian

+2

[Parlando di "fortemente tipizzato" e "debolmente digitato" ...] (http : //stackoverflow.com/a/9929697/395760) – delnan

+0

La domanda nel titolo è una perfetta domanda SO, e se è il modo efficiente/migliore, tu hai la risposta da solo. Tuttavia la domanda successiva alla fine che hai lì, è più di una domanda http://programmers.stackexchange.com/. – woozyking

risposta

6

Smettere di farlo.

Il punto di utilizzo di un linguaggio "dinamico" (fortemente digitato per valori *, non tipizzato rispetto alle variabili e late binding) è che le funzioni possono essere correttamente polimorfiche, in quanto faranno fronte a qualsiasi oggetto che supporta l'interfaccia su cui si basa la tua funzione ("digitazione anatra").

Python definisce un numero di protocolli comuni (ad esempio iterabili) che diversi tipi di oggetto possono implementare senza essere correlati l'uno all'altro. I protocolli non sono di per se una funzione di lingua (a differenza di un'interfaccia java).

Il risultato pratico di questo è che in generale, purché comprendiate i tipi nella vostra lingua e commentate in modo appropriato (anche con docstring, così anche altre persone capiscono i tipi del vostro programma), potete generalmente scrivere meno codice, perché non devi codificare il tuo sistema di tipi. Non finirai per scrivere lo stesso codice per tipi diversi, solo con dichiarazioni di tipo diverse (anche se le classi sono in gerarchie disgiunte), e non dovrai capire quali sono le casts sicure e quali no, se tu voglio provare a scrivere solo un pezzo di codice.

Ci sono altri linguaggi che offrono teoricamente la stessa cosa: digitare le lingue desunte. I più popolari sono C++ (usando i modelli) e Haskell. In teoria (e probabilmente nella pratica), si può finire a scrivere meno codice, perché i tipi sono risolti staticamente, quindi non sarà necessario scrivere gestori di eccezioni per gestire il tipo sbagliato. Trovo che richiedano ancora di programmare il sistema di tipi, piuttosto che i tipi effettivi nel programma (i loro sistemi di tipi sono teorici, e per essere rintracciabili, non analizzano l'intero programma). Se questo ti suona bene, prendi in considerazione l'uso di una di quelle lingue al posto di Python (o ruby, smalltalk o qualsiasi variante di lisp).

Invece del test del tipo, in python (o in qualsiasi linguaggio dinamico simile) si desidera utilizzare eccezioni da rilevare quando un oggetto non supporta un metodo particolare. In tal caso, lasciarlo andare in cima allo stack o prenderlo e sollevare l'eccezione relativa a un tipo improprio. Questo tipo di "meglio chiedere il perdono che il permesso" la codifica è un python idiomatico e contribuisce notevolmente al codice più semplice.

* In pratica. I cambiamenti di classe sono possibili in Python e Smalltalk, ma rari. Non è uguale al casting in un linguaggio di basso livello.

2

Questo è un modo non idiomatico di fare le cose. Tipicamente in Python si usano i test try/except.

def orSearch(d, query): 
    try: 
     d.get(something) 
    except TypeError: 
     print("oops") 
    try: 
     foo = query[:2] 
    except TypeError: 
     print("durn") 
+1

Assicuro a tutti che utilizzo un messaggio di errore molto migliore nel mio codice. –

2

Personalmente ho una certa avversione per asserisce sembra che il programmatore poteva vedere problemi a venire, ma non poteva essere disturbati a pensare a come gestirli, l'altro problema è che il vostro esempio sarà affermare se uno dei parametri è una classe derivata da quelle che ti aspetti, anche se tali classi dovrebbero funzionare! - nel tuo esempio sopra vorrei andare per qualcosa come:

def orSearch(d, query): 
    """ Description of what your function does INCLUDING parameter types and descriptions """ 
    result = None 
    if not isinstance(d, dict) or not isinstance(query, list): 
     print "An Error Message" 
     return result 
    ... 

tipo Nota corrisponde solo se il tipo è esattamente come previsto, isinstance lavora per classi derivate pure. ad esempio:

>>> class dd(dict): 
... def __init__(self): 
...  pass 
... 
>>> d1 = dict() 
>>> d2 = dd() 
>>> type(d1) 
<type 'dict'> 
>>> type(d2) 
<class '__main__.dd'> 
>>> type (d1) == dict 
True 
>>> type (d2) == dict 
False 
>>> isinstance(d1, dict) 
True 
>>> isinstance(d2, dict) 
True 
>>> 

Si potrebbe considerare di lanciare un'eccezione personalizzata piuttosto che un'asserzione. Potresti anche generalizzare ancora di più controllando che i parametri abbiano i metodi che ti servono.

BTWPuò essere pignoli di me, ma ho sempre cercare di evitare assert in C/C++ per il fatto che se rimane nel codice poi qualcuno nel giro di pochi anni farà un cambiamento che dovrebbe essere preso da esso, non testarlo abbastanza bene nel debug perché ciò avvenga, (o addirittura non testarlo affatto), compilare come deliverable, modalità di rilascio, - che rimuove tutti i asserimenti cioè tutti gli errori di controllo che sono stati fatti in quel modo e ora abbiamo un codice inaffidabile e un grosso mal di testa per trovare i problemi.

+0

Il tuo commento ha un senso. Sono anche d'accordo sul fatto che, specialmente in linguaggi come C, dove è facile far saltare il piede, l'idea di usare asserzioni come sostituto per il controllo degli errori è piuttosto scoraggiante. Ma allora qual è il punto delle affermazioni? È semplicemente da utilizzare nei test unitari? – franklin

+1

C/C++ asserisce solo fare qualcosa nel codice costruito come debug ed è generalmente considerata una cattiva idea fornire codice di debug, nei settori in cui ho lavorato è vietato anche il codice di test unitario diverso dal deliverable - per ovvi motivi per cui personalmente vedo poco gli asserzioni di C/C++ - gli asseriti di Python hanno senso per i test unitari _con gli strumenti di test_ e per dare un IDE un suggerimento su quale tipo dovrebbe essere una variabile per il completamento automatico, ecc, basta non ___rely___ su asserzioni. –

1

Sono d'accordo con l'approccio di Steve quando è necessario eseguire il controllo dei tipi. Non trovo spesso la necessità di eseguire il controllo dei tipi in Python, ma c'è almeno una situazione in cui lo faccio. Quello è dove non controllando il tipo potrebbe restituire una risposta errata che causerà un errore più tardi nel calcolo. Questi tipi di errori possono essere difficili da rintracciare e li ho sperimentati un numero di volte in Python. Come te, ho imparato Java prima, e non ho avuto a che fare con loro spesso.

Supponiamo che tu abbia una semplice funzione che si aspetta un array e restituisce il primo elemento.

def func(arr): return arr[0] 

se lo si chiama con un array, si otterrà il primo elemento dell'array.

>>> func([1,2,3]) 
1 

Si riceverà anche una risposta se lo si chiama con una stringa o un oggetto di qualsiasi classe che implementa il metodo magico getitem.

>>> func("123") 
'1' 

Questo ti darebbe una risposta, ma in questo caso è del tipo sbagliato. Questo può accadere con oggetti che hanno lo stesso method signature. Non si può scoprire l'errore se non molto più tardi nel calcolo. Se si verifica questo nel proprio codice, di solito significa che c'era un errore nel calcolo precedente, ma avendo il controllo ci si accorgerebbe di prima. Tuttavia, se stai scrivendo un pacchetto Python per altri, è probabilmente qualcosa che dovresti considerare a prescindere.

Non dovresti incorrere in una grossa penalizzazione delle prestazioni per il controllo, ma renderà il tuo codice più difficile da leggere, il che è una grande cosa nel mondo Python.

0

Due cose.

In primo luogo, se sei disposto a spendere ~ $ 200, puoi ottenere un IDE Python piuttosto buono. Io uso PyCharm e sono rimasto davvero colpito. (È dalle stesse persone che fanno ReSharper per C#.) Analizzerà il tuo codice mentre lo scrivi, e cercherà i punti in cui le variabili sono del tipo sbagliato (tra una pila di altre cose).

Secondo:

Prima ho usato PyCharm, mi sono imbattuto per lo stesso problema - vale a dire, mi piacerebbe dimenticare le firme specifiche di funzioni che ho scritto. Potrei averlo trovato da qualche parte, ma forse l'ho scritto (non ricordo ora). Ma in ogni caso è un decoratore che puoi usare attorno alle definizioni delle funzioni che controllano il tipo per te.

chiamare in questo modo

@require_type('paramA', str) 
@require_type('paramB', list) 
@require_type('paramC', collections.Counter) 
def my_func(paramA, paramB, paramC): 
    paramB.append(paramC[paramA].most_common()) 
    return paramB 

Comunque, ecco il codice del decoratore.

def require_type(my_arg, *valid_types): 
    ''' 
     A simple decorator that performs type checking. 

     @param my_arg: string indicating argument name 
     @param valid_types: *list of valid types 
    ''' 
    def make_wrapper(func): 
     if hasattr(func, 'wrapped_args'): 
      wrapped = getattr(func, 'wrapped_args') 
     else: 
      body = func.func_code 
      wrapped = list(body.co_varnames[:body.co_argcount]) 

     try: 
      idx = wrapped.index(my_arg) 
     except ValueError: 
      raise(NameError, my_arg) 

     def wrapper(*args, **kwargs): 

      def fail(): 
       all_types = ', '.join(str(typ) for typ in valid_types) 
       raise(TypeError, '\'%s\' was type %s, expected to be in following list: %s' % (my_arg, all_types, type(arg))) 

      if len(args) > idx: 
       arg = args[idx] 
       if not isinstance(arg, valid_types): 
        fail() 
      else: 
       if my_arg in kwargs: 
        arg = kwargs[my_arg] 
        if not isinstance(arg, valid_types): 
         fail() 

      return func(*args, **kwargs) 

     wrapper.wrapped_args = wrapped 
     return wrapper 
    return make_wrapper 
+1

Basta notare, no non devi spendere $ 200 per un IDE python. L'edizione della community di PyCharm è gratuita, così come la maggior parte degli altri IDE decenti. – swalladge

3

Se ti ostini a aggiungere il controllo di tipo al codice, si potrebbe voler esaminare annotations e come potrebbero semplificare ciò che avete da scrivere. Uno dei modelli questions su StackOverflow ha introdotto un correttore di testo piccolo e offuscato che sfrutta le annotazioni. Ecco un esempio in base alla tua domanda:

>>> def statictypes(a): 
    def b(a, b, c): 
     if b in a and not isinstance(c, a[b]): raise TypeError('{} should be {}, not {}'.format(b, a[b], type(c))) 
     return c 
    return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)]))) 

>>> @statictypes 
def orSearch(d: dict, query: dict) -> type(None): 
    pass 

>>> orSearch({}, {}) 
>>> orSearch([], {}) 
Traceback (most recent call last): 
    File "<pyshell#162>", line 1, in <module> 
    orSearch([], {}) 
    File "<pyshell#155>", line 5, in <lambda> 
    return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)]))) 
    File "<pyshell#155>", line 5, in <listcomp> 
    return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)]))) 
    File "<pyshell#155>", line 3, in b 
    if b in a and not isinstance(c, a[b]): raise TypeError('{} should be {}, not {}'.format(b, a[b], type(c))) 
TypeError: d should be <class 'dict'>, not <class 'list'> 
>>> orSearch({}, []) 
Traceback (most recent call last): 
    File "<pyshell#163>", line 1, in <module> 
    orSearch({}, []) 
    File "<pyshell#155>", line 5, in <lambda> 
    return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)]))) 
    File "<pyshell#155>", line 5, in <listcomp> 
    return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)]))) 
    File "<pyshell#155>", line 3, in b 
    if b in a and not isinstance(c, a[b]): raise TypeError('{} should be {}, not {}'.format(b, a[b], type(c))) 
TypeError: query should be <class 'dict'>, not <class 'list'> 
>>> 

Si potrebbe guardare il tipo-checker e meraviglia, "Che diavolo è che facendo?" Ho deciso di scoprirlo e trasformarlo in codice leggibile. La seconda bozza ha eliminato la funzione b (è possibile chiamarla verify). Il terzo e ultimo progetto realizzato alcuni miglioramenti ed è mostrata in basso per il vostro uso:

import functools 

def statictypes(func): 
    template = '{} should be {}, not {}' 
    @functools.wraps(func) 
    def wrapper(*args): 
     for name, arg in zip(func.__code__.co_varnames, args): 
      klass = func.__annotations__.get(name, object) 
      if not isinstance(arg, klass): 
       raise TypeError(template.format(name, klass, type(arg))) 
     result = func(*args) 
     klass = func.__annotations__.get('return', object) 
     if not isinstance(result, klass): 
      raise TypeError(template.format('return', klass, type(result))) 
     return result 
    return wrapper 

Edit:

Sono passati più di quattro anni da quando questa risposta è stato scritto, e molto è cambiato in Python da quel momento. Come risultato di questi cambiamenti e della crescita personale del linguaggio, sembra utile rivisitare il codice di controllo dei tipi e riscriverlo per sfruttare le nuove funzionalità e la migliore tecnica di codifica. Pertanto, viene fornita la seguente revisione che apporta alcuni miglioramenti marginali al decoratore di funzioni statictypes (ora rinominato static_types).

#! /usr/bin/env python3 
import functools 
import inspect 


def static_types(wrapped): 
    def replace(obj, old, new): 
     return new if obj is old else obj 

    signature = inspect.signature(wrapped) 
    parameter_values = signature.parameters.values() 
    parameter_names = tuple(parameter.name for parameter in parameter_values) 
    parameter_types = tuple(
     replace(parameter.annotation, parameter.empty, object) 
     for parameter in parameter_values 
    ) 
    return_type = replace(signature.return_annotation, signature.empty, object) 

    @functools.wraps(wrapped) 
    def wrapper(*arguments): 
     for argument, parameter_type, parameter_name in zip(
      arguments, parameter_types, parameter_names 
     ): 
      if not isinstance(argument, parameter_type): 
       raise TypeError(f'{parameter_name} should be of type ' 
           f'{parameter_type.__name__}, not ' 
           f'{type(argument).__name__}') 
     result = wrapped(*arguments) 
     if not isinstance(result, return_type): 
      raise TypeError(f'return should be of type ' 
          f'{return_type.__name__}, not ' 
          f'{type(result).__name__}') 
     return result 
    return wrapper 
8

Nella maggior parte dei casi interferirebbe con la digitazione anatra e con l'ereditarietà.

  • Ereditarietà: Certamente intenzione di scrivere qualcosa con l'effetto di

    assert isinstance(d, dict) 
    

    per assicurarsi che il codice funziona anche correttamente con sottoclassi di dict. Questo è simile all'utilizzo in Java, penso. Ma Python ha qualcosa che Java non ha, vale a dire

  • duck typing: funzioni più incorporate non richiedono che un oggetto appartiene a una classe specifica, solo che ha alcune funzioni membro che si comportano nel modo giusto . Il ciclo for, ad esempio, richiede solo che la variabile di loop sia iterabile , ovvero che abbia le funzioni membro e next() e che si comportino correttamente.

Pertanto, se non si desidera chiudere la porta alla piena potenza di Python, non verificare la presenza di tipi specifici nel codice di produzione. (Potrebbe essere utile per il debug, tuttavia.)

Problemi correlati