La gran historia de los argumentos de funciones en Python

Bueno, en realidad, la historia de los argumentos en Python no es tan grande.



Siempre me sorprendió que para trabajar con los argumentos de las funciones de Python, solo necesitas comprender *argsy **kwargs. Y me sorprendió no en vano. Resulta que los argumentos no son nada fáciles. En esta publicación, quiero dar una descripción general de todo lo relacionado con los argumentos de función en Python. Espero que al final pueda mostrar el panorama general del trabajo con argumentos y que este artículo no se convierta en una publicación más en la que el lector no pueda encontrar nada nuevo. Y ahora, al grano.







La mayoría de los lectores de este artículo, creo, comprenden la esencia de los argumentos de función. Para los principiantes, permítanme explicarles que estos son objetos enviados a una función por el iniciador de su llamada. Al pasar argumentos a una función, se realizan muchas acciones, dependiendo del tipo de objetos que se envían a la función (objetos mutables o inmutables). Un iniciador de llamada a función es una entidad que llama a una función y le pasa argumentos. Hablando de funciones de llamada, hay algunas cosas a considerar que ahora discutiremos.



Los argumentos, cuyos nombres se especifican cuando se declara la función, almacenan los objetos pasados ​​a la función cuando se llama. Además, si se asigna algo a las correspondientes variables locales de funciones, sus parámetros, esta operación no afecta los objetos inmutables pasados ​​a las funciones. Por ejemplo:



def foo(a):
    a = a+5
    print(a)             #  15

a = 10
foo(a)
print(a)                 #  10


Como puede ver, la llamada a la función no afectó a la variable de ninguna manera a. Esto es exactamente lo que sucede cuando se pasa un objeto inmutable a una función.



Y si se pasan objetos mutables a funciones, es posible que encuentre un comportamiento del sistema diferente al anterior.



def foo(lst):
    lst = lst + ['new entry']
    print(lst)                #  ['Book', 'Pen', 'new entry']

lst = ['Book', 'Pen']
print(lst)                    #  ['Book', 'Pen']
foo(lst)
print(lst)                    #  ['Book', 'Pen']


¿Has notado algo nuevo aquí? Si responde "No", tiene razón. Pero si de alguna manera influimos en los elementos del objeto mutable pasado a la función, seremos testigos de algo diferente.



def foo(lst):
    lst[1] = 'new entry'
    print(lst)                #  ['Book', 'new entry']

lst = ['Book', 'Pen']
print(lst)                     #  ['Book', 'Pen']
foo(lst)
print(lst)                     #  ['Book', 'new entry']


Como puede ver, el objeto del parámetro lstse cambió después de la llamada a la función. Esto sucedió debido al hecho de que estamos trabajando con una referencia a un objeto almacenado en un parámetro lst. Como resultado, cambiar el contenido de este objeto está fuera del alcance de la función. Puede evitar esto simplemente haciendo copias profundas de dichos objetos y escribiéndolos en las variables locales de la función.



def foo(lst):
    lst = lst[:]
    lst[1] = 'new entry'
    print(lst)                   #  ['Book', 'new entry']

lst = ['Book', 'Pen']
print(lst)                       #  ['Book', 'Pen']
foo(lst)
print(lst)                       #  ['Book', 'Pen']


¿No te sorprendió eso todavía? Si no es así, me gustaría asegurarme de que omita lo que sabe e inmediatamente pase a material nuevo para usted. Y si es así, entonces, marque mis palabras, usted, al familiarizarse con los argumentos, aprenderá cosas mucho más interesantes.



Entonces, esto es lo que debe saber sobre los argumentos de función:



  1. El orden en el que se pasan los argumentos posicionales a las funciones.
  2. El orden en el que se pasan los argumentos con nombre a las funciones.
  3. Asignar valores de argumento predeterminados.
  4. Organización del procesamiento de conjuntos de argumentos de longitud variable.
  5. Desempaquetando argumentos.
  6. Usar argumentos que solo se pueden pasar por nombre (solo palabras clave).


Echemos un vistazo a cada uno de estos puntos.



1. Orden de paso de argumentos posicionales a funciones



Los argumentos posicionales se procesan de izquierda a derecha. Es decir, resulta que la posición del argumento pasado a la función está en correspondencia directa con la posición del parámetro utilizado en el encabezado de la función cuando se declaró.



def foo(d, e, f):
    print(d, e, f)

