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:

 1from time import perf_counter
 2from wolframclient.evaluation import WolframLanguageSession
 3
 4with WolframLanguageSession() as session:
 5    start = perf_counter()
 6    print('Starting an evaluation delayed by 2 seconds.')
 7    future = session.evaluate_future('Pause[2]; 1+1')
 8    print('After %.04fs, the code is running in the background, Python execution continues.' %
 9          (perf_counter()-start))
10    # wait for up to 5 seconds.
11    expr = future.result(timeout = 5)
12    print('After %.02fs, result was available. Kernel evaluation returned: %s' 
13      % (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:

 1import asyncio
 2import time
 3from wolframclient.evaluation import WolframLanguageAsyncSession
 4from wolframclient.language import wl
 5
 6async def delayed_evaluation(delay, async_session, expr):
 7    await asyncio.sleep(delay)
 8    return await async_session.evaluate(expr)
 9
10async def main():
11    async with WolframLanguageAsyncSession() as async_session:
12        start = time.perf_counter()
13        print('Starting two tasks sequentially.')
14        result1 = await delayed_evaluation(1, async_session, wl.Range(3))
15        # Compute the Total of the previous evaluation result:
16        result2 = await delayed_evaluation(1, async_session, wl.Total(result1))
17        print('After %.02fs, both evaluations finished returning: %s, %s'
18            % (time.perf_counter()-start, result1, result2))
19
20# python 3.5+
21loop = asyncio.get_event_loop()
22loop.run_until_complete(main())
23
24# python 3.7+
25# 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:

 1import asyncio
 2import time
 3from wolframclient.evaluation import WolframLanguageAsyncSession
 4from wolframclient.language import wl
 5
 6async def delayed_evaluation(delay, async_session, expr):
 7    await asyncio.sleep(delay)
 8    return await async_session.evaluate(expr)
 9
10async def main():
11    async with WolframLanguageAsyncSession() as async_session:
12        start = time.perf_counter()
13        print('Running two tasks concurrently.')
14        task1 = asyncio.ensure_future(delayed_evaluation(1, async_session, '"hello"'))
15        task2 = asyncio.ensure_future(delayed_evaluation(1, async_session, '"world!"'))
16        # wait for the two tasks to finish
17        result1 = await task1
18        result2 = await task2
19        print('After %.02fs, both evaluations finished returning: %s, %s'
20              % (time.perf_counter()-start, result1, result2))
21
22# python 3.5+
23loop = asyncio.get_event_loop()
24loop.run_until_complete(main())
25
26# python 3.7+
27# 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:

 1import asyncio
 2import time
 3from wolframclient.evaluation import WolframEvaluatorPool
 4
 5async def main():
 6    async with WolframEvaluatorPool() as pool:
 7        start = time.perf_counter()
 8        tasks = [
 9            pool.evaluate('Pause[1]')
10            for i in range(10)
11            ]
12        await asyncio.wait(tasks)
13        print('Done after %.02fs, using up to %i kernels.' 
14            % (time.perf_counter()-start, len(pool)))
15
16# python 3.5+
17loop = asyncio.get_event_loop()
18loop.run_until_complete(main())
19
20# python 3.7+
21# 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:

 1from wolframclient.evaluation import WolframLanguageSession
 2import logging
 3
 4# set the root level to INFO
 5logging.basicConfig(level=logging.INFO)
 6
 7try:
 8    session = WolframLanguageSession()
 9    # this will trigger some log messages with the process ID, the sockets
10    # address and the startup timer.
11    session.start()
12    # Warning: Infinite expression Power[0, -1] encountered.
13    res = session.evaluate('1/0')
14finally:
15    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:

 1from wolframclient.evaluation import WolframLanguageSession
 2import logging
 3
 4# set the Python root logger level to INFO
 5logging.basicConfig(level=logging.INFO)
 6
 7# Start a new session, with kernel logging activated and log level set to INFO.
 8with WolframLanguageSession(kernel_loglevel=logging.INFO) as session:
 9    # This message is printed
10    session.evaluate('ClientLibrary`info["ON -- Example message printed from the kernel \
11with log level INFO --"]')
12    # This one is not because its level is debug. 
13    session.evaluate('ClientLibrary`debug["OFF -- Debug message."]')
14    
15    # Disable logging from the kernel side
16    session.evaluate('ClientLibrary`DisableKernelLogging[]')
17    # These messages will not be sent to Python.
18    session.evaluate('ClientLibrary`fatal["OFF -- Fatal message. Not printed"]')
19    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:

 1from __future__ import absolute_import, print_function, unicode_literals
 2
 3from wolframclient.language import wl
 4from wolframclient.serializers import export, wolfram_encoder
 5
 6# define a new class.
 7class Animal(object):
 8    pass
 9
10# register a new encoder for instances of the Animal class.
11@wolfram_encoder.dispatch(Animal)
12def encode_animal(serializer, animal):
13    # encode the class as a symbol called Animal
14    return serializer.encode(wl.Animal)
15
16# create a new instance
17animal = Animal()
18# serialize it
19result = export(animal)
20print(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:

 1from wolframclient.language import wl
 2from wolframclient.serializers import export, wolfram_encoder
 3
 4# define a hierarchy of classes.
 5class Animal(object):
 6    pass
 7
 8class Fish(Animal):
 9    pass
10
11class Tuna(Fish):
12    pass
13
14# will not have its own encoder.
15class Salmon(Fish):
16    pass
17
18# register a new encoder for Animal.
19@wolfram_encoder.dispatch(Animal)
20def encode_animal(serializer, animal):
21    return serializer.encode(wl.Animal)
22
23# register a new encoder for Fish.
24@wolfram_encoder.dispatch(Fish)
25def encode_fish(serializer, animal):
26    return serializer.encode(wl.Fish)
27
28# register a new encoder for Tuna.
29@wolfram_encoder.dispatch(Tuna)
30def encode_tuna(serializer, animal):
31    # encode the class as a function using class name
32    return serializer.encode(wl.Tuna)
33
34
35expr = {'fish' : Fish(), 'tuna': Tuna(), 'salmon': Salmon()}
36result = export(expr)
37print(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:

 1from wolframclient.evaluation import WolframLanguageSession
 2from wolframclient.language import wl
 3
 4with WolframLanguageSession() as session:
 5    # define a matrix of integers
 6    array = [
 7        [-1, 1, 1], 
 8        [1, -1, 1], 
 9        [1, 1, -1]]
10    # expression to evaluate
11    expr = wl.Eigenvalues(array)
12    # send expression to the kernel for evaluation.
13    res = session.evaluate(expr)
14    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.

 1from wolframclient.evaluation import WolframLanguageSession
 2from wolframclient.language import wl
 3from wolframclient.deserializers import WXFConsumer, binary_deserialize
 4
 5class ComplexFunctionConsumer(WXFConsumer):
 6    """Implement a consumer that maps Complex to python complex types."""
 7    
 8    # represent the symbol Complex as a Python class
 9    Complex = wl.Complex
10
11    def build_function(self, head, args, **kwargs):
12        # return a built in complex if head is Complex and argument length is 2.
13        if head == self.Complex and len(args) == 2:
14            return complex(*args)
15        # otherwise delegate to the super method (default case).
16        else:
17            return super().build_function(head, args, **kwargs)
18
19with WolframLanguageSession() as session:
20    array = [
21        [0, -2, 0], 
22        [1, 0, -1], 
23        [0, 2, 0]]
24    # expression to evaluate
25    expr = wl.Eigenvalues(array)
26    # send expression to the kernel for evaluation.
27    wxf = session.evaluate_wxf(expr)
28    # get the WXF bytes and parse them using the complex consumer:
29    complex_result = binary_deserialize(
30        wxf, 
31        consumer=ComplexFunctionConsumer())
32    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:

 1from wolframclient.evaluation import WolframLanguageSession
 2from wolframclient.language import wl
 3from wolframclient.deserializers import WXFConsumer, binary_deserialize
 4
 5import math
 6import fractions
 7
 8# define Complex symbol once and for all
 9Complex = wl.Complex
10
11class MathConsumer(WXFConsumer):
12    """Implement a consumer with basic arithmetic operation."""
13    
14    # Specific convertion for Pi, other symbols use the default method.
15    def consume_symbol(self, current_token, tokens, **kwargs):
16        # Convert symbol Pi to its numeric value as defined in Python
17        if current_token.data == 'Pi':
18            return math.pi
19        else:
20            return super().consume_symbol(current_token, tokens, **kwargs)
21
22    # Associate heads with the method to convert them to Python types.
23    DISPATCH = {
24        Complex: 'build_complex',
25        wl.Rational: 'build_rational',
26        wl.Plus: 'build_plus',
27        wl.Times: 'build_times'
28    }
29    # Overload the method that builds functions. 
30    def build_function(self, head, args, **kwargs):
31        # check if there is a specific function associated to the function head
32        builder_func = self.DISPATCH.get(head, None)
33        if builder_func is not None:
34            try:
35                # get the class method and apply it to the arguments.
36                return getattr(self, builder_func)(*args)
37            except Exception:
38                # instead of failing, fallback to default case.
39                return super().build_function(head, args, **kwargs)
40        # heads not listed in DISPATCH are delegated to parent's method
41        else:
42            return super().build_function(head, args, **kwargs)
43
44    def build_plus(self, *args):
45        total = 0
46        for arg in args:
47            total = total + arg
48        return total
49
50    def build_times(self, *args):
51        total = 1
52        for arg in args:
53            total = total * arg
54        return total
55
56    def build_rational(self, *args):
57        if len(args) != 2:
58            raise ValueError('Rational format not supported.')
59        return fractions.Fraction(args[0], args[1])
60    
61    def build_complex(self, *args):
62        if len(args) != 2:
63            raise ValueError('Complex format not supported.')
64        return complex(args[0], args[1])
65
66with WolframLanguageSession() as session:
67    array = [
68        [wl.Pi, -2, 0], 
69        [1, wl.Pi, -1],
70        [0, 2, wl.Pi]]
71
72    # expression to evaluate: Eigenvalues[array]
73    expr = wl.Eigenvalues(array)
74
75    # Eigenvalues are exact, but the result is a symbolic expression:
76    # [Times[Rational[1, 2], Plus[Complex[0, 4], Times[2, Pi]]], 
77    # Times[Rational[1, 2], Plus[Complex[0, -4], Times[2, Pi]]], Pi]
78    print(session.evaluate(expr))
79    
80    # Use evaluate_wxf to evaluate without deserializing the result.
81    wxf = session.evaluate_wxf(expr)
82    # deserialize  using the math consumer:
83    complex_result = binary_deserialize(wxf, consumer=MathConsumer())
84    # get a numerical result, only made of built-in Python types.
85    # [(3.141592653589793+2j), (3.141592653589793-2j), 3.141592653589793]
86    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:

 1from wolframclient.evaluation import WolframLanguageSession
 2from wolframclient.language import wl
 3from wolframclient.deserializers import WXFConsumer, binary_deserialize
 4
 5# represent the symbol Complex as a Python class
 6Complex = wl.Complex
 7
 8class ComplexFunctionConsumer(WXFConsumer):
 9    """Implement a consumer that maps Complex to python complex types."""
10    def build_function(self, head, args, **kwargs):
11        if head == Complex and len(args) == 2:
12            return complex(*args)
13        else:
14            return super().build_function(head, args, **kwargs)
15
16with WolframLanguageSession() as session:
17    array = [
18        [wl.Pi, -2, 0], 
19        [1, wl.Pi, -1],
20        [0, 2, wl.Pi]]
21
22    # expression to evaluate: N[EigenValues[array]]
23    expr = wl.N(wl.Eigenvalues(array))
24    
25    # evaluate without deserializing
26    wxf = session.evaluate_wxf(expr)
27    # deserialize using the math consumer:
28    complex_result = binary_deserialize(wxf, consumer=ComplexFunctionConsumer())
29    # [(3.141592653589793+2j), (3.141592653589793-2j), 3.141592653589793]
30    print(complex_result)