Nombramiento forzado de parámetros en Python

111

En Python puede tener una definición de función:

def info(object, spacing=10, collapse=1)

que podría llamarse de cualquiera de las siguientes formas:

info(odbchelper)                    
info(odbchelper, 12)                
info(odbchelper, collapse=0)        
info(spacing=15, object=odbchelper)

gracias a que Python permite argumentos de cualquier orden, siempre que tengan nombre.

El problema que tenemos es que a medida que crecen algunas de nuestras funciones más grandes, la gente podría estar agregando parámetros entre spacingy collapse, lo que significa que los valores incorrectos pueden ir a parámetros que no tienen nombre. Además, a veces no siempre está claro qué se debe incluir. Buscamos una manera de obligar a las personas a nombrar ciertos parámetros, no solo un estándar de codificación, sino idealmente una bandera o un complemento de pydev.

de modo que en los 4 ejemplos anteriores, solo el último pasaría la verificación ya que se nombran todos los parámetros.

Lo más probable es que solo lo activemos para ciertas funciones, pero cualquier sugerencia sobre cómo implementar esto, o si es posible, será apreciada.

Mark Mayo
fuente

Respuestas:

214

En Python 3: Sí, puede especificarlo *en la lista de argumentos.

De los documentos :

Los parámetros después de "*" o "* identificador" son parámetros de palabras clave y solo se pueden pasar argumentos de palabras clave usadas.

>>> def foo(pos, *, forcenamed):
...   print(pos, forcenamed)
... 
>>> foo(pos=10, forcenamed=20)
10 20
>>> foo(10, forcenamed=20)
10 20
>>> foo(10, 20)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() takes exactly 1 positional argument (2 given)

Esto también se puede combinar con **kwargs:

def foo(pos, *, forcenamed, **kwargs):
Eli Bendersky
fuente
32

Puede obligar a las personas a usar argumentos de palabras clave en Python3 definiendo una función de la siguiente manera.

def foo(*, arg0="default0", arg1="default1", arg2="default2"):
    pass

Al convertir el primer argumento en un argumento posicional sin nombre, obliga a todos los que llaman a la función a usar los argumentos de palabras clave, que es lo que creo que estaba preguntando. En Python2, la única forma de hacer esto es definir una función como esta

def foo(**kwargs):
    pass

Eso obligará a la persona que llama a usar kwargs, pero esta no es una solución tan buena, ya que tendría que marcar para aceptar solo el argumento que necesita.

martega
fuente
11

Es cierto que la mayoría de los lenguajes de programación hacen que el orden de los parámetros sea parte del contrato de llamada a la función, pero esto no tiene por qué ser así. ¿Por qué lo haría? Mi comprensión de la pregunta es, entonces, si Python es diferente a otros lenguajes de programación a este respecto. Además de otras buenas respuestas para Python 2, considere lo siguiente:

__named_only_start = object()

def info(param1,param2,param3,_p=__named_only_start,spacing=10,collapse=1):
    if _p is not __named_only_start:
        raise TypeError("info() takes at most 3 positional arguments")
    return str(param1+param2+param3) +"-"+ str(spacing) +"-"+ str(collapse)

La única forma en que una persona que llama podría proporcionar argumentos spacingy collapseposicionalmente (sin excepción) sería:

info(arg1, arg2, arg3, module.__named_only_start, 11, 2)

La convención de no usar elementos privados que pertenecen a otros módulos ya es muy básica en Python. Al igual que con el propio Python, esta convención para los parámetros solo se aplicaría parcialmente.

De lo contrario, las llamadas tendrían que tener el formato:

info(arg1, arg2, arg3, spacing=11, collapse=2)

Una llamada

info(arg1, arg2, arg3, 11, 2)

asignaría el valor 11 al parámetro _p y una excepción surgida por la primera instrucción de la función.

