PyBase.jl
TL;DR
It takes just one line (per type) to define a Python interface:
using MyModule: MyType
using PyCall
using PyBase
PyCall.PyObject(self::MyType) = PyBase.UFCS.wrap(self)
# PyCall.PyObject(self::MyType) = PyBase.Plain.wrap(self) # alternative
It defines an interface for MyType
which can be imported from Python by from julia.MyModule import MyType
via PyJulia.
Overview
Suppose you have a Julia type and want to expose its API to the Python world:
julia> mutable struct MyType
x::Number
end
julia> add1!(self::MyType) = self.x += 1;
julia> add2(self::MyType) = self.x + 2;
julia> add3(self::MyType) = self.x + 3
add3!(self::MyType) = self.x += 3;
Using PyBase
, it is just a one line to wrap it into a Python interface:
julia> using PyCall
using PyBase
julia> PyCall.PyObject(self::MyType) = PyBase.UFCS.wrap(self);
Now MyType
is usable from Python. Here, for a demonstration purpose, let's send it to Python namespace using PyCall's @py_str
macro:
julia> py"""
MyType = $MyType # emulating "from julia.MyModule import MyType"
"""
Python users can now use MyType
as if it is a Python function.
julia> py"""
obj = MyType(0)
assert obj.x == 0
"""
Since MyType
uses PyBase.UFCS.wrap
, Julia functions taking MyType
as the first argument can be called as if it was a Python method of MyType
:
julia> py"""
obj.add1()
assert obj.x == 1
"""
julia> py"""
assert obj.add2() == 3
assert obj.x == 1
"""
julia> py"""
assert obj.add3(inplace=False) == 4
assert obj.x == 1
"""
julia> py"""
obj.add3() # default to inplace=True
assert obj.x == 4
"""
It may be useful to provide custom Python methods. This can be done by overloading PyBase.__getattr__
:
julia> PyBase.__getattr__(self::MyType, ::Val{:explicit_method}) =
(arg) -> "explicit method call with $arg"
julia> py"""
assert obj.explicit_method(123) == "explicit method call with 123"
"""
Note that various Julia interfaces are automatically usable from Python. For example, indexing just works by translating indices as the offsets from Base.firstindex
(which keeps 0-origin in Python side):
julia> Base.firstindex(obj::MyType) = 1;
julia> Base.getindex(obj::MyType, i::Integer) = i;
julia> py"""
assert obj[0] == 1
"""
Pre-defined wrapper factories
"Uniform Function Call Syntax"
PyBase.UFCS
— Module.Uniform Function Call Syntax [UFCS] wrapper.
Usage:
PyCall.PyObject(self::MyModule.MyType) = PyBase.UFCS.wrap(self)
It then translates Python method call self.NAME(*args, **kwargs)
to
MyModule.MyType.NAME(self, args...; kwargs...) # or
MyModule.MyType.NAME!(self, args...; kwargs...)
in Julia where NAME
is a placeholder (i.e., it can be a method named solve
or plot
). If both NAME
and NAME!
exist, NAME!
is used unless False
is passed to the special keyword argument inplace
, i.e.,
# in Python: # in Julia:
self.NAME(*args, **kwargs) # => NAME(self, args...; kwargs...)
# or NAME!(self, args...; kwargs...)
self.NAME(*args, inplace=False, **kwargs) # => NAME(self, args...; kwargs...)
self.NAME(*args, inplace=True, **kwargs) # => NAME!(self, args...; kwargs...)
The modules in which method NAME
is searched can be specified as a keyword argument to PyBase.UFCS.wrap
:
using MyBase, MyModule
PyCall.PyObject(self::MyType) = PyBase.UFCS.wrap(self,
modules = [MyBase, MyModule])
The first match in the list of modules is used.
Usual method overloading of
PyBase.__getattr__(self::MyModule.MyType, name::Symbol) # and/or
PyBase.__getattr__(self::MyModule.MyType, ::Val{name})
can still control the behavior of Python's __getattr__
if name
is not already handled by the above UFCS mechanism.
PyBase.UFCS.wrap
— Function.PyBase.UFCS.wrap(self; methods, modules) :: PyObject
"Uniform Function Call Syntax" wrapper. See PyBase.UFCS
for details.
Plain
PyBase.Plain
— Module.Plain wrapper.
Usage:
PyCall.PyObject(self::MyModule.MyType) = PyBase.Plain.wrap(self)
PyBase.Plain.wrap
— Function.PyBase.Plain.wrap(self) :: PyObject
Python interface methods
PyBase
provides interface for defining special methods for Python data model. Note that these methods do not follow Julia's convention that mutating functions have to end with !
to avoid extra complication of naming transformation.
PyBase.__getattr__
— Function.PyBase.__getattr__(self, ::Val{name::Symbol})
PyBase.__getattr__(self, name::Symbol)
Python's attribute getter interface __getattr__
.
Roughly speaking, the default implementation is:
__getattr__(self, name::Symbol) = __getattr__(self, Val(name))
__getattr__(self, ::Val{name}) = getproperty(self, name)
To add a specific Python property to self
, overload __getattr__(::MyType, ::Val{name})
. Use __getattr__(::MyType, ::Symbol)
to fully control the attribute access in Python.
At the lowest level (which is invoked directly by Python), PyBase
defines __getattr__(self, name::String) = __getattr__(self, Symbol(name))
this can be used for advanced control when using predefined wrappers. However, __getattr__(::Shim{MyType}, ::String)
has to be overloaded instead of __getattr__(::MyType, ::String)
.
PyBase.__setattr__
— Function.PyBase.__setattr__(self, ::Val{name::Symbol}, value)
PyBase.__setattr__(self, name::Symbol, value)
Python's attribute setter interface __setattr__
. Default implementation invokes setproperty!
. See PyBase.__getattr__
.
PyBase.__delattr__
— Function.PyBase.delattr(self, ::Val{name::Symbol})
PyBase.delattr(self, name::Symbol)
Python's attribute deletion interface __delattr__
. Default implementation raises AttributeError
in Python. See PyBase.__getattr__
.
PyBase.__dir__
— Function.Python's __dir__
PyBase.convert_itemkey
— Function.PyBase.convert_itemkey(self, key::Tuple)
Often __getitem__
, __setitem__
and __delitem__
can be directly mapped to getindex
, setindex!
and delete!
respectively, provided that the key/(multi-)index is mapped correctly. This mapping is done by convert_itemkey
. The default implementation does a lot of magics (= run-time introspection) to convert Python/Numpy-like semantics to Julia-like semantics.
PyBase.__getitem__
— Function.__getitem__(self, key)
Python's __getitem__
. Default implementation is:
getindex(self, convert_itemkey(self, key)...)
PyBase.__setitem__
— Function.__setitem__(self, key, value)
Python's __setitem__
. Default implementation is:
setindex!(self, value, convert_itemkey(self, key)...)
Note that the order of key
and value
for __setitem__
is the opposite of setindex!
.
PyBase.__delitem__
— Function.__delitem__(self, key)
Python's __delitem__
. Default implementation is:
delete!(self, convert_itemkey(self, key)...)
PyBase.__add__
— Function.__add__(self, other)
Python's __add__
. Default implementation is roughly: broadcast(+, self, other)
.
PyBase.__radd__
— Function.__radd__(self, other)
Python's __radd__
. Default implementation: PyBase.__add__(other, self)
.
PyBase.__iadd__
— Function.__iadd__(self, other)
Python's __iadd__
. If self
is mutated, PyBase.mutated
must be returned. Default implementation:
function __iadd__(self, other)
broadcast!(+, self, self, other)
return PyBase.mutated
end