Is it reasonable to mimic a Python class with mutable structs?

Just beginning to learn Julia. I’ve realized that you can mimic a Python class using a module and mutable structs. For example,

module MimicPyClass

mutable struct Self{T<:AbstractFloat,D<:Integer}
    a::T
    b::T
    n::D
end

function __init__()
    return Self(1.0,2.0,10)
end
self = __init__()
    
function method1(;self=self)
    self.n += 1
    nothing
end

function method2(;self=self)
    return self.a + self.b
end

end
mim = MimicPyClass

println(mim.self.n)
mim.method1()
println(mim.self.n)

out = mim.method2()
println(out)

The only difference is that you need one more layer (which is a mutable struct) between the fake-class and the “attributes”. Is this a reasonable way to construct a larger project, which involves lots of module data which is used in many different methods? Thanks!

You can’t create two instances of this class I think, so why do you want to do this? In my experience it’s good to let go of the wish to have obj.method() syntax. method(obj, args…) is not worse to write (does have worse auto complete, though) and it’s more powerful because other people can extend your functions for their types if you do it correctly. That might not be your priority now, but you benefit from this type of coding all over the ecosystem, so it’s good to get acquainted with this way of thinking

4 Likes

Jeff answered a question on this some time ago here: oop - How to create a "single dispatch, object-oriented Class" in julia that behaves like a standard Java Class with public / private fields and methods - Stack Overflow

5 Likes

You can probably create some awkward version of this in Julia. But in that case you should probably rather use python.

Doing this for a larger project seems wasteful. Why not use idiomatic Julia?

2 Likes

Actually, there is no need to mimic python class, because it is already exists in Julia. In a sense, python classes are subset of Julia capabilities. Compare this two implementations

class PyClass:
   def __init__(self):
      self.a = 1.0
      self.b = 2.0
      self.n = 10

   def method1(self):
      self.n += 1

   def methof2(self):
       return self.a + self.b

mim = PyClass()
mim.method1()
mim.method2()

It can be written as following Julia code:

mutable struct PyClass
   a::Float64
   b::Float64
   n::Int
end

PyClass() = PyClass(0.0, 0.0, 0)
function __init__(self::PyClass)
  self.a = 1.0
  self.b = 2.0
  self.n = 10
end

function method1(self::PyClass)
   self.n += 1
   nothing
end

function method2(self::PyClass)
  return self.a + self.b
end

mim = __init__(PyClass())
method1(mim)
method2(mim)

If you look closely, you’ll see, that there is almost no difference between python and julia definitions. The only observable difference is slight change of syntax, but in a sense, Julia is more consistent.

In python you have
definition: method - class instance - arguments
usage: class instance - dot - method - arguments

In Julia you have
definition: method - class instance - arguments
usage: method - class instance - arguments

So, what am I trying to say, it’s rather easy to move python class code to Julia: you should do the same things, just use different (and more consistent) notation when you are calling methods of these “classes”.

By the way, do you know that you can use Julia style in python?

mim = PyClass()

PyClass.method1(mim)
print(mim.n) # 11

def method3(self):
    print("Hello")

PyClass.method3 = method3

mim.method3()              # prints Hello
PyClass.method3(mim) # prints Hello

It’s just that Julia can bind method to the type of the argument, so there is no need in adding PyClass at the outside method definition.

5 Likes

Completely agree with @Skoffer here. Just a small thing:

currently returns 10, you should put a return self at the end to get the instance back.

Also, if you desparately need __init__ to work like it does in Python, you can pretend that it is your constructor with:

julia> mutable struct PyClass2
          a::Float64
          b::Float64
          n::Int
          PyClass2() = __init__(new())
       end

julia> function __init__(self::PyClass2)
         self.a = 1.0
         self.b = 2.0
         self.n = 10
         return self
       end

julia> mim = PyClass2()
PyClass2(1.0, 2.0, 10)

Although personally, I find the idiomatic julian way to be much more readable and clear. Feel free to drop __init__ and never look back!

4 Likes

Uh, just don’t put a function named __init__ into a module or you might get some very strange behaviour. __init__() is called whenever a module is loaded.

7 Likes

Thanks so much for all these responses! All very useful. DNF, and several others said something like

Why not use idiomatic Julia?

I’ve read the style guide but I’m still having issues imagining how to do a big project using idiomatic Julia. Is there a project on Github, that is relatively simple and understandable (!), which will give me a sense for how to use idiomatic Julia?

Some background: My research group uses a model of atmospheric chemistry which was originally written in Fortran 77. I’ve tried to improve it by moving to Fortran 90, but it is still a bit of a disaster because it rests on a weak foundation of the original code. I think I want to propose, for a post-doc project, to re-write the photochemical model from the ground up in Julia. But before starting, I’d like to have a very solid understanding of how to do things best/fastest in Julia. Perhaps the end product should work something like this below? Advice and thoughts are welcome.

