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) # alternativeIt 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) :: PyObjectPython 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