a, b, c = 1, 2, 3
foo(a, b, c)                  #  1, 2, 3
foo(b, a, c)                  #  2, 1, 3
foo(c, b, a)                  #  3, 2, 1


Las variables a, by ctienen los valores 1, 2 y 3. Estas variables juegan el papel de los argumentos con los que se llama a la función foo. Que, en la primera llamada de la función, corresponden a los parámetros d, ey f. Este mecanismo se aplica a casi todos los 6 puntos anteriores sobre lo que necesita saber sobre los argumentos de funciones en Python. La ubicación del argumento posicional que se pasa a la función cuando se llama juega un papel importante en la asignación de valores a los parámetros de la función.



2. Orden de transferencia de argumentos con nombre a funciones



Los argumentos con nombre se pasan a funciones con los nombres de estos argumentos correspondientes a los nombres que se les asignaron cuando se declaró la función.



def foo(arg1=0, arg2=0, arg3=0):
    print(arg1, arg2, arg3)

a, b, c = 1, 2, 3
foo(a,b,c)                          #  1 2 3
foo(arg1=a, arg2=b, arg3=c)         #  1 2 3
foo(arg3=c, arg2=b, arg1=a)         #  1 2 3
foo(arg2=b, arg1=a, arg3=c)         #  1 2 3


Como puede ver, la función footoma 3 argumentos. Estos argumentos se nombran arg1, arg2y arg3. Preste atención a cómo cambiamos la posición de los argumentos cuando llamamos a la función. Los argumentos con nombre se tratan de manera diferente a los argumentos posicionales, aunque el sistema continúa leyéndolos de izquierda a derecha. Python considera los nombres de los argumentos, no sus posiciones, al asignar los valores apropiados a los parámetros de las funciones. Como resultado, resulta que la función genera lo mismo independientemente de las posiciones de los argumentos que se le pasen. Siempre lo es 1 2 3.



Tenga en cuenta que los mecanismos descritos en el párrafo 1 siguen funcionando aquí.



3. Asignar valores de argumento predeterminados



Se pueden asignar valores predeterminados a argumentos con nombre. Cuando se usa este mecanismo en una función, ciertos argumentos se vuelven opcionales. La declaración de tales funciones se parece a lo que consideramos en el punto # 2. La única diferencia es cómo se llaman estas funciones.



def foo(arg1=0, arg2=0, arg3=0):
    print(arg1, arg2, arg3)

a, b, c = 1, 2, 3
foo(arg1=a)                         #  1 0 0
foo(arg1=a, arg2=b )                #  1 2 0
foo(arg1=a, arg2=b, arg3=c)         #  1 2 3


Tenga en cuenta que en este ejemplo no estamos pasando todos los argumentos a la función como se describe en su declaración. En estos casos, a los parámetros correspondientes se les asignan los valores predeterminados. Continuemos con este ejemplo:



foo(arg2=b)                         #  0 2 0
foo(arg2=b, arg3=c )                #  0 2 3

foo(arg3=c)                         #  0 0 3
foo(arg3=c, arg1=a )                #  1 0 3


Estos son ejemplos simples y directos del uso de los mecanismos descritos anteriormente para llamar a funciones con pasarle argumentos con nombre. Ahora compliquemos nuestros experimentos combinando lo que hemos hablado hasta ahora en los puntos # 1, # 2 y # 3:



foo(a, arg2=b)                      #  1 2 0
foo(a, arg2=b, arg3=c)              #  1 2 3
foo(a, b, arg3=c)                   #  1 2 3

foo(a)                              #  1 0 0
foo(a,b)                            #  1 2 0


Aquí, tanto los argumentos posicionales como los nombrados se utilizan al llamar a la función. Cuando se utilizan argumentos posicionales, el orden en el que se especifican sigue desempeñando un papel fundamental para pasar correctamente la entrada a la función.



Aquí me gustaría llamar su atención sobre un detalle notable. Consiste en que los argumentos posicionales no se pueden especificar después de los argumentos con nombre. Aquí hay un ejemplo para ayudarlo a comprender mejor esta idea:



foo(arg1=a, b)
>>>
foo(arg1=a, b)
           ^
SyntaxError: positional argument follows keyword argument
foo(a, arg2=b, c)
>>>
foo(a, arg2=b, c)
              ^
SyntaxError: positional argument follows keyword argument


