Advanced Usage

Expression Representation

Basic aspects of expression representation are covered in basic usages.

Contexts

In the Wolfram Language, symbols live in contexts, which serve as a namespace mechanism. Built-in functions belong to the System` context. User-defined symbols and functions are stored by default in the Global` context.

Wolfram Language expressions are represented in Python using the attributes of the factory wl:

>>> from wolframclient.language import wl
>>> wl.Range(3)
Range[3]

The attributes of the factory wl do not have a context attached:

>>> wl.myFunction(1)
myFunction[1]

The representation of a given expression using its string InputForm is readable but ambiguous since the context is resolved in the kernel during evaluation, and as such different kernels may return different results. See $ContextPath.

On the other hand, in the WXF format, expressions are represented with their full names. Context must be fully specified for all symbols, the exception being the System` context, which can be omitted. As a consequence, in WXF, a symbol with no context is always deserialized as a System` symbol.

The method evaluate() accepts two types of input:

  • Python strings are treated as a string InputForm; as such, context is resolved during evaluation. User-defined functions are most of the time automatically created with the Global` context.
  • Serializable Python objects are serialized to WXF before evaluation; as such, context must be explicitly specified, except for System` symbols.

System Context

In order to explicitly represent a system symbol in Python, the factory System can be used. First import it:

>>> from wolframclient.language import System

Create a Python object representing the built-in function Classify:

>>> System.Classify
System`Classify

Global Context

User-defined functions and variables are associated to the Global` context by default. The factory Global can be used to represent those symbols:

>>> from wolframclient.language import Global
>>> Global.f
Global`f

Arbitrary Contexts

The factory wl can be used to build symbols with arbitrary context, as well as subcontexts:

>>> wl.Developer.PackedArrayQ
Developer`PackedArrayQ

>>> wl.Global.f
Global`f

>>> wl.System.Predict
System`Predict

>>> wl.MyContext.MySubContext.myFunction
MyContext`MySubContext`myFunction

Use Cases

Create a new function max that takes a list of strings and returns the longer ones. The function definition is:

max[s:List[__String]] := MaximalBy[s, StringLength]

Set up a new local evaluator session:

>>> from wolframclient.evaluation import WolframLanguageSession
>>> from wolframclient.language import wl, Global, wlexpr
>>> session = WolframLanguageSession()

Most of the time, it is much more convenient to define functions using an InputForm string expression rather than wl. Define the function max for the current session:

>>> session.evaluate(wlexpr('max[s : List[__String]] := MaximalBy[s, StringLength]'))

Apply function Global.max to a list of strings:

>>> session.evaluate(Global.max(['hello', 'darkness', 'my', 'old', 'friend']))
['darkness']

