Function Arguments in Julia and Python
Variable number of arguments, default arguments etc
This is not a complicated topic, but it is something very useful to be aware of because it can simplify the way you write code a lot. Variable number of named arguments e.g. gives you the ability to create what looks like domain specific languages.
Once I created a library for specifying GUI layout for Qt widgets, that looked something like this:
Ui(
class = "EmptyForm",
version = "4.0",
root = QWidget(
name = "EmptyForm",
geometry = Rect(0, 0, 120, 120),
windowTitle = "Form"
)
)
The challenge is how do you write code looking something like this:
QPushButton(name = "done", caption = "click me!", enabled = false)
You may not know when creating this code how many attributes some UI component can have, and you don’t really care either. You just want to output them say in a JSON format or XML. This is where variable named arguments comes in handy.
Named Arguments
Here is an example of an attempt at implementing something like this in Julia:
function QPushButton(name = "nothing", caption = "button")
println("name: " name, " caption:", caption)
end
This works for setting up default argument values:
julia> QPushButton()
name: nothing caption:button
However it does not work for named arguments:
julia> QPushButton(name = "egg", caption = "click me")
ERROR: function QPushButton does not accept keyword arguments
Python OTOH interprets this differently.
def QPushButton(name = "nothing", caption = "button"):
print("name: ", name, " caption:", caption)
I have no problems calling this with named arguments:
python> QPushButton()
name: nothing caption: button
python> QPushButton(name = "egg")
name: egg caption: button
However I cannot call with named arguments I did not already name in the argument list:
python> QPushButton(name = "egg", enable = False)
TypeError: QPushButton() got an unexpected keyword argument 'enable'
Before looking at the solution, let us look at how named arguments are handled in Julia. Python is different in that every argument is potentially a named one. In Julia however named arguments have to be separated from the rest with a semicolon ;
.
function QPushButton(name = "noname"; caption = "button", enable = true)
end
In this case name
is a regular argument with default value "noname"
while caption
and enable
are named arguments with default values.
You could call it like this:
QPushButton("name", enable = false, caption = "click me")
The order of unnamed arguments matter but not the order of named arguments.
Variable Number of Arguments
Julia uses ...
as a suffix to indicate variable number of arguments for both normal and named arguments, while Python uses both *
and **
. It needs two symbols because Python does not separate named variables form the rest.
Here is an example of functions returning the arguments as two separate arrays in Julia.
getvalues(args...; kwargs...) = args, kwargs
The names args
and kwargs
are not important. I could have written instead:
getvalues(foo...; bar...) = foo, bar
Strongly speaking these are not arrays, but iterators. But we can Julia’s collect
function to turn them into arrays.
julia> a, d = getvalues(false, 2, "three", four = 4, five="fünf")
((false, 2, "three"), Base.Iterators.Pairs{Symbol,Any,Tuple{Symbol,Symbol},NamedTuple{(:four, :five),Tuple{Int64,String}}}(:four=>4,:five=>"fünf"))
julia> collect(a)
3-element Array{Any,1}:
false
2
"three"
julia> collect(d)
2-element Array{Pair{Symbol,Any},1}:
:four => 4
:five => "fünf"
Notice how collect(d)
gives us an array of pairs. In Julia pairs are formed with key => value
syntax. :four
and :five
are symbols rather than strings. In LISP tradition Julia like LISP, Scheme and Ruby use extensively symbols. A symbol is essentially an immutable string which there are only one of, and which has to be a valid identifier in a programming language. You cannot throw in spaces e.g. Python will tend to use regular strings instead of symbols.
Let us look at the python version. The call looks almost identical:
python> a, d = getvalues(False, 2, "three", four = 4, five="fünf")
However Python does not return some sort of iterators which we need to collect values from. Instead we get a tuple for regular arguments.
python> a
(False, 2, 'three')
python> type(a)
<class 'tuple'>
And a dictionary for the named arguments. Notice the keys are just strings, not symbols.
python> d
{'four': 4, 'five': 'fünf'}
python> type(d)
<class 'dict'>
Unpacking Arrays to Arguments
This is essentially the reverse problem of turning a list of arguments into an array. Instead consider the case where all the arguments are contained within an array, and we want to pass these arguments to a function.
In Julia we need to use ;
when calling the function this way to separate arguments and keyed arguments.
julia> ar, dic = getvalues(a...; d...)
((false, 2, "three"), Base.Iterators.Pairs{Symbol,Any,Tuple{Symbol,Symbol},NamedTuple{(:four, :five),Tuple{Int64,String}}}(:four=>4,:five=>"fünf"))
julia> collect(ar)
3-element Array{Any,1}:
false
2
"three"
julia> collect(dic)
2-element Array{Pair{Symbol,Any},1}:
:four => 4
:five => "fünf"
In Python this is a bit more symmetrical, since using *
and **
makes it easy to distinguish regular and keyed arguments:
python> ar, dic = getvalues(*a, **d)
python> ar
(False, 2, 'three')
python> dic
{'four': 4, 'five': 'fünf'}
List Comprehensions
If you don’t know what a list comprehension is, it is easier to just give an example.
julia> [x*x for x in 1:4]
4-element Array{Int64,1}:
1
4
9
16
You can see that we basically do a for-loop and each element in the loop is squared to give the value of each element in an array. It is almost the same in python:
python> [x*x for x in range(1,5)]
[1, 4, 9, 16]
In fact we can even filter in the same way:
julia> [x*x for x in 1:4 if x != 3]
3-element Array{Int64,1}:
1
4
16
python> [x*x for x in range(1,5) if x != 3]
[1, 4, 16]
But it goes deeper than that, in both Julia and Python the comprehension syntax is not limited to defining lists or arrays. In fact in both cases a generator object is created. It is the generator which provides values to construct the array.
So I e.g. use it in Julia to create an array of pairs to initialize a dictionary:
julia> Dict(Char(64 + i) => i for i in 1:5)
Dict{Char,Int64} with 5 entries:
'C' => 3
'D' => 4
'A' => 1
'E' => 5
'B' => 2
To understand how this works, let us construct a simple function in both Julia and Python to extract the object created by the comprehension.
julia> getobj(c) = c
julia> c = getobj(x*x for x in 1:4 if x != 3)
julia> collect(c)
3-element Array{Int64,1}:
1
4
16
If we check the type, it gets a bit complicated because Julia supports parameterized types:
julia> typeof(c)
Base.Generator{Base.Iterators.Filter{getfield(Main, Symbol("##47#49")),UnitRange{Int64}},getfield(Main, Symbol("##46#48"))}
What this basically says is that a comprehension is of type Generator
, which is Julia's Base
package. Generators support Julia's iteration interface. Anything you want to be able to iterate over in e.g. a for loop, must implement the iterator interface.
julia> v, state = iterate(c)
(1, 1)
julia> v, state = iterate(c, state)
(4, 2)
julia> v, state = iterate(c, state)
(16, 4)
Let me show you have this works in Python, and you will see it follows a very similar logic.
python> def getobj(c): return c
python> c = getobj(x*x for x in range(1,5) if x != 3)
python> c
<generator object <genexpr> at 0x105717de0>
python> list(c)
[1, 4, 16]
Except the Python generators mutates when it is used. Thus it gets exhausted and you cannot call next
on it the way you can call iterate
on the Julia generator:
python> next(c)
Traceback (most recent call last):
File "<input>", line 1, in <module>
next(c)
StopIteration
The state of the iterator is not kept outside in a state object like with Julia. This we need to recreate the generator:
python> c = getobj(x*x for x in range(1,5) if x != 3)
python> next(c)
1
python> next(c)
4
python> next(c)
16
To understand more about the differences I would need to get into generators, but we’ll leave that for next time.