Caracteristicas:

  • Parámetros antes _p=__named_only_start se admiten posicionalmente (o por nombre).
  • Los parámetros posteriores _p=__named_only_startdeben proporcionarse solo por el nombre (a menos que __named_only_startse obtenga y se utilice el conocimiento sobre el objeto centinela especial ).

Pros:

  • Los parámetros son explícitos en número y significado (lo último si también se eligen buenos nombres, por supuesto).
  • Si el centinela se especifica como primer parámetro, entonces todos los argumentos deben especificarse por nombre.
  • Al llamar a la función, es posible cambiar al modo posicional utilizando el objeto centinela __named_only_start en la posición correspondiente.
  • Se puede anticipar un mejor desempeño que otras alternativas.

Contras:

  • La verificación se produce durante el tiempo de ejecución, no durante la compilación.
  • Uso de un parámetro adicional (aunque no argumento) y una verificación adicional. Pequeña degradación del rendimiento con respecto a las funciones habituales.
  • La funcionalidad es un truco sin el apoyo directo del idioma (ver nota a continuación).
  • Al llamar a la función, es posible cambiar al modo posicional utilizando el objeto centinela __named_only_starten la posición correcta. Sí, esto también puede verse como un profesional.

Tenga en cuenta que esta respuesta solo es válida para Python 2. Python 3 implementa el mecanismo similar, pero muy elegante, compatible con el lenguaje descrito en otras respuestas.

Descubrí que cuando abro mi mente y pienso en ello, ninguna pregunta o decisión de otros parece estúpida, tonta o simplemente tonta. Muy al contrario: normalmente aprendo mucho.

Mario Rossi
fuente
"La comprobación se produce durante el tiempo de ejecución, no durante la compilación". - Creo que eso es cierto para todas las comprobaciones de argumentos de funciones. Hasta que ejecute la línea de la invocación de la función, no siempre sabrá qué función se está ejecutando. Además, +1 : esto es inteligente.
Eric
@Eric: Es solo que hubiera preferido la verificación estática. Pero tienes razón: eso no habría sido Python en absoluto. Aunque no es un punto decisivo, la construcción "*" de Python 3 también se verifica dinámicamente. Gracias por tu comentario.
Mario Rossi
Además, si nombra la variable del módulo _named_only_start, es imposible hacer referencia a ella desde un módulo externo, lo que elimina un pros y un contra. (Los guiones bajos iniciales únicos en el ámbito del módulo son privados, IIRC)
Eric
Con respecto al nombre del centinela, también podríamos tener una __named_only_starty una named_only_start(sin subrayado inicial), la segunda para indicar que el modo nombrado es "recomendado", pero no al nivel de "promoción activa" (ya que uno es público y el otro no lo es). Con respecto a la "privacidad" de _namescomenzar con guiones bajos, el lenguaje no la impone de manera muy estricta: se puede eludir fácilmente mediante el uso de importaciones específicas (no *) o nombres calificados. Es por esto que varios documentos de Python prefieren usar el término "no público" en lugar de "privado".
Mario Rossi
6

Puede hacerlo de una manera que funcione tanto en Python 2 como en Python 3 , creando un primer argumento de palabra clave "falso" con un valor predeterminado que no ocurrirá "naturalmente". Ese argumento de palabra clave puede estar precedido por uno o más argumentos sin valor:

_dummy = object()

def info(object, _kw=_dummy, spacing=10, collapse=1):
    if _kw is not _dummy:
        raise TypeError("info() takes 1 positional argument but at least 2 were given")

Esto permitira:

info(odbchelper)        
info(odbchelper, collapse=0)        
info(spacing=15, object=odbchelper)

pero no:

info(odbchelper, 12)                

Si cambia la función a:

def info(_kw=_dummy, spacing=10, collapse=1):

entonces todos los argumentos deben tener palabras clave y info(odbchelper) ya no funcionarán.

