2011-12-28 23 views
6

Sé que no es pitónico escribir funciones que se preocupen por el tipo de argumentos, pero hay casos en los que es simplemente imposible ignorar tipos porque se manejan de manera diferente.Decorador para sobrecargar en Python

Tener un montón de isinstance comprobaciones en su función es feo; ¿Hay algún decorador de funciones disponible que habilite las sobrecargas de funciones? Algo como esto:

@overload(str) 
def func(val): 
    print('This is a string') 

@overload(int) 
def func(val): 
    print('This is an int') 

Actualización:

Aquí hay algunos comentarios que dejé en David Zaslavsky's answer:

Con unas pocas modificaciones [s], esto va a satisfacer mis propósitos bastante bien. Otra limitación que noté en su implementación, ya que usa func.__name__ como la clave del diccionario, es propenso a nombrar colisiones entre módulos, lo que no siempre es deseable. [Continuación]

[cont.] Por ejemplo, si tengo un módulo que sobrecarga func, y otro módulo completamente ajenos que también sobrecarga func, estas sobrecargas chocarán porque el dict función de despacho es global. Ese dict debería hacerse local al módulo, de alguna manera. Y no solo eso, también debería apoyar algún tipo de 'herencia'. [continuación]

[cont.] Por 'herencia' me refiero a esto: digamos que tengo un módulo first con algunas sobrecargas. Luego dos módulos más que no están relacionados pero cada uno importa first; ambos módulos agregan nuevas sobrecargas a las ya existentes que acaban de importar. Estos dos módulos deberían poder usar las sobrecargas en first, pero los nuevos que acaban de agregar no deberían colisionar entre ellos entre los módulos. (Esto es realmente muy difícil hacerlo bien, ahora que lo pienso.)

Algunos de estos problemas podrían resolverse posiblemente cambiando la sintaxis de decorador un poco:

first.py

@overload(str, str) 
def concatenate(a, b): 
    return a + b 

@concatenate.overload(int, int) 
def concatenate(a, b): 
    return str(a) + str(b) 

second.py

from first import concatenate 

@concatenate.overload(float, str) 
def concatenate(a, b): 
    return str(a) + b 
+0

Hmm ... así que en su edición, ¿qué quiere decir con 'contatenate.overload' en' first.py'? Como está escrito, esto intentaría acceder al atributo 'sobrecarga' de la función' concatenar', que no existe en este ejemplo. –

+0