Puedes tomarlo como regla. Los argumentos posicionales no tienen que seguir a los argumentos con nombre al llamar a una función.



4. Organización del procesamiento de conjuntos de argumentos de longitud variable



Aquí hablaremos de construcciones *argsy **kwargs. Cuando estas construcciones se utilizan en una declaración de función, esperamos que cuando se llame a la función, los conjuntos de argumentos de longitudes arbitrarias se representen como parámetros argsy kwargs. Cuando se aplica la construcción *args, el parámetro argsrecibe argumentos posicionales representados como una tupla. Cuando se aplica **kwargsen el kwargsotoño, los argumentos con nombre se enumeran en un diccionario.



def foo(*args):
    print(args)

a, b, c = 1, 2, 3

foo(a, b, c)                #  (1, 2, 3)
foo(a, b)                   #  (1, 2)
foo(a)                      #  (1)
foo(b, c)                   #  (2, 3)


Este código prueba que el parámetro argsalmacena una tupla que contiene lo que se pasó a la función cuando se llamó.



def foo(**kwargs):
    print(kwargs)

foo(a=1, b=2, c=3)        #  {'a': 1, 'b': 2, 'c': 3}
foo(a=1, b=2)             #  {'a': 1, 'b': 2}
foo(a=1)                  #  {'a': 1}
foo(b=2, c=3)             #  {'b': 2, 'c': 3}


El código anterior muestra que el parámetro kwargsalmacena un diccionario de pares clave-valor que representan los argumentos nombrados que se pasan a la función cuando se llama.



Sin embargo, debe tenerse en cuenta que una función diseñada para aceptar argumentos posicionales no puede pasar argumentos con nombre (y viceversa).



def foo(*args):
    print(args)

foo(a=1, b=2, c=3)
>>>
foo(a=1, b=2, c=3)
TypeError: foo() got an unexpected keyword argument 'a'
#########################################################
def foo(**kwargs):
    print(kwargs)

a, b, c = 1, 2, 3
foo(a, b, c)
>>>
TypeError: foo() takes 0 positional arguments but 3 were given


Ahora juntemos todo lo que analizamos en los puntos # 1, # 2, # 3 y # 4, y experimentemos con todo esto, examinando diferentes combinaciones de argumentos que se pueden pasar a funciones cuando son llamadas.



def foo(*args,**kwargs):
    print(args, kwargs)

foo(a=1,)
# () {'a': 1}

foo(a=1, b=2, c=3)
# () {'a': 1, 'b': 2, 'c': 3}

foo(1, 2, a=1, b=2)
# (1, 2) {'a': 1, 'b': 2}

foo(1, 2)
# (1, 2) {}


Como ves, tenemos una tupla argsy un diccionario a nuestra disposición kwargs.



Y aquí hay otra regla. Se basa en el hecho de que la estructura *argsno se puede utilizar después de la estructura **kwargs.



def foo(**kwargs, *args):
    print(kwargs, args)
>>>
    def foo(**kwargs, *args):
                      ^
SyntaxError: invalid syntax


La misma regla se aplica al orden en el que se especifican los argumentos al llamar a funciones. Los argumentos posicionales no deben seguir a argumentos con nombre.



foo(a=1, 1)
>>>
    foo(a=1, 1)
            ^
SyntaxError: positional argument follows keyword argument
foo(1, a=1, 2)
>>>
    foo(1, a=1, 2)
               ^
SyntaxError: positional argument follows keyword argument


Al declarar funciones, puede combinar argumentos posicionales *argsy de la *kwagrssiguiente manera:



def foo(var, *args,**kwargs):
    print(var, args, kwargs)

foo(1, a=1,)                            #  1
# 1 () {'a': 1}

foo(1, a=1, b=2, c=3)                   #  2
# 1 () {'a': 1, 'b': 2, 'c': 3}

foo(1, 2, a=1, b=2)                     #  3
# 1 (2,) {'a': 1, 'b': 2}
foo(1, 2, 3, a=1, b=2)                  #  4
# 1 (2, 3) {'a': 1, 'b': 2}
foo(1, 2)                               #  5
# 1 (2,) {}


Al declarar una función, fooasumimos que debe tener un argumento posicional requerido. Es seguido por un conjunto de argumentos posicionales de longitud variable, y este conjunto es seguido por un conjunto de argumentos denominados de longitud variable. Sabiendo esto, podemos "descifrar" fácilmente cada una de las llamadas de función anteriores.