Esto le permitirá colocar argumentos de palabras clave adicionales en cualquier lugar después _kw , sin forzarlo a colocarlos después de la última entrada. Esto a menudo tiene sentido, por ejemplo, agrupar cosas lógicamente o ordenar las palabras clave alfabéticamente puede ayudar con el mantenimiento y el desarrollo.

Por lo tanto, no es necesario volver a usar def(**kwargs)y perder la información de la firma en su editor inteligente. Su contrato social consiste en proporcionar cierta información, al forzar (algunas de ellas) a requerir palabras clave, el orden en que se presentan se ha vuelto irrelevante.

Anthon
fuente
2

Actualizar:

Me di cuenta de que usarlo **kwargsno resolvería el problema. Si sus programadores cambian los argumentos de la función como lo deseen, uno podría, por ejemplo, cambiar la función a esto:

def info(foo, **kwargs):

y el código anterior se rompería nuevamente (porque ahora cada llamada a función debe incluir el primer argumento).

Realmente se reduce a lo que dice Bryan.


(...) personas podrían estar agregando parámetros entre spacingy collapse(...)

En general, al cambiar funciones, los argumentos nuevos siempre deben ir al final. De lo contrario, rompe el código. Debería ser obvio.
Si alguien cambia la función para que el código se rompa, este cambio debe rechazarse.
(Como dice Bryan, es como un contrato)

(...) a veces no siempre está claro qué se debe introducir.

Al observar la firma de la función (es decir def info(object, spacing=10, collapse=1)), uno debería ver inmediatamente que todos los argumentos que no tienen un valor predeterminado son obligatorios.
Para qué es el argumento, debe ir en la cadena de documentos.


Respuesta anterior (guardada para completar) :

Probablemente esta no sea una buena solución:

Puede definir funciones de esta manera:

def info(**kwargs):
    ''' Some docstring here describing possible and mandatory arguments. '''
    spacing = kwargs.get('spacing', 15)
    obj = kwargs.get('object', None)
    if not obj:
       raise ValueError('object is needed')

kwargses un diccionario que contiene cualquier argumento de palabra clave. Puede comprobar si hay un argumento obligatorio y, en caso contrario, generar una excepción.

La desventaja es que puede que ya no sea tan obvio qué argumentos son posibles, pero con una cadena de documentos adecuada, debería estar bien.

Felix Kling
fuente
3
Me gustó más tu vieja respuesta. Simplemente escriba un comentario sobre por qué solo acepta ** kwargs en la función. Después de todo, cualquiera puede cambiar cualquier cosa en el código fuente: necesita documentación para describir la intención y el propósito detrás de sus decisiones.
Brandon
¡No hay una respuesta real en esta respuesta!
Phil
2

Los argumentos de solo palabra clave de python3 ( *) se pueden simular en python2.x con**kwargs

Considere el siguiente código de python3:

def f(pos_arg, *, no_default, has_default='default'):
    print(pos_arg, no_default, has_default)

y su comportamiento:

>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes 1 positional argument but 3 were given
>>> f(1, no_default='hi')
1 hi default
>>> f(1, no_default='hi', has_default='hello')
1 hi hello
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() missing 1 required keyword-only argument: 'no_default'
>>> f(1, no_default=1, wat='wat')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() got an unexpected keyword argument 'wat'

Esto puede ser simulado utilizando la siguiente, tenga en cuenta que he tomado la libertad de cambiar TypeErrora KeyErroren el caso "requerido llamado argumento", no sería demasiado trabajo para hacer que el mismo tipo de excepción, así

def f(pos_arg, **kwargs):
    no_default = kwargs.pop('no_default')
    has_default = kwargs.pop('has_default', 'default')
    if kwargs:
        raise TypeError('unexpected keyword argument(s) {}'.format(', '.join(sorted(kwargs))))

    print(pos_arg, no_default, has_default)

Y comportamiento:

>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes exactly 1 argument (3 given)
>>> f(1, no_default='hi')
(1, 'hi', 'default')
>>> f(1, no_default='hi', has_default='hello')
(1, 'hi', 'hello')
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
KeyError: 'no_default'
>>> f(1, no_default=1, wat='wat')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in f
TypeError: unexpected keyword argument(s) wat