Trying to evaluate wl.max, which is the undefined symbol System`max, leads to an unevaluated expression:

>>> session.evaluate(wl.max(['hello', 'darkness', 'my', 'old', 'friend']))
max[['hello', 'darkness', 'my', 'old', 'friend']]

It is important to understand that wlexpr() applied to a string is equivalent to evaluating ToExpression on top of the string input, and as such some context inference is performed when evaluating. In the above example, the max function’s explicit name is Global`max. When the Python object wl.max is passed to evaluate(), it is serialized to WXF first, which has strict context specification rules; the only context that can be omitted is the System` context. As a consequence, any symbol without context is attached to the System` context; max is thus System`max, which is not defined.

Finally, terminate the session:

>>> session.stop()

Local Kernel Evaluation

The following sections provide executable demonstrations of the local evaluation features of the client library.

Note

all examples require a local Wolfram Engine installed in the default location.

Evaluation Methods

Synchronous

Initialize a session:

>>> from wolframclient.evaluation import WolframLanguageSession
>>> from wolframclient.language import wlexpr
>>> session=WolframLanguageSession()

Expressions involving scoped variables are usually more easily represented with wlexpr(). Compute an integral:

>>> session.evaluate(wlexpr('NIntegrate[Sqrt[x^2 + y^2 + z^2], {x, 0, 1}, {y, 0, 1}, {z, 0, 1}]'))
0.9605920064034617

Messages may be issued during evaluation. By default, the above evaluation methods log error messages with severity warning. It usually results in the message being printed out. It is also possible to retrieve both the evaluation result and the messages, wrapped in an instance of WolframKernelEvaluationResult, by using evaluate_wrap():

>>> eval = session.evaluate_wrap('1/0')
>>> eval.result
DirectedInfinity[]

Messages are stored as tuples of two elements, the message name and the formatted message:

>>> eval.messages
[('Power::infy', 'Infinite expression Power[0, -1] encountered.')]

Asynchronous

Some computations may take a significant time to finish. The library provides various form of control on evaluations.

Evaluate Future

Evaluation methods all have a future-based counterpart:

>>> from wolframclient.evaluation import WolframLanguageSession
>>> session = WolframLanguageSession()
>>> future = session.evaluate_future('Pause[3]; 1+1')

The future object is immediately returned; the computation is done in the background. Return the evaluated expression:

>>> future.result()
2

Sometimes a fine control over the maximum duration of an evaluation is required. TimeConstrained ensures that a given evaluation duration is not exceeding a timeout in seconds. When the timeout is reached, the symbol $Aborted is returned.

Wrap an artificially long evaluation to last at most one second:

>>> long_eval = wl.Pause(10)
>>> timeconstrained_eval = wl.TimeConstrained(long_eval, 1)

Evaluate the time-constrained expression:

>>> result = session.evaluate(timeconstrained_eval)

Check if the result is $Aborted:

>>> result.name == '$Aborted'
True

Terminate the session:

>>> session.terminate()
Concurrent Future

Sometimes, the result is not required immediately. Asynchronous evaluation is a way to start evaluations on a local kernel as a background task, without blocking the main Python execution. Asynchronous evaluation methods are evaluate_future(), evaluate_wrap_future() and evaluate_wxf_future(). They wrapped the evaluation result into a Future object. Result is the one that would be returned by the non-future method (e.g evaluate_future() returns the result of evaluate()).

Evaluate an artificially delayed code (using Pause), and print the time elapsed at each step:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from time import perf_counter
from wolframclient.evaluation import WolframLanguageSession

with WolframLanguageSession() as session:
    start = perf_counter()
    print('Starting an evaluation delayed by 2 seconds.')
    future = session.evaluate_future('Pause[2]; 1+1')
    print('After %.04fs, the code is running in the background, Python execution continues.' %
          (perf_counter()-start))
    # wait for up to 5 seconds.
    expr = future.result(timeout = 5)
    print('After %.02fs, result was available. Kernel evaluation returned: %s' 
      % (perf_counter()-start, expr))

The standard output should display:

Starting an evaluation delayed by 2 seconds.
After 0.0069s, the code is running in the background, Python execution continues.
After 2.02s, result was available. Kernel evaluation returned: 2
Coroutine and Asyncio APIs

asyncio provides high-level concurrent code and asynchronous evaluation using coroutines and the async/await keywords. Asynchronous evaluation based on asyncio requires an instance of WolframLanguageAsyncSession, whose methods are mostly coroutines.

Define a coroutine delayed_evaluation that artificially delays evaluation, using an asyncio sleep coroutine. Use this newly created coroutine to evaluate a first expression, wait for the coroutine to finish and evaluate the second:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import asyncio
import time
from wolframclient.evaluation import WolframLanguageAsyncSession
from wolframclient.language import wl

async def delayed_evaluation(delay, async_session, expr):
    await asyncio.sleep(delay)
    return await async_session.evaluate(expr)

async def main():
    async with WolframLanguageAsyncSession() as async_session:
        start = time.perf_counter()
        print('Starting two tasks sequentially.')
        result1 = await delayed_evaluation(1, async_session, wl.Range(3))
        # Compute the Total of the previous evaluation result:
        result2 = await delayed_evaluation(1, async_session, wl.Total(result1))
        print('After %.02fs, both evaluations finished returning: %s, %s'
            % (time.perf_counter()-start, result1, result2))

# python 3.5+
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

# python 3.7+
# asyncio.run(main())

The timer printed in the standard output indicates that the total evaluation took roughly two seconds, which is the expected value for sequential evaluations:

Starting two tasks sequentially.
After 2.04s, both evaluations finished returning: [1, 2, 3], 6

When coroutines can be evaluated independently one from each other, it is convenient to run them in parallel. Start two independent coroutines in a concurrent fashion and wait for the result:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import asyncio
import time
from wolframclient.evaluation import WolframLanguageAsyncSession
from wolframclient.language import wl

async def delayed_evaluation(delay, async_session, expr):
    await asyncio.sleep(delay)
    return await async_session.evaluate(expr)

async def main():
    async with WolframLanguageAsyncSession() as async_session:
        start = time.perf_counter()
        print('Running two tasks concurrently.')
        task1 = asyncio.ensure_future(delayed_evaluation(1, async_session, '"hello"'))
        task2 = asyncio.ensure_future(delayed_evaluation(1, async_session, '"world!"'))
        # wait for the two tasks to finish
        result1 = await task1
        result2 = await task2
        print('After %.02fs, both evaluations finished returning: %s, %s'
              % (time.perf_counter()-start, result1, result2))

# python 3.5+
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

# python 3.7+
# asyncio.run(main())

The total evaluation took roughly one second, indicating that both delayed coroutines ran in parallel:

Running two tasks concurrently.
After 1.03s, both evaluations finished returning: hello, world!

In the examples shown, only one Wolfram kernel was used, which is a single-threaded process. Evaluating two computation-heavy Wolfram Language expressions in parallel will have no impact on performance. This requires more than one kernel, which is exactly what kernel pool was designed for.

Kernel Pool

A WolframEvaluatorPool starts up a certain amount of evaluators and dispatches work load to them asynchronously. The pool is usable right after the first one has successfully started. Some may take more time to start and become available after a delay:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import asyncio
import time
from wolframclient.evaluation import WolframEvaluatorPool

async def main():
    async with WolframEvaluatorPool() as pool:
        start = time.perf_counter()
        tasks = [
            pool.evaluate('Pause[1]')
            for i in range(10)
            ]
        await asyncio.wait(tasks)
        print('Done after %.02fs, using up to %i kernels.' 
            % (time.perf_counter()-start, len(pool)))

# python 3.5+
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

# python 3.7+
# asyncio.run(main())

Evaluation output shows that if more than one evaluator was started, the total time is less than ten seconds:

Done after 3.04s, using up to 4 kernels.
parallel_evaluate

It is possible to evaluate many expressions at once using parallel_evaluate(). This method starts a kernel pool and uses it to compute expressions yield from an iterable object. The pool is then terminated.

Import the function:

>>> from wolframclient.evaluation import parallel_evaluate

Build a list of ten delayed $ProcessID expressions, which returns the kernel process identified (pid) after one second:

>>> expressions = ['Pause[1]; $ProcessID' for _ in range(10)]

Evaluate in parallel and get back a list of ten pid values:

>>> parallel_evaluate(expressions)
[72094, 72098, 72095, 72096, 72099, 72097, 72094, 72098, 72095, 72096]

The result varies, but the pattern remains the same—namely, at least one process was started, and each process is eventually used more than once.

Logging

Logging is often an important part of an application. The library relies on the standard logging module and exposes various methods to control the level of information logged.

The first level of control is through the logging module itself. The Python library logs at various levels. Set up a basic configuration for the logging module to witness some messages in the standard output:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from wolframclient.evaluation import WolframLanguageSession
import logging

# set the root level to INFO
logging.basicConfig(level=logging.INFO)

try:
    session = WolframLanguageSession()
    # this will trigger some log messages with the process ID, the sockets
    # address and the startup timer.
    session.start()
    # Warning: Infinite expression Power[0, -1] encountered.
    res = session.evaluate('1/0')
finally:
    session.terminate()

The standard output should display:

INFO:wolframclient.evaluation.kernel.kernelcontroller:Kernel writes commands to socket: <Socket: uri=tcp://127.0.0.1:61335>
INFO:wolframclient.evaluation.kernel.kernelcontroller:Kernel receives evaluated expressions from socket: <Socket: uri=tcp://127.0.0.1:61336>
INFO:wolframclient.evaluation.kernel.kernelcontroller:Kernel process started with PID: 54259
INFO:wolframclient.evaluation.kernel.kernelcontroller:Kernel 54259 is ready. Startup took 1.74 seconds.

It is also possible to log from within the kernel. This feature is disabled by default. When initializing a WolframLanguageSession, the parameter kernel_loglevel can be specified with one of the following values to activate kernel logging: logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR.

Note

if a WolframLanguageSession is initialized with the default kernel_loglevel (i.e. logging.NOTSET), kernel logging is disable for the session, and it is not possible to activate it afterward.

From the Wolfram Language, it is possible to issue log messages using one of the following functions, given with its signature:

(* Sends a log message to Python with a given log level *)
ClientLibrary`debug[args__]
ClientLibrary`info[args__]
ClientLibrary`warn[args__]
ClientLibrary`error[args__]

The log level of the kernel is independent of the Python one. The following functions can be used to restrict the amount of log data sent by the kernel:

(* Sends only messages of a given level and above *)
ClientLibrary`SetDebugLogLevel[]
ClientLibrary`SetInfoLogLevel[]
ClientLibrary`SetWarnLogLevel[]
ClientLibrary`SetErrorLogLevel[]
(* Sends no message at all *)
ClientLibrary`DisableKernelLogging[]

