Typing declaring Real numbers and using Vectors as default arguments

In Python I can make a method

from typing import Optional, List
from numbers import Real
def mul(a, m:Optional[List[Real]]=None):
    m = [1,2] if m is None else m
    return a*m[0], a*m[1]

Which allows

>>> mul(2)
(2, 4)
>>> mul(2,[10,20])
(20, 40)
>>> issubclass(int, Real)
True

Here, m has a default value of None which is then made into [1,2]. This is seen as good practice as def mul(a, m = [1,2]): can lead to unpredictable values of m. Additionally, m can take the form of any Real numbers (floats and ints).

In Julia I have

function mul(a, m::Vector{Real}=[1,2])
    return a*m[1], a*m[2]
end

calling

julia> mul(2)
ERROR: MethodError: no method matching mul(::Int64, ::Vector{Int64})
julia> Int64 <: Real
true

This confuses me. So by default m is a Vector{Int64} but which Julia says there’s no method for even though Int64 is a subtype of Real. There are two questions here:

  1. How can I correctly label mul as requiring a vector of real numbers?
  2. In Julia is it safe to have mutable objects as default values? mul(a, m=[1,2])

Vector{S} is not a subtype of Vector{T} even if S <: T. This is documented in the manual: Types · The Julia Language, and you can also find many discussions about it here: Search results for 'invariant type' - JuliaLang The correct function signature for you is:

mul(a, m::Vector{<:Real}=[1,2])
# or
mul(a, m::Vector{T}=[1,2]) where {T<:Real}

The former is just syntactic sugar for the latter.

As for your second question, I’m not sure I understand. Safe in what way? I wouldn’t hesitate to use it, anyway. Why is the python version with None as default preferred?

BTW, since your function anyway returns a tuple, maybe you should allow tuple input?

1 Like

Thank you. I’ve seen functions declared in this way but never really understood what is going on. I’ll implement this.

In place operations can cause unexpected behaviour as the default argument isn’t set each time the function is called, whereas it is when using None as default. Therefore, if mutable objects are the default and they’re changed inplace unexpected things can happen. See this silly example

>>> def l(a=[2]):
...     a.append(-1)
...     print(len(a))
...
>>> l([1])
2
>>> l([1])
2
>>> l()
2
>>> l()
3
>>> l()
4

It seems Julia is safe against this, but I wasn’t sure whether there could be another reason to avoid.

That’s a great point, and another thing for me to learn how to do.

Yes, Julia is safe against this. The default arguments are avaluated at run-time (at least it is guaranteed to behave that way).

mul(a, m=[1,2])

gets translated to

mul(a, m) = ...
mul(a) = mul(a, [1,2])

So you can do some things that Python cannot (I think), like

foo(x, y=2x)

Here, the default second argument depends on the first argument.

1 Like

I must say, I find this pretty terrible. Isn’t this basically a bug? I mean, I know it is expected and probably documented, but it’s so strange and unnatural. It’s like a bug you can’t fix, so you just have to document it as “This doesn’t work right.”

Or does there exist some justification why this could be beneficial?

I didn’t realise you can doo foo(x, y=2x), I like that.

I think mutable default arguments has been discussed a lot this stack is 13 years old and references the official docs, it can also cause things to be a bit of mess if you use lru_cache. In general my python experience has made me very weary of inplace operations, which Julia uses extensively as they have benefits. But I still feel uncomfortable with them.

I’m not sure what the advantage would be, maybe saving a tiny bit of memory but at the cost of breaking “what you see is what you get”