@DavidZaslavsky La primera sobrecarga de la función debe decorarse con '@ overload', que devolverá un objeto invocable que tenga y atribuirá' overload'. Todas las sobrecargas posteriores deberían decorarse con este objeto ya existente, '@ object.overload', por lo que solo hay una dict por objeto, no una dict global. (Funciona de manera similar a la 'propiedad @ estándar.) Escribiré una implementación y le notificaré cuando la publique. –

+1

Eso sería una desviación de la sintaxis de sobrecarga tradicional. Pero si eso es lo que quieres, echa un vistazo al [paquete de sobrecarga] (http://pypi.python.org/pypi/overload/1.1) (que estoy a punto de editar en mi respuesta). Utiliza esencialmente ese método. –

Respuesta

4

respuesta rápida: hay un overload package en PyPI que implementa esta forma más robusta de lo que describo a continuación, aunque usando una sintaxis ligeramente diferente. Se declaró que solo funcionaba con Python 3, pero parece que solo se necesitarían algunas modificaciones (si las hubiera, no lo he intentado) para que funcionen con Python 2.


Respuesta larga: En lenguajes donde se puede sobrecargar funciones, el nombre de una función es (ya sea literalmente o con eficacia) aumentada por información sobre su firma de tipo, tanto cuando se define la función y cuando está llamado. Cuando un compilador o intérprete busca la definición de la función, utiliza tanto el nombre declarado como los tipos de los parámetros para resolver a qué función acceder. Entonces, la forma lógica de implementar la sobrecarga en Python es implementar un contenedor que use el nombre declarado y los tipos de parámetros para resolver la función.

Aquí es una implementación simple:

from collections import defaultdict 

def determine_types(args, kwargs): 
    return tuple([type(a) for a in args]), \ 
      tuple([(k, type(v)) for k,v in kwargs.iteritems()]) 

function_table = defaultdict(dict) 
def overload(arg_types=(), kwarg_types=()): 
    def wrap(func): 
     named_func = function_table[func.__name__] 
     named_func[arg_types, kwarg_types] = func 
     def call_function_by_signature(*args, **kwargs): 
      return named_func[determine_types(args, kwargs)](*args, **kwargs) 
     return call_function_by_signature 
    return wrap 

overload debe ser llamado con dos argumentos opcionales, una tupla que representa los tipos de los argumentos posicionales y una tupla de tuplas que representan las asignaciones de tipo nombre de todos los argumentos de palabras clave. He aquí un ejemplo de uso:

>>> @overload((str, int)) 
... def f(a, b): 
...  return a * b 

>>> @overload((int, int)) 
... def f(a, b): 
...  return a + b 

>>> print f('a', 2) 
aa 
>>> print f(4, 2) 
6 

>>> @overload((str,), (('foo', int), ('bar', float))) 
... def g(a, foo, bar): 
...  return foo*a + str(bar) 

>>> @overload((str,), (('foo', float), ('bar', float))) 
... def g(a, foo, bar): 
...  return a + str(foo*bar) 

>>> print g('a', foo=7, bar=4.4) 
aaaaaaa4.4 
>>> print g('b', foo=7., bar=4.4) 
b30.8 

deficiencias de esto incluyen

  • En realidad, no compruebe que la función del decorador se aplica es incluso compatible con los argumentos dados al decorador. Puede escribir

    @overload((str, int)) 
    def h(): 
        return 0 
    

    y obtendrá un error cuando se llame a la función.

  • No maneja con gracia el caso en que no existe versión sobrecargada que corresponden a los tipos de los argumentos pasados ​​(que ayudaría a elevar un error más descriptivo)

  • Se distingue entre argumentos con nombre y posicionales, así que algo como

    g('a', 7, bar=4.4) 
    

    no funciona.

  • Hay muchos paréntesis anidados implicados en el uso de esto, como en las definiciones de g.
  • Como se menciona en los comentarios, esto no se aplica a las funciones que tienen el mismo nombre en diferentes módulos.

Todo esto podría ser remediado con suficiente manipulación, creo. En particular, el problema de las colisiones de nombres se resuelve fácilmente almacenando la tabla de envío como un atributo de la función devuelta por el decorador. Pero como dije, este es solo un ejemplo simple para demostrar los conceptos básicos de cómo hacerlo.

+0

Con algunas modificaciones, esto se adaptará bastante bien a mis propósitos. Otra limitación que noté en su implementación, ya que usa 'func .__ name__' como la clave del diccionario, es propenso a nombrar colisiones entre módulos, lo que no siempre es deseable. [cont'd] –

+0

[cont.] Por ejemplo, si tengo un módulo que sobrecarga 'func', y otro módulo _completely unrelated_ que también sobrecarga' func', estas sobrecargas colisionarán porque la función dich de despacho es global. Ese dict debería hacerse local al módulo, de alguna manera. Y no solo eso, también debería apoyar algún tipo de 'herencia'. [continuación] –

+0

[cont.] Por 'herencia' me refiero a esto: digamos que tengo un módulo 'primero' con algunas sobrecargas. Luego dos módulos más que no están relacionados pero cada uno importa 'primero'; ambos módulos agregan nuevas sobrecargas a las ya existentes que acaban de importar. Estos dos módulos deberían poder usar las sobrecargas en "primero", pero los nuevos que acaban de agregar no deberían colisionar entre ellos entre los módulos. (Esto es realmente bastante difícil de hacer bien, ahora que lo pienso). –

0

Esto no responde directamente a tu pregunta, pero si realmente quieres tener algo que se comporte como una función sobrecargada para diferentes tipos y (con razón) no quiero usar isinstance entonces sugeriría algo como:

def func(int_val=None, str_val=None): 
    if sum(x != None for x in (int_val, str_val)) != 1: 
     #raise exception - exactly one value should be passed in 
    if int_val is not None: 
     print('This is an int') 
    if str_val is not None: 
     print('This is a string') 

en uso, la intención es obvia, y ni siquiera requiere las diferentes opciones que tienen diferentes tipos:

func(int_val=3) 
func(str_val="squirrel") 
+0

Eso funciona para ejemplos simples, pero ¿qué tan bien escala? –

+0

Bueno, si tienes muchas posibilidades, puedes usar '** kwargs' y hacerlo un poco más programático. Lo he usado en un constructor con 17 palabras clave permitidas y funcionó bien para mí. –

Cuestiones relacionadas