Control the log level at both the Python and the kernel levels:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from wolframclient.evaluation import WolframLanguageSession
import logging

# set the Python root logger level to INFO
logging.basicConfig(level=logging.INFO)

# Start a new session, with kernel logging activated and log level set to INFO.
with WolframLanguageSession(kernel_loglevel=logging.INFO) as session:
    # This message is printed
    session.evaluate('ClientLibrary`info["ON -- Example message printed from the kernel \
with log level INFO --"]')
    # This one is not because its level is debug. 
    session.evaluate('ClientLibrary`debug["OFF -- Debug message."]')
    
    # Disable logging from the kernel side
    session.evaluate('ClientLibrary`DisableKernelLogging[]')
    # These messages will not be sent to Python.
    session.evaluate('ClientLibrary`fatal["OFF -- Fatal message. Not printed"]')
    session.evaluate('ClientLibrary`info["OFF -- End of kernel evaluation. Not printed"]')

Extending Serialization: Writing an Encoder

Serialization of a Python object involves encoders, which convert an input object into a stream of bytes. The library defines encoders for most built-in Python types and for some core libraries. It stores a mapping between types and encoder implementation. In order to serialize more classes, new encoders must be registered.

An encoder is a function of two arguments, the serializer and an object, associated with a type. The object is guaranteed to be an instance of the associated type.

