Explanatory Examples - Prolog Calling Python
We discuss some of Janus’ core functionality for Prolog
calling Python via a series of examples. As background,
when janus
is loaded, Python is also loaded
and initialized within the Prolog process, the core Prolog
modules of janus
are loaded into Prolog, and
paths to janus
and its sub-directories are
added both to Prolog and to Python (the latter paths are
added by modifying Python’s sys.path
). Later,
when Prolog calls Python, Python will search for modules
and packages in the same manner as if they were
stand-alone.
Example 1.1. Calling a Python Function (I)
Suppose janus
has been loaded by the
command ?- [janus].
and consider the
call:
py_func(json,prolog_loads('{"name":"Demo term"
"created": {"day":null,
"month":"December",
"year":2007 },
"confirmed":true,
"members":[1,2,3]}'
This call loads the Python json
module
(if needed) and then calls the Python function
json.loads()
with the above JSON string as its argument. In
Python the atom is parsed and converted into a Python
dictionary whose syntax is very close to that of the JSON.
Next, janus
translates this dictionary to a
Prolog term that can be pretty printed as:
{name:'Demo term'),
created:{day:@(none),
month:'December',
year:2007},
confirmed:@(true),
members:[1,2,3]}
The syntactic flexibility of Prolog allows the
Python dictionary to be represented as a logical term
whose syntax is very close to that of both a Python
dictionary and a JSON object. We call a term that maps to
a Python dictionary a (Janus) dictionary term.
Such a term is simply a Prolog term whose outer functor is
'{}'
/1, whose argument is a comma list, where
the elements of the comma list are attribute-value pairs
(sometimes called key-value pairs) designated by the
predicate :/2
. The attributes and values
themselves may contain nested dictionaries, lists, tuples
or sets in accordance with the restrictions of Python
dictionaries.
Example 1.2. Calling a Python Function (II): Where to Maintain Python Objects?
A slightly more complex call to Python loads a JSON object from a file as opposed to a string. For this, we may use the Python function
json.load(Stream)
which loads a JSON string from a file into a Python
dictionary which can then be translated to Prolog. A small
problem arises in that the input to
json.load()
is a Python input stream
(sometimes called a file pointer fp
is Python
documentation). Python input streams are of course
different than Prolog input streams. This can be handled
in several ways.
Maintain The Stream Reference in Python
A straightforward solution to the problem is to write a small amount of glue code in Python as follows.
def prolog_load(File): with open(File) as fileptr: return(json.load(fileptr))
If this code were kept in the file
jns_json.py
the callpy_func(jns_json,prolog_load('sample.json'),Json)
would unify
Json
with ajanus
dictionary term as in the last example.Maintain The Stream Reference in Prolog
janus
also allows the user to obtain any Python object reference in Prolog. The goalpy_func(builtins,open('sample.json',r),Stream), py_func(json,load(Stream),Json), py_dot(Stream,close(),Return).
makes three calls to Python: one to obtain the Python stream as a Python object reference, a second to parse the JSON object from the file and load it into Prolog, and a third to close the stream. Note that closing a stream requires a method to be applied to a stream object rather than a function call, so the
janus
predicatepy_dot/[3,4]
is called instead ofpy_func/[3,4]
. Also, note thatpy_func/[3,4]
uses the pseudo-modulebuiltins
to call standard Python functions.
Each of these approaches has advantages. Maintaining
the stream reference in Prolog requires no glue code, but
does require explicitly opening and closing the file.
Either works well from the viewpoint of performance: the
janus
interface is so fast that making one
call vs. three calls to Python make no measurable
difference (see Section 2.4). Whether to
maintain object references in Python or Prolog is largely
a matter of taste.
Example 1.3. Calling a
Python Function (III): Keyword Arguments
~< Python library functions often make heavy use of
keyword arguments. These are easily handled by
py_func/[3,4]
along with other
janus
functions such as
py_dot/[3,4]
and py_iter/[3,4]
.
Suppose we want to call the Python function
json.dumps(Dict,indent=2)
where Dict
is a Python dictionary that
is to be written out as a JSON string. This can easily be
called via
py_func(json,dumps(PlgDict,indent=2),Ret)
where PlgDict
is a Prolog dictionary
term that is to be converted to the Python dictionary
Dict
. Note that py_func/[3,4]
handles keyword arguments using the same syntax as Python:
positional arguments must occur first in a call followed
by 0, 1 or more keyword arguments.
The janus
predicate py_dot/3
was briefly introduced in
Example 1.2. Let’s take a
closer look at it.
Example 1.4. Calling a Python Method
Consider the following simple Python class:
class Person:
def __init__(self, name, age, ice_cream=None):
self.name = name
self.age = age
if ice_cream is None:
favorite_ice_cream = 'chocolate'
self.ice_cream = favorite_ice_cream
def hello(self,mytype):
return("Hello my name is " + self.name + " and I'm a " + mytype)
The call
py_func('Person','Person'(john,35),Obj),
creates a new instance of the Person
class, and returns a reference to this instance that can
later be used to call a method. We refer to this reference
abstractly as < obj>
as the form of a Python object reference can differ
between Prologs that support janus
. Using
this reference, the goal
py_dot(
< obj>,hello(programmer),Ret2).
returns the Prolog atom:
’Hello my name is john and I’m a programmer’
Note that unlike py_func/[3,4]
which
requires a module as its first argument, the module is not
needed in py_dot/[3,4]
as the module is
implicit in the object reference.
Example 1.5. Examining a Python Object
Example
1.4 showed how to
create a Python object, pass it back to Prolog and apply a
method to it. Suppose we create another
Person
instance:
py_func('Person','Person'(bob,34),Obj),
and later want to find out all attributes of
bob
both explicitly assigned, and default.
This is easily done by
janus:obj_dict/2
:
obj_dict(Obj ,ObjDict ).
returns
ObjDict = {name:bob,age:34,favorite_ice_cream:chocolate}
There are times when using the dictionary
associated with a class is either not possible or not
appropriate. For instance, not all Python classes have
__dict__
methods defined for them, or only a
single attribute of an object might be required. In these
cases, py_dot/4
can be used:
py_dot(
< obj>,favorite_ice_cream,I)
returns I = chocolate
.
Summarizing from the previous two examples,
py_dot/[3,4]
can be used in two ways. If the
second argument of a call to py_dot/4
is a
Prolog structure, the structure is interpreted as a
method. In this case, a Python method is applied to the
object, and its return is unified with the last argument
of py_dot/4
. If the second argument is a
Prolog atom, it is interpreted as attribute of the object.
In this case, the attribute is accessed and returned to
Prolog. Note that the functionality of
py_dot/4
is overloaded in direct analogy to
the functionality of the ’.’
connector in
Python.
Example 1.6. Eager and
Lazy Returns Prolog can either “lazily” backtrack
through solutions to a goal G or “eagerly” return
all solutions to G as a list via
findall/3
or similar predicates. In an
analogous manner, Python can either 1) return a list or
set of returns via a mechanism such as comprehension; or
2) return solutions one at a time through the
yield
statement or similar framework¡.
janus
provides full flexibility in handling
both lazy and eager returns.
Consider a file range.py
that contains
the following functions:
def demo_yield():
for i in range(10):
yield i
def demo_comp():
return [i for i in range(10)]
To improve performance, many Python libraries, such
as SpaCy and the RDF-HDT interface to Wikidata, use
yield
to return generators rather than
returning lists or other data structures. A
demo_yield()
that returns a generator may be
considered a function with lazy returns. while
demo_comp()
may be considered a function with
eager returns.
Eager return to Prolog of a eager Python
function This case reflects the usual behavior of
janus
with most Python functions. An example
is
py_func(range,demo_comp(),Ret).
which unifies Ret
with the list
[0,1,2,3,4,5,6,7,8,9]
. Such goals can be
extremely efficient in janus
due to its
high-speed translation of Python data structures.
Eager return to Prolog of a lazy Python function The goal
py_func(range,demo_yield(),YieldObj).
in fact returns the same ten element list as in the
previous case that used demo_comp()
. The
default behavior of py_func/[3,4]
is that if
a function returns a Python object O that is not of a
type directly handled by bi-translation, O is checked to see
whether it is a generator or has an associated iterator
IO.
If so the generator or iterator traversed to return all
answers eagerly to Prolog.
Lazy return to Prolog of an lazy Python
function If, rather than using
py_func()
with its default behavior, the
goal
py_iter(range,demo_yield(),Return).
is set, Return
will be unified with
the first list element,0
, while the rest of
its answers can be returned via backtracking.
An alternate approach is to call
py_func(range,demo_yield(),Return,[py_object(true)]).
In this case, Return
will be unified
with a Python object if the Python function returns any
non-base Python data type. Returning an explicit object
through which to iterate may be useful of the object is
needed in Prolog for other purposes. Otherwise it is
better to use py_iter()
which does not create
an explicit Python object reference that should be
freed.
Lazy return to Prolog of a eager Python function If the goal
py_iter(range,demo_comp(),Return).
were called, py_iter/3
would lazily
backtrack through the list returned by
demo_comp()
, rather than eagerly returning
the list. Similarly
py_func(range,demo_comp(),Return,[py_object(true)]).
will return a Python object reference as in the immediately previous case.
Example 1.7. py_call/[2,3]
py_call/[2,3]
provides an alternate
syntax for py_dot/[3,4]
and
py_func/[3,4]
(and vice-versa). Rather than
calling
py_func(Module,Function,Return)
one may equivalently call
py_call(Module:Function,Return)
and rather than calling
py_dot(Object,Function,Return)
one may equivalently call
py_call(Object:Function,Return)
These equivalences also hold when options are provided for a call.
The syntax of py_func/[3,4]
and
py_dot/[3,4]
is arguably slightly more
“Pythonic” than py_call/[2,3]
since Python
distinguishes between calling a function and applying a
method or obtaining an attribute: this distinction is
maintained when using py_func/[3,4]
and
py_dot/[3,4]
. On the other hand,
py_call/[2,3]
is arguably slightly more
“Prologic”, since it treats module qualification in the
same manner as with Prolog goals, and does not require the
user to distinguish between Python methods and functions.
The following example shows how py_call/[3.4]
can be used to write concise code.
Example 1.8. Like many languages, Python allows simple functional composition – a simple case might be
>>> make_squares(make_list(4))
which makes a list of the first four integers and then squares each integer in the list producing
>>> [1,4,9,25]
py_call/2
supports a similar form of
recursion, by clothing arguments in eval/1
,
for instance:
?- py_call(test_janus:squares(eval(test_janus:makelist(4))),Res).
unifies Res
with the expected
result.
Compositions of method application to objects is similar. The goal
py_call(returnVal:returnVal({a:b,c:d}),Obj,[py_object(true)]).
unifies Obj
with a reference to the
Python dictionary object for {a:b,c:d}
. Using
this binding, the goal
py_call(Obj:'__class__':'__name__', Name),
first finds the Python class for Obj
and then unifies Name
with its string
representation.
There is no deep difference between
py_call/[2,3]
and the mixture of
py_func/[3,4]
and py_dot/[3,4]
.
They are merely alternate syntaxes. In XSB,
py_call/[2,3]
is defined in terms of
py_func/[3,4]
and py_dot/[3,4]
and so py_func/[3,4]
and
py_dot/[3,4]
are slightly faster; in SWI it
is the reverse. Which form to use is a matter of
taste.