A la 1función se le pasan argumentos 1y a=1. Estos son, respectivamente, argumentos posicionales y con nombre. 2Es una variedad 1. Aquí, la longitud del conjunto de argumentos posicionales es cero.



En 3pasamos funciones 1, 2y a=1,b=2. Esto significa que ahora acepta dos argumentos posicionales y dos argumentos con nombre. Según la declaración de función, resulta que1tomado como un argumento posicional requerido, 2entra en un conjunto de argumentos posicionales de longitud variable a=1y b=2termina en un conjunto de argumentos de longitud variable denominados.



Para llamar a esta función correctamente, debemos pasarle al menos un argumento posicional. De lo contrario, enfrentaremos un error.



def foo(var, *args,**kwargs):
    print(var, args, kwargs)

foo(a=1)
>>>
foo(a=1)
TypeError: foo() missing 1 required positional argument: 'var'


Otra variación de esta función es una función que declara que toma un argumento posicional requerido y un argumento con nombre, seguido de conjuntos de longitud variable de argumentos posicionales y con nombre.



def foo(var, kvar=0, *args,**kwargs):
    print(var, kvar, args, kwargs)

foo(1, a=1,)                               #  1
# 1 0 () {'a': 1}

foo(1, 2, a=1, b=2, c=3)                   #  2
# 1 0 () {'a': 1, 'b': 2, 'c': 3}

foo(1, 2, 3, a=1, b=2)                     #  3
# 1 2 () {'a': 1, 'b': 2}

foo(1, 2, 3, 4, a=1, b=2)                  #  4
# 1 2 (3,) {'a': 1, 'b': 2}

foo(1, kvar=2)                             #  5
# 1 2 () {}


Las llamadas a esta función se pueden "descifrar" de la misma forma que se hizo al analizar la función anterior.



Al llamar a esta función, se debe pasar al menos un argumento posicional. De lo contrario, encontraremos un error:



foo()
>>>
foo()
TypeError: foo() missing 1 required positional argument: 'var'
foo(1)
# 1 0 () {}


Tenga en cuenta que la llamada foo(1)funciona bien. El punto aquí es que si se llama a una función sin especificar un valor para un argumento con nombre, el valor se le asigna automáticamente.



Y aquí hay algunos errores más que se pueden encontrar si esta función se llama incorrectamente:



foo(kvar=1)                             #  1
>>>
TypeError: foo() missing 1 required positional argument: 'var'
foo(kvar=1, 1, a=1)                      #  2
>>>
SyntaxError: positional argument follows keyword argument
foo(1, kvar=2, 3, a=2)                   #  3
>>>
SyntaxError: positional argument follows keyword argument


Preste especial atención al error de tiempo de ejecución 3.



5. Desempaquetando argumentos



En las secciones anteriores, hablamos sobre cómo recopilar conjuntos de argumentos pasados ​​a funciones en tuplas y diccionarios. Y aquí discutiremos la operación inversa. Es decir, analizaremos el mecanismo que le permite descomprimir los argumentos proporcionados a la entrada de la función.



args = (1, 2, 3, 4)
print(*args)                  #  1 2 3 4
print(args)                   #  (1, 2, 3, 4)

kwargs = { 'a':1, 'b':2}
print(kwargs)                 #  {'a': 1, 'b': 2}
print(*kwargs)                #  a b


Puede descomprimir las variables usando la sintaxis *y **. Así es como se utilizan al pasar tuplas, listas y diccionarios a una función.



def foo(a, b=0, *args, **kwargs):
    print(a, b, args, kwargs)

tup = (1, 2, 3, 4)
lst = [1, 2, 3, 4]
d = {'e':1, 'f':2, 'g':'3'}

foo(*tup)             # foo(1, 2, 3, 4)
# 1 2 (3, 4) {}

foo(*lst)             # foo(1, 2, 3, 4)
# 1 2 (3, 4) {}

foo(1, *tup)          # foo(1, 1, 2, 3, 4)
# 1 1 (2, 3, 4) {}

foo(1, 5, *tup)       # foo(1, 5, 1, 2, 3, 4)
# 1 5 (1, 2, 3, 4) {}

foo(1, *tup, **d)     # foo(1, 1, 2, 3, 4 ,e=1 ,f=2, g=3)
# 1 1 (2, 3, 4) {'e': 1, 'f': 2, 'g': '3'}