Register a new encoder for a user-defined class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from __future__ import absolute_import, print_function, unicode_literals

from wolframclient.language import wl
from wolframclient.serializers import export, wolfram_encoder

# define a new class.
class Animal(object):
    pass

# register a new encoder for instances of the Animal class.
@wolfram_encoder.dispatch(Animal)
def encode_animal(serializer, animal):
    # encode the class as a symbol called Animal
    return serializer.encode(wl.Animal)

# create a new instance
animal = Animal()
# serialize it
result = export(animal)
print(result) # b'Animal'

During export, for each object to serialize, the proper encoder is found by inspecting the type hierarchy (field __mro__). First, check that an encoder is associated with the object type; if not, repeat with the first parent type until one is found. The default encoder, associated with object, is used as the last resort.

Register some encoders for a hierarchy of classes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from wolframclient.language import wl
from wolframclient.serializers import export, wolfram_encoder

# define a hierarchy of classes.
class Animal(object):
    pass

class Fish(Animal):
    pass

class Tuna(Fish):
    pass

# will not have its own encoder.
class Salmon(Fish):
    pass

# register a new encoder for Animal.
@wolfram_encoder.dispatch(Animal)
def encode_animal(serializer, animal):
    return serializer.encode(wl.Animal)

# register a new encoder for Fish.
@wolfram_encoder.dispatch(Fish)
def encode_fish(serializer, animal):
    return serializer.encode(wl.Fish)

