pythonnumpyjuliapyobjectpycall

Modify a Python object's attribute when using PyCall.jl in Julia


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.


Solution

  • 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