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

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

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

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

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.

Written by

Geek dad, living in Oslo, Norway with passion for UX, Julia programming, science, teaching, reading and writing.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store