# register a new encoder for Tuna.
@wolfram_encoder.dispatch(Tuna)
def encode_tuna(serializer, animal):
    # encode the class as a function using class name
    return serializer.encode(wl.Tuna)


expr = {'fish' : Fish(), 'tuna': Tuna(), 'salmon': Salmon()}
result = export(expr)
print(result) # b'<|"fish" -> Fish, "tuna" -> Tuna, "salmon" -> Fish|>'

Note: the encoder for Animal is never used, not even for the instance of Salmon, because Fish has a dedicated encoder, and type Fish appears first in the method resolution order of type Salmon:

>>> Salmon.__mro__
(<class '__main__.Salmon'>, <class '__main__.Fish'>, <class '__main__.Animal'>, <class 'object'>)

Extending WXF Parsing: Writing a WXFConsumer

Integer Eigenvalues

Use the Wolfram Client Library to access the Wolfram Language algebra functions. Compute the integer Eigenvalues on a Python matrix of integers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from wolframclient.evaluation import WolframLanguageSession
from wolframclient.language import wl

with WolframLanguageSession() as session:
    # define a matrix of integers
    array = [
        [-1, 1, 1], 
        [1, -1, 1], 
        [1, 1, -1]]
    # expression to evaluate
    expr = wl.Eigenvalues(array)
    # send expression to the kernel for evaluation.
    res = session.evaluate(expr)
    print(res)  # [-2, -2, 1]

Complex Eigenvalues

Python has the built-in class complex. By default, the function binary_deserialize() deserializes Wolfram Language functions using a generic class WLFunction but conveniently provides a way to extend the mapping. Define ComplexFunctionConsumer, a subclass of WXFConsumer that overrides the method build_function(). The subclassed method maps Complex to the built-in Python class complex.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from wolframclient.evaluation import WolframLanguageSession
from wolframclient.language import wl
from wolframclient.deserializers import WXFConsumer, binary_deserialize

class ComplexFunctionConsumer(WXFConsumer):
    """Implement a consumer that maps Complex to python complex types."""
    
    # represent the symbol Complex as a Python class
    Complex = wl.Complex

    def build_function(self, head, args, **kwargs):
        # return a built in complex if head is Complex and argument length is 2.
        if head == self.Complex and len(args) == 2:
            return complex(*args)
        # otherwise delegate to the super method (default case).
        else:
            return super().build_function(head, args, **kwargs)

with WolframLanguageSession() as session:
    array = [
        [0, -2, 0], 
        [1, 0, -1], 
        [0, 2, 0]]
    # expression to evaluate
    expr = wl.Eigenvalues(array)
    # send expression to the kernel for evaluation.
    wxf = session.evaluate_wxf(expr)
    # get the WXF bytes and parse them using the complex consumer:
    complex_result = binary_deserialize(
        wxf, 
        consumer=ComplexFunctionConsumer())
    print(complex_result)  # [2j, -2j, 0]

Symbolic Eigenvalues

A Python-Heavy Approach

Sometimes the resulting expression of an evaluation is a symbolic exact value, which nonetheless could be approximated to a numerical result. The eigenvalues of 3x3 Matrix. If this image does not display, it might be that your browser does not support the SVG image format. are Eigenvalue of the matrix. If this image does not display, it might be that your browser does not support the SVG image format., Eigenvalue of the matrix. If this image does not display, it might be that your browser does not support the SVG image format. and Eigenvalue of the matrix. If this image does not display, it might be that your browser does not support the SVG image format..

It is possible to build a subclass of WXFConsumer that can convert a subset of all Wolfram Language symbols into pure built-in Python objects. It has to deal with Plus and Times and converts Pi to math.pi, Rational to fractions.Fraction and Complex to complex. It results in a significant code inflation but provides a detailed review of the extension mechanism. However, as will be shown, this is not really necessary:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
from wolframclient.evaluation import WolframLanguageSession
from wolframclient.language import wl
from wolframclient.deserializers import WXFConsumer, binary_deserialize

import math
import fractions

# define Complex symbol once and for all
Complex = wl.Complex