La receta funciona igualmente bien en python3.x, pero debe evitarse si solo es python3.x

Anthony Sottile
fuente
Ah, ¿entonces kwargs.pop('foo')es un modismo de Python 2? Necesito actualizar mi estilo de codificación. Todavía estaba usando este enfoque en Python 3 🤔
Neil
0

Puede declarar que sus funciones **argssolo reciben . Eso exigiría argumentos de palabras clave, pero tendría un trabajo adicional para asegurarse de que solo se pasen nombres válidos.

def foo(**args):
   print args

foo(1,2) # Raises TypeError: foo() takes exactly 0 arguments (2 given)
foo(hello = 1, goodbye = 2) # Works fine.
Noufal Ibrahim
fuente
1
No solo tiene que agregar verificaciones de palabras clave, sino pensar en un consumidor que sabe que tiene que llamar a un método con la firma foo(**kwargs). ¿Qué paso en eso? foo(killme=True, when="rightnowplease")
Dagrooms
Depende realmente. Considere dict.
Noufal Ibrahim
0

Como dicen otras respuestas, cambiar las firmas de funciones es una mala idea. Agregue nuevos parámetros al final o corrija todas las llamadas si se insertan argumentos.

Si aún desea hacerlo, use un decorador de funciones y la función inspect.getargspec . Se usaría algo como esto:

@require_named_args
def info(object, spacing=10, collapse=1):
    ....

La implementación de require_named_argsse deja como ejercicio para el lector.

No me molestaría. Será lento cada vez que se llame a la función, y obtendrá mejores resultados escribiendo código con más cuidado.

Daniel Newby
fuente
-1

Podrías usar el **operador:

def info(**kwargs):

de esta manera la gente se ve obligada a utilizar parámetros con nombre.

Olivier Verdier
fuente
2
Y no tiene idea de cómo llamar a su método sin leer su código, lo que aumenta la carga cognitiva en su consumidor :(
Dagrooms
Por la razón mencionada, esta es una práctica realmente mala y debe evitarse.
David S.
-1
def cheeseshop(kind, *arguments, **keywords):

en python si usa * args, eso significa que puede pasar n-número de argumentos para este parámetro, que vendrá como una lista dentro de la función para acceder

y si usa ** kw, eso significa sus argumentos de palabra clave, a los que se puede acceder como dict; puede pasar n-número de kw args, y si desea restringir, ese usuario debe ingresar la secuencia y los argumentos en orden, entonces no use * y ** - (su forma pitónica de proporcionar soluciones genéricas para grandes arquitecturas ...)

si desea restringir su función con valores predeterminados, puede verificar dentro de ella

def info(object, spacing, collapse)
  spacing = spacing or 10
  collapse = collapse or 1
shahjapan
fuente
¿Qué sucede si se desea que el espaciado sea 0? (respuesta, obtienes 10). Esta respuesta es tan incorrecta como todas las demás respuestas de ** kwargs por las mismas razones.
Phil
-2

No entiendo por qué un programador agregará un parámetro entre otros dos en primer lugar.

Si desea que los parámetros de la función se usen con nombres (p info(spacing=15, object=odbchelper). Ej. ), Entonces no debería importar en qué orden se definen, por lo que también puede poner los nuevos parámetros al final.

Si desea que el orden sea importante, ¡no puede esperar que nada funcione si lo cambia!

Umang
fuente
2
Esto no responde a la pregunta. Si es una buena idea o no es irrelevante, alguien podría hacerlo de todos modos.
Graeme Perrow
1
Como mencionó Graeme, alguien lo hará de todos modos. Además, si está escribiendo una biblioteca para que la utilicen otros, forzar (solo en Python 3) el paso de argumentos de solo palabras clave permite una flexibilidad adicional cuando tiene que refactorizar su API.
s0undt3ch