2013-03-08 13 views
22

Vorrei definire alcuni decoratori generici per verificare gli argomenti prima di chiamare alcune funzioni.Come utilizzare i decoratori Python per controllare gli argomenti delle funzioni?

Qualcosa di simile:

@checkArguments(types = ['int', 'float']) 
def myFunction(thisVarIsAnInt, thisVarIsAFloat) 
    ''' Here my code ''' 
    pass 

note collaterali:

  1. Tipo controllo è qui solo per mostrare un esempio
  2. sto usando Python 2.7, ma Python 3.0 whould essere interessante anche
+5

Come una nota, che in genere è davvero una cattiva idea - va contro il chicco di Python. Digitare il controllo è una brutta cosa in quasi tutti i casi. Vale anche la pena notare che potrebbe essere più sensato usare le annotazioni degli argomenti per farlo se si è in 3.x. –

+3

@Lattyware: l'applicazione degli argomenti delle funzioni e dei tipi restituiti è uno degli esempi in [l'originale pep per decoratori] (http://www.python.org/dev/peps/pep-0318/) – jfs

+0

Qual è la tua domanda? –

risposta

25

Dal Decorators for Functions and Methods:

def accepts(*types): 
    def check_accepts(f): 
     assert len(types) == f.func_code.co_argcount 
     def new_f(*args, **kwds): 
      for (a, t) in zip(args, types): 
       assert isinstance(a, t), \ 
         "arg %r does not match %s" % (a,t) 
      return f(*args, **kwds) 
     new_f.func_name = f.func_name 
     return new_f 
    return check_accepts 

Usage:

@accepts(int, (int,float)) 
def func(arg1, arg2): 
    return arg1 * arg2 

func(3, 2) # -> 6 
func('3', 2) # -> AssertionError: arg '3' does not match <type 'int'> 
+0

Lo sto usando su qualche metodo, ma sembra che f abbia sempre il valore dell'ultima funzione definita. Sai per caso da dove potrebbe venire? – AsTeR

+1

@AsTeR: crea un esempio [codice completo minimo] (http://sscce.org/) che riproduce il tuo problema e [pubblicalo come una nuova domanda] (http://stackoverflow.com/questions/ask). – jfs

+0

Lo farò, la mia richiesta è stata nel caso in cui qualcosa di ovvio sia apparso nella tua mente. – AsTeR

13

Su Python 3.3, è possibile utilizzare le annotazioni delle funzioni e ispezionare:

import inspect 

def validate(f): 
    def wrapper(*args): 
     fname = f.__name__ 
     fsig = inspect.signature(f) 
     vars = ', '.join('{}={}'.format(*pair) for pair in zip(fsig.parameters, args)) 
     params={k:v for k,v in zip(fsig.parameters, args)} 
     print('wrapped call to {}({})'.format(fname, params)) 
     for k, v in fsig.parameters.items(): 
      p=params[k] 
      msg='call to {}({}): {} failed {})'.format(fname, vars, k, v.annotation.__name__) 
      assert v.annotation(params[k]), msg 
     ret = f(*args) 
     print(' returning {} with annotation: "{}"'.format(ret, fsig.return_annotation)) 
     return ret 
    return wrapper 

@validate 
def xXy(x: lambda _x: 10<_x<100, y: lambda _y: isinstance(_y,float)) -> ('x times y','in X and Y units'): 
    return x*y 

xy = xXy(10,3) 
print(xy) 

Se c'è un errore di convalida, stampe:

AssertionError: call to xXy(x=12, y=3): y failed <lambda>) 

Se non c'è un errore di convalida, stampe:

wrapped call to xXy({'y': 3.0, 'x': 12}) 
    returning 36.0 with annotation: "('x times y', 'in X and Y units')" 

È possibile utilizzare una funzione piuttosto che una lambda a ottenere un nome nel fallimento dell'asserzione.

+0

Sembra interessante ma davvero difficile da capire a prima vista. Darò un'occhiata quando sarò meno stanco. – AsTeR

+3

Questa è un'implementazione _incredibly_ obfuscatory. Tecnicamente, funziona. Ma fa sanguinare gli occhi. Per un'alternativa molto più leggibile (anche se leggermente meno potente), vedi [sweeneyrod] (https://stackoverflow.com/users/2387370/sweeneyrod) concisa ['@ checkargs' decorator] (https://stackoverflow.com)/a/19684962/2809027) in una [domanda simile] (https://stackoverflow.com/a/19684962/2809027). –

1

Per far rispettare argomenti stringa ad un parser che getterebbe errori criptici Quando dispongono di ingresso non-string, ho scritto il seguente, che cerca di evitare allocazioni e chiamate di funzione:

from functools import wraps 

def argtype(**decls): 
    """Decorator to check argument types. 

    Usage: 

    @argtype(name=str, text=str) 
    def parse_rule(name, text): ... 
    """ 

    def decorator(func): 
     code = func.func_code 
     fname = func.func_name 
     names = code.co_varnames[:code.co_argcount] 

     @wraps(func) 
     def decorated(*args,**kwargs): 
      for argname, argtype in decls.iteritems(): 
       try: 
        argval = args[names.index(argname)] 
       except ValueError: 
        argval = kwargs.get(argname) 
       if argval is None: 
        raise TypeError("%s(...): arg '%s' is null" 
            % (fname, argname)) 
       if not isinstance(argval, argtype): 
        raise TypeError("%s(...): arg '%s': type is %s, must be %s" 
            % (fname, argname, type(argval), argtype)) 
      return func(*args,**kwargs) 
     return decorated 

    return decorator 
+0

Ho finito per usare questo: relativamente semplice, usa solo la libreria standard e funziona con un numero variabile di * args e ** kwargs. Unica avvertenza è che 'func_code' è stato rinominato in' __code__' in Python 3, non so se c'è un modo cross-version per farlo. – astrojuanlu

0

Ho un leggermente migliorata versione di @jbouwmans sollution, utilizzando il modulo python decoratore, il che rende il decoratore completamente trasparente e mantiene non solo la firma, ma anche docstring a posto e potrebbe essere il modo più elegante di utilizzare decoratori

from decorator import decorator 

def check_args(**decls): 
    """Decorator to check argument types. 

    Usage: 

    @check_args(name=str, text=str) 
    def parse_rule(name, text): ... 
    """ 
    @decorator 
    def wrapper(func, *args, **kwargs): 
     code = func.func_code 
     fname = func.func_name 
     names = code.co_varnames[:code.co_argcount] 
     for argname, argtype in decls.iteritems(): 
      try: 
       argval = args[names.index(argname)] 
      except IndexError: 
       argval = kwargs.get(argname) 
      if argval is None: 
       raise TypeError("%s(...): arg '%s' is null" 
          % (fname, argname)) 
      if not isinstance(argval, argtype): 
       raise TypeError("%s(...): arg '%s': type is %s, must be %s" 
          % (fname, argname, type(argval), argtype)) 
    return func(*args, **kwargs) 
return wrapper 
5

Come certamente sapete , non è pitonico rifiutare un argomento solo in base al suo tipo.
approccio Pythonic è piuttosto "cercare di affrontare il problema prima"
Ecco perché avrei preferito fare un decoratore per convertire gli argomenti

def enforce(*types): 
    def decorator(f): 
     def new_f(*args, **kwds): 
      #we need to convert args into something mutable 
      newargs = []   
      for (a, t) in zip(args, types): 
       newargs.append(t(a)) #feel free to have more elaborated convertion 
      return f(*newargs, **kwds) 
     return new_f 
    return decorator 

In questo modo, la funzione è alimentato con il tipo che ci si aspetta Ma se il parametro può ciarlatano come un galleggiante, è accettato

@enforce(int, float) 
def func(arg1, arg2): 
    return arg1 * arg2 

print (func(3, 2)) # -> 6.0 
print (func('3', 2)) # -> 6.0 
print (func('three', 2)) # -> ValueError: invalid literal for int() with base 10: 'three' 

io uso questo trucco (con la corretta metodo di conversione) per affrontare vectors.
Molti metodi che scrivo si aspettano la classe MyVector in quanto ha un sacco di funzionalità; ma a volte si vuole solo scrivere

transpose ((2,4)) 
+0

"Come certamente sapete, non è pitonico rifiutare un argomento solo in base al suo tipo.". Hai un riferimento per questo? – spinkus

0

Penso che la risposta Python 3.5 a questa domanda è beartype.Come spiegato in questo post viene fornito con funzionalità a portata di mano. Il tuo codice sarebbe quindi simile a questa

from beartype import beartype 
@beartype 
def sprint(s: str) -> None: 
    print(s) 

e si traduce in

>>> sprint("s") 
s 
>>> sprint(3) 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
    File "<string>", line 13, in func_beartyped 
TypeError: sprint() parameter s=3 not of <class 'str'> 
Problemi correlati