class MathConsumer(WXFConsumer):
    """Implement a consumer with basic arithmetic operation."""
    
    # Specific convertion for Pi, other symbols use the default method.
    def consume_symbol(self, current_token, tokens, **kwargs):
        # Convert symbol Pi to its numeric value as defined in Python
        if current_token.data == 'Pi':
            return math.pi
        else:
            return super().consume_symbol(current_token, tokens, **kwargs)

    # Associate heads with the method to convert them to Python types.
    DISPATCH = {
        Complex: 'build_complex',
        wl.Rational: 'build_rational',
        wl.Plus: 'build_plus',
        wl.Times: 'build_times'
    }
    # Overload the method that builds functions. 
    def build_function(self, head, args, **kwargs):
        # check if there is a specific function associated to the function head
        builder_func = self.DISPATCH.get(head, None)
        if builder_func is not None:
            try:
                # get the class method and apply it to the arguments.
                return getattr(self, builder_func)(*args)
            except Exception:
                # instead of failing, fallback to default case.
                return super().build_function(head, args, **kwargs)
        # heads not listed in DISPATCH are delegated to parent's method
        else:
            return super().build_function(head, args, **kwargs)

    def build_plus(self, *args):
        total = 0
        for arg in args:
            total = total + arg
        return total

    def build_times(self, *args):
        total = 1
        for arg in args:
            total = total * arg
        return total

    def build_rational(self, *args):
        if len(args) != 2:
            raise ValueError('Rational format not supported.')
        return fractions.Fraction(args[0], args[1])
    
    def build_complex(self, *args):
        if len(args) != 2:
            raise ValueError('Complex format not supported.')
        return complex(args[0], args[1])

with WolframLanguageSession() as session:
    array = [
        [wl.Pi, -2, 0], 
        [1, wl.Pi, -1],
        [0, 2, wl.Pi]]

    # expression to evaluate: Eigenvalues[array]
    expr = wl.Eigenvalues(array)

    # Eigenvalues are exact, but the result is a symbolic expression:
    # [Times[Rational[1, 2], Plus[Complex[0, 4], Times[2, Pi]]], 
    # Times[Rational[1, 2], Plus[Complex[0, -4], Times[2, Pi]]], Pi]
    print(session.evaluate(expr))
    
    # Use evaluate_wxf to evaluate without deserializing the result.
    wxf = session.evaluate_wxf(expr)
    # deserialize  using the math consumer:
    complex_result = binary_deserialize(wxf, consumer=MathConsumer())
    # get a numerical result, only made of built-in Python types.
    # [(3.141592653589793+2j), (3.141592653589793-2j), 3.141592653589793]
    print(complex_result)

A Wolfram Language Alternative

It is recommended to delegate as much as possible to the Wolfram Language. Instead of implementing a (fragile) counterpart of core functions such as Plus or Times, it is best to compute a numerical result within the kernel. This can be achieved with the function N. Once applied to the eigenvalues, the result becomes a mixture of complex values and reals, which was already dealt with in the previous section:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from wolframclient.evaluation import WolframLanguageSession
from wolframclient.language import wl
from wolframclient.deserializers import WXFConsumer, binary_deserialize

# represent the symbol Complex as a Python class
Complex = wl.Complex

class ComplexFunctionConsumer(WXFConsumer):
    """Implement a consumer that maps Complex to python complex types."""
    def build_function(self, head, args, **kwargs):
        if head == Complex and len(args) == 2:
            return complex(*args)
        else:
            return super().build_function(head, args, **kwargs)

with WolframLanguageSession() as session:
    array = [
        [wl.Pi, -2, 0], 
        [1, wl.Pi, -1],
        [0, 2, wl.Pi]]

    # expression to evaluate: N[EigenValues[array]]
    expr = wl.N(wl.Eigenvalues(array))
    
    # evaluate without deserializing
    wxf = session.evaluate_wxf(expr)
    # deserialize using the math consumer:
    complex_result = binary_deserialize(wxf, consumer=ComplexFunctionConsumer())
    # [(3.141592653589793+2j), (3.141592653589793-2j), 3.141592653589793]
    print(complex_result)