JuliaPy / pyjulia

python interface to julia
MIT License
886 stars 101 forks source link

Pass symbol to Julia function #308

Open jdupl123 opened 5 years ago

jdupl123 commented 5 years ago

I am trying to pass a symbol to the hclust function from http://juliastats.github.io/Clustering.jl/stable/hclust.html

from julia import Clustering as C
result = hclust(costs, linkage=":ward")

I keep getting

TypeError: in #hclust, in typeassert, expected Symbol, got String

I also tried

from julia import Main as M
C.hclust(costs, linkage=M.Symbol('ward'))

but same error. I believe this is because PyCall is converting the symbol to a string before passing it through.

Current work around is:

jl = Julia(compiled_modules=False)
M.costs = costs
ctree=jl.eval('hclust(costs, linkage=:ward)')
tkf commented 5 years ago

This is a PyCall issue https://github.com/JuliaPy/PyCall.jl/issues/11

jl = Julia(compiled_modules=False)
M.costs = costs
ctree=jl.eval('hclust(costs, linkage=:ward)')

If you need to call hclust many times, a slightly better workaround than mutating the global variable in Main is to create a (anonymous) function on the Julia side:

hclust = jl.eval("costs -> hclust(costs, linkage=:ward)")
hclust(costs)
jdupl123 commented 5 years ago

Thanks for the quick reply.

manuelbb-upb commented 3 years ago

I came across the same problem when writing a python wrapper for my Julia project. In the project, I'm using several “configuration structs” written with Parameters.jl so that the constructors are quite flexible. These struct have many fields, some of which require Symbol values. Wrapping them in a PyCall.pyfunction or anonymous functions would have resulted in a loss flexibility and would have made matters somewhat complicated.

Instead, I Main.include the following script right after initializing Julia in Python:

PC = PyCall;

PC.py"""
class SymStr():
    def __init__(self, *args, **kwargs):
        self.s = str(*args, **kwargs)
    def __str__(self):
        return self.s.__str__()
    def __repr__(self):
        return f'SymStr("{self.__str__()}")'
"""

sym_str_py_type = PC.py"SymStr";

PC.PyObject( s :: Symbol ) = PC.py"SymStr($(string(s)))"o
function PC.convert( ::Type{Symbol}, po :: PC.PyObject ) 
    sym_str = PC.pyisinstance( po, sym_str_py_type ) ? po.s : po;
    return Symbol(PC.convert(AbstractString, sym_str))
end
PC.pytype_mapping(sym_str_py_type, Symbol);
nothing

This basically follows the proposal in the corresponding PyCall issue (https://github.com/JuliaPy/PyCall.jl/issues/11) by defining a python class to wrap around strings that are meant to be Julia Symbols. I can now do something like Main.MyJuliaConstructor( sym_field = Main.Symbol("sym_val") ). Within Python I can get the string value from a SymStr from the field s or by wrapping it in str().

I don't know whether to open a pull request over at PyCall. Are they still working on an alternative conversion routine?

Edit: Removed single quotes within PC.py"SymStr($(string(s)))"o

yonachache commented 3 years ago

I came across the same problem when writing a python wrapper for my Julia project. In the project, I'm using several “configuration structs” written with Parameters.jl so that the constructors are quite flexible. These struct have many fields, some of which require Symbol values. Wrapping them in a PyCall.pyfunction or anonymous functions would have resulted in a loss flexibility and would have made matters somewhat complicated.

Instead, I Main.include the following script right after initializing Julia in Python:

PC = PyCall;

PC.py"""
class SymStr():
    def __init__(self, *args, **kwargs):
        self.s = str(*args, **kwargs)
    def __str__(self):
        return self.s.__str__()
    def __repr__(self):
        return f'SymStr("{self.__str__()}")'
"""

sym_str_py_type = PC.py"SymStr";

PC.PyObject( s :: Symbol ) = PC.py"SymStr($(string(s)))"o
function PC.convert( ::Type{Symbol}, po :: PC.PyObject ) 
    sym_str = PC.pyisinstance( po, sym_str_py_type ) ? po.s : po;
    return Symbol(PC.convert(AbstractString, sym_str))
end
PC.pytype_mapping(sym_str_py_type, Symbol);
nothing

This basically follows the proposal in the corresponding PyCall issue (JuliaPy/PyCall.jl#11) by defining a python class to wrap around strings that are meant to be Julia Symbols. I can now do something like Main.MyJuliaConstructor( sym_field = Main.Symbol("sym_val") ). Within Python I can get the string value from a SymStr from the field s or by wrapping it in str().

I don't know whether to open a pull request over at PyCall. Are they still working on an alternative conversion routine?

Edit: Removed single quotes within PC.py"SymStr($(string(s)))"o

Is there a way to pass a constructor that has symbols into another function? For example, a = Main.MyJuliaConstructor( sym_field1 = Main.Symbol("sym_val1"), sym_field2 = Main.Symbol("sym_val2" ) and then do 'Main.MyFucntion(a)'?

manuelbb-upb commented 3 years ago

I would have supposed that it should just work. In fact, I guess I am doing something similar in my project (outdated atm): Here I initilize a Julia AlgoConfig with kwargs and reference it in the Python object cfg as self.jlObj. In this line I then call the Julia function optimize and pass the AlgoConfig via cfg.jlObj.

sibyjackgrove commented 2 years ago

@jdupl123 Thank you for the solution. I think using the Main.eval is the best way to go.