Functions in python can receive parameters of two different types: those that invoke the function are assigned by the position they occupy in the parameter list, and those that are assigned by the name of the parameter when invoking.
For example, consider the following function with five parameters. The first two do not have default values, the last three do:
def ejemplo(a, b, c=1, d=0, e=2):
print("He recibido a={}, b={}, c={}, d={}, e={}".format(a,b,c,d,e))
If we invoke it like this: ejemplo(10, 20)
, we will be using the positional method, so that a
will take the value 10
and b
the value 20
. The remaining parameters will take the default value specified in the declaration of the function. We can also invoke it like this: ejemplo(10, 20, 30)
, in that case the 30
will go to c
, since positionally it is the third parameter.
But we can also invoke it like this: ejemplo(b=20, a=10, d=4)
. In this case, the order in the invocation does not matter, since we are using the names of the parameters to specify to which parameter each value goes. Of course, we have to assign values to a
and b
since they have no default value in the declaration of the function.
But there are times when we do not know in advance how many parameters the function will receive. A typical case is when the function in question receives as a parameter another function that must be invoked, together with the parameters that must be passed to that other function. This type of "wrapping" function of another is widely used in decorators.
I'm not going to enter the theme of the wrapping function yet. Instead, just consider that you have a function that can be invoked with a different number of parameters and you do not know in advance how many.
For this Python offers the possibility to declare a single parameter in the function with an asterisk in front. It is customary to call *args
to that parameter, but in reality you can call it as you want. What will happen is that this parameter will be a tuple with all the arguments that you have passed in the function during the call. So, for example:
def ejemplo2(*args):
print("He recibido estos parámetros: ', args)
This function will print the tuple it receives as a parameter. You can invoke that function like this: ejemplo2(1,2,3)
and it will print "He recibido estos parámetros: (1, 2, 3)"
.
However, if you try to call the function using the assign syntax, for example: ejemplo2(a=1, b=2, c=3)
, it will fail, since the asterisk of *args
indicates that this variable will pick up all positional parameters , but will not pick up any non-positional. In fact, the declaration of ejemplo2()
prevents you from passing non-positional parameters.
For the second is the syntax with double asterisk. If you precede a parameter with **
, that parameter will collect all the arguments that have been passed to the function in a non-positional way (with name), in a single data of type dictionary. In that dictionary the keys are the names of the parameters. Typically this other parameter is called **kwargs
, but the name again could be another.
So, the following example:
def ejemplo3(*args, **kwargs):
print("He recibido estos parámetros posicionales:", args)
print("Y estos no posicionales:", kwargs)
It can be invoked in many ways. Let's say: ejemplo3(1, 2, a=20, b=12)
. In that case, the screen would appear:
He recibido estos parámetros posicionales: (1, 2)
Y estos no posicionales: { 'a': 20, 'b': 12 }
To finish completing the puzzle, it only remains to say that the syntax *
or **
can also be used in the invocation of a function to "expand" a tuple or a dictionary and convert it into a series of arguments for move to the function. You can do this by calling to another function regardless of whether that other function has used *
, **
in its declaration or has used the "normal" way of declaring parameters.
Consider, for example, the function ejemplo
of the beginning. I can also invoke it like this:
posicionales = (10, 20)
no_posicionales = { 'c': 30, 'd': 40 }
ejemplo(*posicionales, **no_posicionales)
The first *
will expand the tuple (10, 20)
to fill in two arguments of the function. The next **
will expand the dictionary to provide additional named parameters. The call therefore amounts to:
ejemplo(10, 20, c=30, d=40)
Thanks to this, you can make a function that receives another as a parameter, together with the arguments that must be passed to that other, all without the "wrapper" function knowing in advance the declaration of the "wrapped" function.
Example:
def envoltorio(funcion_a_ejecutar, *args, **kwargs):
print("Ejecuto la función dada")
r = funcion_a_ejecutar(*args, **kwargs)
print("Que ha retornado", r)
return r
And I can use it like this:
envoltorio(ejemplo, 1, 2, d=40)
so that envoltorio
will receive the tuple (1,2)
in args
and the dictionary {'d':40}
in kwargs
, and when you execute the internal function it will expand that tuple and dictionary in the appropriate arguments, so which in this case will execute ejemplo(1,2,d=40)
.