JuliaPy / PythonCall.jl

Python and Julia in harmony.
https://juliapy.github.io/PythonCall.jl/stable/
MIT License
792 stars 64 forks source link

setindex! not defined for np arrays of non-conventional (isbits) types #217

Closed lmiq closed 2 years ago

lmiq commented 2 years ago

Minimum example (using ipython3).

I have a list of tuples Tuple{Int,Int,Float64}, and I can copy the values from the julia array to a np.array of the same type on the python side, but I can't perform this copy of elements on the Julia side, because setindex! is not defined.

Would it be possible to support any isbits type?

In [6]: from juliacall import Main as jl

In [7]: jl.seval("""
   ...: function copy_value(python_list, julia_list)
   ...:     python_list[1] = julia_list[1]
   ...: end
   ...: """)
Out[7]: copy_value (generic function with 1 method)

In [8]: import numpy as np

In [9]: python_list = np.array([(0,0.0)], dtype='i,f')

In [10]: julia_list = jl.seval("[(0,0.0)]")

In [11]: python_list[0] = julia_list[0] # works!

In [12]: jl.copy_value(python_list, julia_list)
---------------------------------------------------------------------------
JuliaError                                Traceback (most recent call last)
<ipython-input-12-e89b29633981> in <module>
----> 1 jl.copy_value(python_list, julia_list)

~/.local/lib/python3.10/site-packages/juliacall/__init__.py in __call__(self, *args, **kwargs)
    199             return ValueBase.__dir__(self) + self._jl_callmethod($(pyjl_methodnum(pyjlany_dir)))
    200         def __call__(self, *args, **kwargs):
--> 201             return self._jl_callmethod($(pyjl_methodnum(pyjlany_call)), args, kwargs)
    202         def __len__(self):
    203             return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(length))))

JuliaError: MethodError: no method matching setindex!(::PythonCall.PyIterable{PythonCall.Py}, ::Tuple{Int64, Float64}, ::Int64)
Stacktrace:
 [1] copy_value(python_list::PythonCall.PyIterable{PythonCall.Py}, julia_list::Vector{Tuple{Int64, Float64}})
   @ Main ./none:3
 [2] pyjlany_call(self::typeof(copy_value), args_::PythonCall.Py, kwargs_::PythonCall.Py)
   @ PythonCall ~/.julia/packages/PythonCall/2Y5CR/src/jlwrap/any.jl:31
 [3] _pyjl_callmethod(f::Any, self_::Ptr{PythonCall.C.PyObject}, args_::Ptr{PythonCall.C.PyObject}, nargs::Int64)
   @ PythonCall ~/.julia/packages/PythonCall/2Y5CR/src/jlwrap/base.jl:69
 [4] _pyjl_callmethod(o::Ptr{PythonCall.C.PyObject}, args::Ptr{PythonCall.C.PyObject})
   @ PythonCall.C ~/.julia/packages/PythonCall/2Y5CR/src/cpython/jlwrap.jl:47
cjdoris commented 2 years ago

Indeed, you can see from the error message that the python_list has been converted to a PyIterable which is the fallback if conversion to PyArray fails. And generic iterables do not support setindex!.

It should indeed be possible to support nested types, but it's fiddly to do in general (e.g. numpy dtypes can use packed or aligned memory layout) so I haven't got around to it yet.

cjdoris commented 2 years ago

You can now convert numpy arrays with structured dtypes like this to PyArray (provided the dtype is aligned). This means your code now works:

In [1]: from juliacall import Main as jl

In [2]: jl.seval("""
   ...: function copy_value(python_list, julia_list)
   ...:   @show python_list julia_list
   ...:   python_list[1] = julia_list[1]
   ...: end
   ...: """)
Out[2]: copy_value (generic function with 1 method)

In [3]: import numpy as np

In [4]: python_list = np.array([(0,0.0)], dtype="i,f")

In [5]: julia_list = jl.seval("[(0,0.0)]")

In [6]: python_list[0] = julia_list[0]

In [7]: jl.copy_value(python_list, julia_list)
python_list = Tuple{Int32, Float32}[(0, 0.0)]
julia_list = [(0, 0.0)]
Out[7]: (0, 0.0)

In [8]: python_list
Out[8]: array([(0, 0.)], dtype=[('f0', '<i4'), ('f1', '<f4')])

In [9]: python_list[0] = (1,1.1)

In [10]: python_list
Out[10]: array([(1, 1.1)], dtype=[('f0', '<i4'), ('f1', '<f4')])

In [11]: jl.copy_value(python_list, julia_list)
python_list = Tuple{Int32, Float32}[(1, 1.1)]
julia_list = [(0, 0.0)]
Out[11]: (0, 0.0)

In [12]: python_list
Out[12]: array([(0, 0.)], dtype=[('f0', '<i4'), ('f1', '<f4')])

Assuming tests pass, I'll make a release today.