foo(*tup, **d)         # foo(1, 1, 2, 3, 4 ,e=1 ,f=2, g=3)
# 1 2 (3, 4) {'e': 1, 'f': 2, 'g': '3'}
d['b'] = 45
foo(2, **d)             # foo(1, e=1 ,f=2, g=3, b=45)
# 2 45 () {'e': 1, 'f': 2, 'g': '3'}


Deconstruya cada una de las llamadas a funciones que se muestran aquí usando el desempaquetado de argumentos y observe cómo se verían las llamadas correspondientes sin usar *y **. Intente comprender qué sucede cuando realiza estas llamadas y cómo se desempaquetan las distintas estructuras de datos.



Si experimenta desempaquetando argumentos, puede encontrar un nuevo error:



foo(1, *tup, b=5)
>>>
TypeError: foo() got multiple values for argument 'b'
foo(1, b=5, *tup)
>>>
TypeError: foo() got multiple values for argument 'b'


Este error ocurre debido a un conflicto entre el argumento nombrado ,, b=5y el argumento posicional. Como descubrimos en la sección # 2, el orden de los argumentos nombrados no importa cuando se pasan. Como resultado, se produce el mismo error en ambos casos.



6. Usar argumentos que solo se pueden pasar por nombre (solo palabras clave)



En algunos casos, debe hacer que la función acepte los argumentos con nombre requeridos. Si, al declarar una función, describen argumentos que solo se pueden pasar por nombre, entonces dichos argumentos deben pasarse a ella siempre que se llame.



def foo(a, *args, b):
    print(a, args, b)

tup = (1, 2, 3, 4)

foo(*tup, b=35)
# 1 (2, 3, 4) 35

foo(1, *tup, b=35)
# 1 (1, 2, 3, 4) 35

foo(1, 5, *tup, b=35)
# 1 (5, 1, 2, 3, 4) 35

foo(1, *tup, b=35)
# 1 (1, 2, 3, 4) 35

foo(1, b=35)
# 1 () 35

foo(1, 2, b=35)
# 1 (2,) 35

foo(1)
# TypeError: foo() missing 1 required keyword-only argument: 'b'

foo(1, 2, 3)
# TypeError: foo() missing 1 required keyword-only argument: 'b'


Como puede ver, se espera que a la función se le pase necesariamente un argumento con nombre b, que, en la declaración de la función, se especifica después *args. En este caso, en la declaración de una función, simplemente puede usar un símbolo *, después de lo cual, separados por comas, hay identificadores de argumentos con nombre que se pueden pasar a la función solo por el nombre. Tal función no estaría diseñada para aceptar un conjunto de argumentos posicionales de longitud variable.



def foo(a, *, b, c):
    print(a, b, c)

tup = (1, 2, 3, 4)

foo(1, b=35, c=55)
# 1 35 55

foo(c= 55, b=35, a=1)
# 1 35 55

foo(1, 2, 3)
# TypeError: foo() takes 1 positional argument but 3 were given

foo(*tup, b=35)
# TypeError: foo() takes 1 positional argument but 4 positional arguments (and 1 keyword-only argument) were given

foo(1, b=35)
# TypeError: foo() takes 1 positional argument but 4 positional arguments (and 1 keyword-only argument) were given


La función declarada en el ejemplo anterior toma un argumento posicional y dos argumentos con nombre, que solo se pueden pasar por nombre. Esto lleva al hecho de que para que una función se llame correctamente, necesita pasar ambos argumentos con nombre. Después de eso, *también puede describir los argumentos nombrados, que reciben los valores predeterminados. Esto nos da cierta libertad al llamar a tales funciones.



def foo(a, *, b=0, c, d=0):
    print(a, b, c, d)

foo(1, c=55)
# 1 0 55 0

foo(1, c=55, b=35)
# 1 35 55 0

foo(1)
# TypeError: foo() missing 1 required keyword-only argument: 'c'


Tenga en cuenta que la función se puede llamar normalmente sin pasarle ningún argumento by dporque se le han dado valores predeterminados.



Salir



Quizás tengamos, de hecho, una historia muy larga sobre discusiones. Espero que los lectores de este material hayan aprendido algo nuevo por sí mismos. Y, por cierto, la historia de los argumentos de funciones en Python continúa. Quizás hablemos de ellos más tarde.



¿Aprendiste algo nuevo sobre los argumentos de funciones en Python de este material?






All Articles