using PhotoChem
prob = PhotoProblem("inputfile1.yaml","inputfile2.yaml","inputfile3.yaml")
sol1 = steady_state(prob) # Find steady state of ODEs using the steady-state solvers in DifferentialEquations.jl
sol2 = integrate(prob,[0.0,100.0]) # ODE integration from 0 to 100 s using DifferentialEquations.jl

Just a tiny side-note, which might also shed some light on how things are working in Python:

>>> class Foo:
...     def greet(self, name):
...         print(f"servus {name}")

>>> f = Foo()

>>> f.greet("Tom")
servus Tom

>>> Foo.greet(f, "Tom")
servus Tom

Here, you can see single dispatch in action and Foo (the class itself) is basically just a namespace, so to say ;)

2 Likes

Yes, that looks like the right idea. If you have a python class which looks like:

# foo.py

class Bar:
  def __init__(self, a):
    self.a = a
    self.b = a + 1
  def do_something(self, c):
    print(self.b + c)

then your Julia code would look like:

module Foo

export Bar, do_something

struct Bar  # could also be a `mutable struct` if you want to be able to mutate `a` and `b` later
  a::Int  
  b::Int  
end
# By default, the `struct` definition creates a convenient constructor 
# which lets you do `Bar(a, b)`. Since our Python code had a constructor 
# which took just `a`, we can create another constructor matching
# that signature:

function Bar(a)
  Bar(a, a + 1)
end

function do_something(bar::Bar, c)
  println(bar.b + c)
end

end
6 Likes

It may be difficult to get a solid understanding “before starting”. So I’d suggest to start by formulating some specific small to middle-sized real problem and try to solve it in Julia, asking spesific questions here as you go. You could then even call this Julia computation from your current Python wrapper package.

On a general note, mimicking Python objects is technically to a large extent possible, and this is what many of those who are/were about to migrate a Python software to Julia would be initially planning (me too), before finding out its not worth it.

5 Likes

Just another awkward version :grinning:

mutable struct Foo
   a::Int
   obj::Foo
   inc_a::Function
   function Foo(a)
      x = new()
      x.a = a

      function inc_a(i)
         x.a += i
         return nothing
      end

      x.inc_a = inc_a
      x.obj = x
   end
end
julia> f1 = Foo(1); f2 = Foo(2);
julia> f1.a
1
julia> f2.a
2
julia> f1.inc_a(10); f1.a
11
julia> f2.inc_a(20); f2.a
22

By the way - how can I specify the type of the field inc_a ?
EDIT: see the next post.

I am not sure this is possible, unless you meant Function (you probably can annotate it as ::Function), but to annotate it with the exact type you need to get the type of the closure after it is lowered and externalized. Luckily, they will be closures of the same type:

julia> f(x) = y -> (x + y)
f (generic function with 1 method)

julia> g = f(10)
#1 (generic function with 1 method)

julia> h = f(11)
#1 (generic function with 1 method)

julia> typeof(g) === typeof(h)
true

julia> typeof(h)
var"#1#2"{Int64}

Yes, this worked, thank you

I think that learning by doing is the best way. Start coding, ask questions here (like you did), occasionally revisit code and refactor.

3 Likes

For this, I really recommend reading lots of Base code (ie GitHub - JuliaLang/julia: The Julia Programming Language). This has two advantages: First, it gives you lots of additional understanding how the standard library works; second, this is the canonical example for “idiomatic” code (ie how the language is meant to work – literally, and bidirectionally: Not just is Base written to work well with the language primitives, the language primitives and compiler are also written to work well for Base).

Sure you won’t read all of it; sure it ain’t all perfect, and many things can be done simpler for smaller projects that don’t aim for that level of generality and API stability, and don’t have the issue of participating in the bootstrap process. But many parts are relatively self-contained, and shockingly readable.

If you want more self-contained, stdlib is also attractive: This gives a feel for smaller codebases, instead of sprawling behemoths like Base. GitHub - JuliaGraphs/LightGraphs.jl: An optimized graphs package for the Julia programming language is also a pretty contained, well-designed and readable project.

For your specific project, you probably rely on the DiffEq ecosystem more than julia-the-language. So you should maybe read code from your direct upstream or siblings (other packages that implement specific models). I have no idea about readability or internal code quality of these projects, though. You might consider shooting at-ChrisRackauckas a message on slack.

3 Likes

Checking my Zotero bookmarks I came across this post: Workflow for converting Python scripts - #3 by tamasgal

1 Like