Using functions loaded via pycall (kwargs)

I think this might be an easy thing but currently I don’t know how to do
this. I managed to import my python code and can call it but part
of the functions require me to do something like f(x=a, y=c, **rest) and as
Julia handles positional arguments differently I don’t know what to do really.
I tried f(x, y, rest…) and f(x, y, PyDict(rest)…) but to no avail.

The next idea I had was to put everything into a python block:

py"""
def py_test(x):
    print(x)
    return
"""
py"py_test(noisy_data)"

but here the issue is that “noisy_data” which I generated in Julia is not
in the namespace. Is there a way to get access to Julia variables on that level?

Can you give an explicit minimal example of a Python function signature you are trying to call?

2 Likes

Sounds like you want to interpolate your variable, i.e. use py"py_test($noisy_data)". For example,

julia> d = Dict("a" => rand(5), "b" => 0);

julia> py"py_test($d)"
{'b': 0, 'a': array([0.57195721, 0.89319442, 0.96402566, 0.50358093, 0.19589507])}
1 Like

I wouldn’t do it that way. Instead, I would just assign your Python function to a Julia variable and then call it normally as you would a Julia function:

julia> py_test = py"py_test"
PyObject <function py_test at 0x1151bc160>

julia> py_test(3)
3

julia> x = 4
4

julia> py_test(x)
4

Thanks that works. Should have thought of that!

I would do that except I don’t know how kwargs from python translate to
Julia. The function call unpacks a dictionary via the ** operator in python
into the function. I need to replicate that on the Julia level if I am not missing something.

Julia has keyword arguments too, which you can also splat:

julia> py"""
       def foo(x, y):
           return 2*x + 3*y
       """

julia> foo = py"foo"
PyObject <function foo at 0x1675680d0>

julia> foo(3, 4) # call as positional
18

julia> foo(y=4, x=3) # call as keywords
18

julia> args = (; x=3, y=4)
(x = 3, y = 4)

julia> foo(; args...) # splat keywords
18
2 Likes

Do you want something like

julia> py"""
       def py_f(a=1, b=2, **kwargs):
           if "c" in kwargs:
               return a + b + kwargs["c"]
           return a + b
       """

julia> f = py"py_f"
PyObject <function py_f at 0x00000243056A2440>

julia> f(1, 2)
3

julia> f(1, 2; c=3)
6

Edit: or

julia> py"""
       def py_g(x, *, a, b, **rest):
           return x + 10 * a + 100 * b + 1000 * rest.get("c", 0) + 10_000 * rest.get("d", 0)
       """

julia> g = py"py_g"
PyObject <function py_g at 0x00000243056A27A0>

julia> g(1; d=5, a=2, b=3)
50321

?

2 Likes

This would indeed be nicer. Thanks for your suggestions. I think it did teach me some things. Sadly with the function at hand it doesn’t work. I think I tried almost all permutations of your suggestions.

fit_result = fit_model.fit(data=ydata, x=xdata, **fit_model.estimate(ydata, xdata))

This is the call signature of the python function. It is an lmfit model ( from lmfit the python package). ydata and xdata are python arrays and fit_model.estimate returns a dictionary

Dict{Any, Any} with 4 entries:
  "offset"    => PyObject <Parameter 'offset', value=-0.00018379672152062232, bounds=[-1.0019158522526124:1.009325529…  "phase"     => PyObject <Parameter 'phase', value=-0.04986655005698104, bounds=[-3.141592653589793:3.14159265358979…  "frequency" => PyObject <Parameter 'frequency', value=0.15873015873015872, bounds=[0:5.000000000000018]>
  "amplitude" => PyObject <Parameter 'amplitude', value=1.0056206910907135, bounds=[0:4.022482764362854]>

I am not sure if I want to invest much more time into the problem as the other way may not be so clean but it works, so I am kind of happy!.

The problem is probably that a dictionary mapping strings to values is not allowed for keyword arguments in Julia — if you want to splat, it needs to be something like a NamedTuple, or a dictionary mapping symbols to values.

One option here would be to simply pass the keyword arguments explicitly rather than trying to splat them:

estimate = fit_model.estimate(ydata, xdata)
fit_result = fit_model.fit(data=ydata, x=xdata, offset=estimate["offset"], amplitude=estimate["amplitude"])

Another option would be to make sure you treat the returned dictionary as having symbols for keys. You can do this via the lower-level function pycall, which lets you specify the desired function return type. For example:

estimate = pycall(fit_model.estimate, PyDict{Symbol,PyAny}, ydata, xdata)
fit_result = fit_model.fit(; data=ydata, x=xdata, estimate...)

The basic lesson here is that you need to understand how Python types relate to Julia types, and you sometimes need to specify the desired type conversion explicitly rather than relying on PyCall’s auto-conversion.

(Another package, PythonCall.jl, never auto-converts Python objects to native Julia types at all, so you always have to convert explicitly.)

2 Likes

Wow good spot there! Never would have figured that out. Now it works!