Appearance
Embedding Python objects into MeTTa
py-atom
Introducing tokens for grounded atoms allows for both convenient syntax and direct representation of expressions with corresponding grounded atoms in a Space. However, wrapping all functions of rich Python libraries can be not always desirable. There is a way to invoke Python objects such as functions, classes, methods or other statements from MeTTa without additional Python code wrapping these objects into atoms.
py-atom
allows obtaining a grounded atom for a Python object imported from a given module or submodule. Let us consider usage of numpy
as an example, which should be installed. For instance, the absolute
value of a number in MeTTa can be calculated by employing the absolute
function from the numpy
library:
metta
! ((py-atom numpy.absolute) -5) ; 5
Here, py-atom
imports numpy
library and returns an atom associated with the numpy.absolute
function.
It is possible to designate types for the grounded atom in py-atom
. For convenience, one can associate the result of py-atom
with a token using bind!
:
metta
! (bind! abs (py-atom numpy.absolute (-> Number Number)))
! (+ (abs -5) 10) ; 15
We specify here that the constructed grounded operation can accept an argument of type Number
and its result will be of Number
type.
When (abs -5)
is executed, it triggers a call to absolute(-5)
. It can be seen that the results of executing Python objects imported via py-atom
can then be directly utilized in other MeTTa expressions.
py-atom
can actually execute some Python code, which shouldn't be a statement like x = 42
, but should be an expression, which evaluation produces a Python object. In the following example, (py-atom "[1, 2, 3]")
produces a Python list, which then passed to numpy.array
.
metta
! (bind! np-array (py-atom numpy.array))
! (np-array (py-atom "[1, 2, 3]")) ; array([1, 2, 3])
py-atom
can be applied to functions accepting keyword arguments. Constructed grounded atoms will also support Kwargs
(mentioned earlier),
which allows for passing only the required arguments to the function
while skipping arguments with default values. For example, there is numpy.arange
in NumPy, which returns evenly spaced values within a given interval. numpy.arange
can be called with a varying number of positional arguments:
metta
! (bind! np-arange (py-atom numpy.arange)) ; ()
! (np-arange 4) ; array([0, 1, 2, 3])
! (np-arange (Kwargs (step 2) (stop 8))) ; array([0, 2, 4, 6])
! (np-arange (Kwargs (start 2) (stop 10) (step 3))) ; array([2, 5, 8])
py-dot
What if we wish to call functions from a submodule, say numpy.random
? Accessing these functions via something like (py-atom numpy.random.randint)
will work. However, it would be more efficient to get numpy.random
itself as a Python object and access other objects in it. py-dot
is introduced to carry out this operation.
metta
! (bind! np-rnd (py-atom numpy.random))
! ((py-dot np-rnd randint) 25)
In this case py-dot
operates with two arguments: it takes the first argument, which is the
grounded atom wrapping a Python object, and then searches for the value
of an attribute within that object based on the name provided in the
second argument.
This second argument can also contain objects in submodules. In the following example, we wrap numpy
in the grounded atom:
metta
! (bind! np (py-atom numpy))
! ((py-dot np abs) -5)
! ((py-dot np random.randint) -25 0)
! ((py-dot np abs) ((py-dot np random.randint) -25 0))
Here, when (py-dot np random.randint)
is executed, it takes numpy
object and searches for random
in it and then for randint
in random
. The overall result is the grounded operation wrapping numpy.random.randint
, which is then applied to some argument. Similar to py-atom
, py-dot
also permits the designation of types for the function, and supports Kwargs
for arguments specification.
Binding np
to (py-atom numpy)
and accessing functions in it via (py-dot np abs)
looks not more convenient than just using (py-atom numpy.abs)
, but is slightly more efficient if numpy.abs
is accessed multiple times.
py-dot
works for any Python object - not only modules:
metta
! ((py-dot "Hello World" swapcase)) ; "hELLO wORLD"
Notice the additional brackets to call swapcase
. The equivalent Python code is "Hello World".swapcase()
, which also contains ()
. One more pair of brackets in MeTTa is needed, because py-dot
is also a function.
Let us consider another example.
metta
! ((py-dot (py-atom "{5: \'f\', 6: \'b\'}") get) 5)
Here, a dictionary {5: 'f', 6: 'b'}
is created by py-atom
, and then the value corresponding to the key 5
is retrieved from this dictionary using get
accessed via py-dot
.
py-list
, py-tuple
, py-dict
While it is possible to create Python lists and dictionaries using code evaluation by py-atom
, it can be desirable to construct these data structures by combining atoms in MeTTa.
In
this context, since passing dictionaries, lists or tuples as arguments
to functions in Python is very common, such dedicated functions as py-dict
, py-list
and py-tuple
were introduced.
metta
! ((py-atom max) (py-list (-5 5 -3 10 8))) ; 10
! ((py-atom numpy.inner)
(py-list (1 2)) (py-list (3 4))) ; 1 * 3 + 2 * 4 = 11
In this example, py-list
generates three Python lists: [-5, 5, -3, 10, 8]
, [1,2]
and [3,4]
, which are passed to max
and numpy.inner
.
Of course, one can use py-dict
, py-list
, and py-tuple
independently - not just as function arguments:
metta
! (py-dict (("a" "b") ("b" "c"))) ; creates a dict {"a":"b", "b":"c"}
! (py-tuple (1 5)) ; creates a tuple (1, 5)
! (py-list (1 (2 (3 "3")))) ; creates a nested list [1, [2, [3, '3']]]