2

I'm trying to interface with a python library via PyCall.jl where the library returns a python object (PyObject in Julia) with attributes I want to modify in Julia. For example say I have the following dummy python class,

import numpy as np

class MyNumpy:
     def __init__(self,n,m):
          self.array = np.zeros((n,m))
          self.size = (n,m)

Now in Julia I load this python class using PyCall.jl and instantiate, something like:

using PyCall

mynumpy = pyimport("MyNumpy.MyNumpy")
pyobject = mynumpy(3,3)
...

> pyobject.array
> 3×3 Array{Float64,2}:
  0.0  0.0  0.0
  0.0  0.0  0.0
  0.0  0.0  0.0
...

pyobject.array[1,1] = 1.0 

> pyobject.array
> 3×3 Array{Float64,2}:
  0.0  0.0  0.0
  0.0  0.0  0.0
  0.0  0.0  0.0

The last line of code executes without any error, however upon investigation of the pyobject.array[1,1] the value has not changed (i.e., remains 0.0).

How would one accomplish changing a Pycall.jl PyObject attribute value in Julia, for example, can I use pointers do this, if so how? Sorry if this is obvious but I've had no luck and can't figure out how to do so using the PyCall.jl documentation. Thanks in advance.

P.S. The actual python library is not something that can be easily modified.

S_B
  • 159
  • 1
  • 6

1 Answers1

3

PyCall defaults to converting objects to Julia types if they quack appropriately. In this case, it's happening when you access the array field of your MyNumpy class: it returns a numpy array, which PyCall will convert it to a Julian Array at the boundary. If you want to opt out of that auto-conversion, you can use the uglier, dot-access with a string:

julia> py"""
       import numpy as np

       class MyNumpy:
            def __init__(self,n,m):
                 self.array = np.zeros((n,m))
                 self.size = (n,m)
       """

julia> mynumpy = py"MyNumpy"
PyObject <class '__main__.MyNumpy'>

julia> pyobject = mynumpy(3,3)
PyObject <__main__.MyNumpy object at 0x1383398d0>

julia> pyobject.array # converted (copied!) into a Julian Array
3×3 Array{Float64,2}:
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> pyobject."array" # This is the "raw" numpy array!
PyObject array([[0., 0., 0.],
       [0., 0., 0.],
       [0., 0., 0.]])

Now, you can work within Python's list-of-lists representation, but it's pretty annoying; the API isn't the greatest and you have to remember the 0-based, row major implementation. PyCall has a nice, convenient helper that exposes the array as shared memory through a Julian AbstractArray:

julia> array = PyArray(pyobject."array")
3×3 PyArray{Float64,2}:
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> array[1,1] = 1.0
1.0

julia> array
3×3 PyArray{Float64,2}:
 1.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> pyobject.array # remember, this is a copy
3×3 Array{Float64,2}:
 1.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0
mbauman
  • 30,958
  • 4
  • 88
  • 123
  • Thanks for the response. I'm not sure this address my main challenge as this just instantiates the PyObject, but does necessarily guarantees being able to modify elements of the PyObject's attribute that may be a nested list or numpy array. – S_B Nov 04 '19 at 23:08
  • Ah, the conversion isn't happening on your `MyNumpy`, but it's happening when you access `.array`. I had thought your MWE was a bit too minimal... I've updated my answer. – mbauman Nov 04 '19 at 23:44
  • Thank you very much, this is helpful. Now my issue is that PyArray can only handle numerical types and the pyobject attribute I'm actually dealing with needs to be PyArray{Any,2}... ```This implements a nocopy wrapper to a NumPy array (currently of only numeric types only).``` – S_B Nov 05 '19 at 00:21
  • The `PyArray` wrapper isn't necessary — it just makes your life a bit easier. You can use the direct Python API: `(pyobject."array").__setitem__((0,0), 1.0)` – mbauman Nov 05 '19 at 14:47
  • Thanks @MattB. this is exactly what I needed, much appreciated! – S_B Nov 06 '19 at 17:01