Skip to content

Truthiness of Julia callables in Python #323

Closed
@dingraha

Description

@dingraha

Is your feature request related to a problem? Please describe.
In my application I'd like to pass a Julia callable to a Python library. The Python library checks if the callable is passed (it's an optional argument) with something like:

if f:
    # f is here, use it
else:
    # do something else

When I try to pass a Julia callable I get an error about __len__ not being implemented. I think this is because, in Python, the truthiness of a class is controlled by the __bool__ method, if present, and the __len__ method if not, and then finally is always True if the class implements neither. In PythonCall, a Julia function is treated as an AnyValue in Python, which doesn't implement __bool__ but does implement __len__. But the AnyValue __len__ eventually calls the Julia length function, which isn't implemented for Julia functions and triggers an error.

I believe in Python a plain function is always considered True.

Here is an example of a Julia REPL session that shows the error:

julia> is_it_truthy = PythonCall.@pyeval `lambda f: "yes!" if f else "no!"`
Python: <function <lambda> at 0x7fb2c9799ee0>

julia> is_it_truthy(8)
Python: 'yes!'

julia> is_it_truthy(0)
Python: 'no!'

julia> is_it_truthy("foo")
Python: 'yes!'

julia> is_it_truthy("bar")
Python: 'yes!'

julia> is_it_truthy("")
Python: 'no!'

julia> myfunc(x) = x+1
myfunc (generic function with 1 method)

julia> myfunc(8)
9

julia> is_it_truthy(myfunc)
ERROR: Python: TypeError: Julia: MethodError: no method matching length(::typeof(myfunc))
Closest candidates are:
  length(!Matched::Union{Base.KeySet, Base.ValueIterator}) at abstractdict.jl:58
  length(!Matched::Union{LinearAlgebra.Adjoint{T, S}, LinearAlgebra.Transpose{T, S}} where {T, S}) at ~/local/julia/1.8.5/share/julia/st
dlib/v1.8/LinearAlgebra/src/adjtrans.jl:172
  length(!Matched::Union{Tables.AbstractColumns, Tables.AbstractRow}) at ~/.julia/packages/Tables/AcRIE/src/Tables.jl:180
  ...
Python stacktrace:
 [1] __len__
   @ ~/projects/pythoncall_openmdao/dev/PythonCall.jl/src/jlwrap/any.jl:210
 [2] <lambda>
   @ REPL[14]:1:1
Stacktrace:
 [1] pythrow()
   @ PythonCall ~/projects/pythoncall_openmdao/dev/PythonCall.jl/src/err.jl:94
 [2] errcheck
   @ ~/projects/pythoncall_openmdao/dev/PythonCall.jl/src/err.jl:10 [inlined]
 [3] pycallargs(f::PythonCall.Py, args::PythonCall.Py)
   @ PythonCall ~/projects/pythoncall_openmdao/dev/PythonCall.jl/src/abstract/object.jl:210
 [4] pycall(f::PythonCall.Py, args::Function; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
   @ PythonCall ~/projects/pythoncall_openmdao/dev/PythonCall.jl/src/abstract/object.jl:228
 [5] pycall
   @ ~/projects/pythoncall_openmdao/dev/PythonCall.jl/src/abstract/object.jl:218 [inlined]
 [6] #_#11
   @ ~/projects/pythoncall_openmdao/dev/PythonCall.jl/src/Py.jl:341 [inlined]
 [7] (::PythonCall.Py)(args::Function)
   @ PythonCall ~/projects/pythoncall_openmdao/dev/PythonCall.jl/src/Py.jl:341
 [8] top-level scope
   @ REPL[22]:1

shell> 

Describe the solution you'd like
It would be cool if a Julia function/callable could be considered True in Python, similar to Python functions.

Describe alternatives you've considered
I have a workaround where I wrap the Julia function I'm passing to Python in a Julia struct, then implement the Julia length function for that as something like Base.length(::MyStruct) = 1. That works fine, but it would be great if I didn't have to do it. :-)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions