ipyjulia_hacks¶
Horrible hacks for Julia-IPython integration¶
Features¶
- Julia’s Multimedia I/O hooked into IPython’s display system
- Code completion inside Julia magic (by monkey-patching IPython)
@asyncworks in Jupyter (Julia’s event loop is integrated to ipykernel’s asyncio event loop)printworks in Jupyter (Julia’s standard streams are integrated to ipykernel’s I/O)- Syntax highlighting works in
%%juliamagic ofipythonCLI (but not in Jupyter) - Copy-free access to Julia objects from Python
Those are build on top of the great libraries PyCall.jl and PyJulia. (It would be nice to merge some features to PyJulia at some point. But I wanted to do some experiments on Python interface for handling Julia objects.)
API¶
Pythonic wrapper of Julia objects.
>>> from ipyjulia_hacks import get_api
>>> jlapi = get_api()
Mutables:
>>> spam = jlapi.eval('''Base.eval(Module(), quote
... mutable struct Spam
... egg
... end
... Spam(1)
... end)''')
>>> spam.egg
1
>>> spam.egg = 2
>>> spam.egg
2
Numbers:
>>> one = jlapi.eval("1", wrap=True)
>>> one
<JuliaObject 1>
>>> one // 2 # translated to ``1 ÷ 2``, *not* ``1 // 2``
0
>>> assert one == 1
>>> assert one != 0
>>> assert one > 0
>>> assert one >= 1
>>> assert one < 2
>>> assert one <= 1
>>> assert one
Arrays:
>>> a2d = jlapi.eval("reshape((1:6) .- 1, (2, 3))")
>>> a2d
<JuliaObject [0 2 4; 1 3 5]>
>>> a2d[0, 1]
2
>>> jlapi.eval("[1, 2, 3]")
array([1, 2, 3], dtype=int64)
>>> arr = jlapi.eval("[1, 2, 3]", wrap=True)
>>> list(reversed(arr))
[3, 2, 1]
Linear algebra:
>>> jlapi.eval("import LinearAlgebra")
>>> I = jlapi.eval("LinearAlgebra.I")
>>> M = jlapi.eval("reshape(1:6, 2, 3)")
>>> Y = M @ I
>>> Y
array([[1, 3, 5],
[2, 4, 6]], dtype=int64)
>>> import numpy
>>> M @ numpy.ones(3)
array([ 9., 12.])
Named tuple:
>>> nt = jlapi.eval("(a = 1, b = 2)")
>>> nt.a
1
>>> nt.b
2
>>> nt[0]
1
>>> nt[1]
2
>>> len(nt)
2
>>> {"a", "b"} <= set(dir(nt))
True
Dictionary:
>>> dct = jlapi.eval('Dict("b" => 2)')
>>> dct["a"] = 1
>>> del dct["b"]
>>> dct["a"]
1
>>> dct
<JuliaObject Dict("a"=>1)>
>>> dct == {"a": 1}
True
Three-valued logic:
>>> true = jlapi.eval("true", wrap=True)
>>> false = jlapi.eval("false", wrap=True)
>>> missing = jlapi.eval("missing")
>>> true
<JuliaObject true>
>>> false
<JuliaObject false>
>>> true & missing
<JuliaObject missing>
>>> false & missing
False
>>> true | missing
True
>>> false | missing
<JuliaObject missing>
>>> true ^ false
True
>>> true ^ true
False
>>> true ^ missing
<JuliaObject missing>
>>> false ^ false
False
-
ipyjulia_hacks.get_api(*args, **kwargs)¶ Initialize
JuliaAPI.Positional and keyword arguments are passed directly to
julia.Julia>>> from ipyjulia_hacks import get_api >>> get_api(jl_runtime_path="PATH/TO/CUSTOM/JULIA") <JuliaAPI ...>
-
ipyjulia_hacks.get_cached_api()¶ Get pre-initialized
JuliaAPIinstance orNoneif not ready.>>> from ipyjulia_hacks import get_cached_api >>> jlapi = get_cached_api() >>> jlapi.eval("1 + 1") 2
-
class
ipyjulia_hacks.core.JuliaAPI(eval_str, api)¶ Julia-Python interface.
-
start_repl(*, banner=False, history_file=True, **kwargs)¶ Start Julia REPL.
-
eval(code, wrap=None, **kwargs)¶ Evaluate
codeinMainscope of Julia.Parameters: code (str) – Julia code to be evaluated.
Keyword Arguments: Examples
>>> from ipyjulia_hacks import get_api >>> jlapi = get_api()
By default, most of Julia objects returned by this function are the the Python wrapper
JuliaObject. This object just has a reference to the object held by Julia so that passing it back to Julia is easy. However, you can suppress this behavior by passingwrap=False. For example:>>> _ = jlapi.eval("dct = Dict()") >>> dct_jl = jlapi.eval("dct") >>> dct_py = jlapi.eval("dct", wrap=False) >>> dct_jl <JuliaObject Dict{Any,Any}()> >>> dct_py {} >>> assert isinstance(dct_py, dict) >>> dct_jl["a"] = 1 >>> dct_py["b"] = 2 >>> jlapi.eval("dct") <JuliaObject Dict{Any,Any}("a"=>1)>
Note that
dctobject (living in Julia’sMain) does not have the key"b". This is becausedct_pyis a copy of the original Julia object.Some objects such as
Arrayare not wrapped byJuliaObjectby default. JuliaArrayis automatically converted tonumpy.ndarrayin a copy-free manner by PyCall.jl:>>> _ = jlapi.eval("xs = [1, 2, 3]") >>> xs = jlapi.eval("xs") >>> xs array([1, 2, 3], dtype=int64) >>> xs[0] = 100 >>> jlapi.eval("xs") array([100, 2, 3], dtype=int64)
-
getattr(obj, name)¶ Get attribute (property) named
nameof a Julia objectobj.
-
-
class
ipyjulia_hacks.core.JuliaMain(julia)¶ An interface to Julia’s
Mainnamespace.-
eval¶ An alias to
JuliaAPI.eval.
-
import_(module)¶ Run
import <module>in an anonymous module and return<module>.
-
Demos¶
- Using Plots.jl etc. inside IPython Jupyter kernel: Notebook
Using ForwardDiff from Python¶
>>> from ipyjulia_hacks import get_main, jlfunction
>>> @jlfunction
... def f(xs):
... return sum(xs * 2)
>>> Main = get_main()
>>> ForwardDiff = Main.import_("ForwardDiff")
>>> ForwardDiff.gradient(f, [0.0, 1.0])
array([2., 2.])
Fake asyncio integration¶
Executable scripts of the following examples can be fond in examples directory.
Simple example¶
async def main():
ajl = AsyncJuliaAPI()
print(await ajl.eval("1"))
try:
await ajl.eval('error("oops!")')
except RuntimeError as err:
print("Expected exception:")
print(err)
else:
raise AssertionError("No exception!")
Interleaving Python and Julia “background” tasks¶
Suppose you have a Python coroutine that waits for I/O (here just
calling asyncio.sleep) most of the time:
async def py_repeat(name, num):
for i in range(num):
print(name, "i =", i)
await asyncio.sleep(0.1)
return f"{name} done"
and its Julia equivalent:
def jl_repeat(ajl, name, num):
return ajl.eval(rf"""
for i in 1:{num}
print("{name} i = $i\n")
# Using `print` instead of `println` here to force Julia to write
# everything "at once".
sleep(0.1)
end
return "{name} done"
""")
Then you can interleave the execution of those tasks in the event loop
of asyncio:
async def main():
loop = asyncio.get_event_loop()
ajl = AsyncJuliaAPI()
tasks, _ = await asyncio.wait(map(loop.create_task, [
jl_repeat(ajl, "Julia [A]", 2),
jl_repeat(ajl, "Julia [B]", 5),
py_repeat("Python [A]", 2),
py_repeat("Python [B]", 5),
]))
for t in tasks:
print(await t)
This should output something like:
Julia [A] i = 1
Julia [B] i = 1
Python [A] i = 0
Python [B] i = 0
Julia [A] i = 2
Julia [B] i = 2
Python [A] i = 1
Python [B] i = 1
Julia [B] i = 3
Python [B] i = 2
Python [B] i = 3
Julia [B] i = 4
Python [B] i = 4
Julia [B] i = 5
Julia [B] done
Python [B] done
Python [A] done
Julia [A] done