jupyter.net 客户端:一个与 Jupyter 内核交互的 C# 库






4.82/5 (9投票s)
jupyter.net 客户端的描述:一个用于与 Jupyter 内核交互的 C# 库
引言
在本文中,我将介绍一个名为 jupyter.net client 的 C# 库,它允许与 Jupyter 内核进行交互。
Jupyter 是一个为任何编程语言提供交互式计算框架的项目。该框架的主要组件如图所示。客户端与用户交互,请求执行代码并显示从内核收到的输出。内核执行代码并维护计算状态(局部变量、用户函数等)。客户端和服务器之间的通信通过 ZeroMQ 套接字进行,并使用下文所述的协议。
可以连接多个客户端到一个内核,但在本文中,我将只考虑一个内核连接到一个客户端的情况。
Jupyter 项目还定义了 Notebook 规范:一种客户端可以用来将源代码、计算结果和其他数据保存到文件中的文件格式。
现有的 Jupyter 客户端实现有很多。以下是最常用的独立应用程序:
- Jupyter notebook
- Jupyter console
- JupyterLab
- Nteract
- CoCalc
- Spyder
此外,还有一个 jupyter_client
库(https://pypi.ac.cn/project/jupyter-client/),用于以编程方式在 Python 中与内核交互。这里介绍的项目旨在成为该库的 C# 替代品。
jupyter.net client 的一些可能用途包括:
- 为任何脚本语言创建一个定制的 C# 前端。例如,您可以使用 Python 库
numpy
和panda
编写一个自定义的数据分析工具。 - 将其用作脚本引擎,在 C# 应用程序中运行任何可用 Jupyter 内核的语言的脚本。
在本文中,我将尝试概述以下主题:
- 如何使用 jupyter.net client
- Jupyter 框架概述
- jupyter.net client 的代码结构
要深入解释这些论点,一篇文章远不够,所以我将只提供关键信息,并提供一些链接供您深入了解。您也可以查看附带的源代码,它相当简单。
jupyter.net client 可在 GitHub(https://github.com/andreaschiavinato/jupyter.net_client)上找到,或者作为一个名为 JupiterNetClient
的 NuGet 包。
本文之后将有另一篇文章,介绍一个完整的 C# Windows 应用程序,该应用程序允许使用 jupyter.net client 与 Jupyter 内核进行交互。
Jupyter 软件安装
有一些对 Jupyter 有用的软件可以安装,包括:
- Jupyter notebook:一个用于交互式计算的 Web 应用程序。
- Jupyter console:一个用于交互式计算的简单命令行应用程序。
- Python kernel:一个用于 Python 语言的内核,在 Jupyter notebook 和 Jupyter console 中默认使用。
- 一些实用程序,如 jupyter-kernelspec.exe,它在 python.net client 中用于获取可用内核。
要安装它,请先安装 Python,然后运行 python -m pip install jupyter
。
要测试您是否正确安装了 Jupyter,您可以运行 python -m jupyter notebook
。稍后,Jupyter Notebook Web 应用程序应该会启动。
Hello World 应用程序
下面的代码是一个简单的 C# 应用程序,它在一个 Python 内核上执行代码 print("Hello from Jupyter")
。要编译它,您需要导入 JupiterNetClient
NuGet 包;要运行它,您需要安装前一节所述的软件。
using JupiterNetClient;
using JupiterNetClient.Nbformat;
using System;
using System.Linq;
class Program
{
static void Main(string[] args)
{
//Initializing the Jupyter client
//The constructor of JupyterBlockingClient will throw an exception
//if the jupyter framework is not found
//It is searched on the folders defined on the PATH system variable
//You can also pass the folder where python.exe is located as
//an argument of the constructor
//(since the jupyter framework is located on the python folder)
var client = new JupyterBlockingClient();
//Getting available kernels
var kernels = client.GetKernels();
if (kernels.Count == 0)
throw new Exception("No kernels found");
//Connecting to the first kernel found
Console.WriteLine($"Connecting to kernel {kernels.First().Value.spec.display_name}");
client.StartKernel(kernels.First().Key);
Console.WriteLine("Connected\n");
//A callback that is executed when there is any information
//that needs to be shown to the user
client.OnOutputMessage += Client_OnOutputMessage;
//Executing some code
client.Execute("print(\"Hello from Jupyter\")");
//Closing the kernel
client.Shutdown();
Console.WriteLine("Press enter to exit");
Console.ReadLine();
}
private static void Client_OnOutputMessage(object sender, JupyterMessage message)
{
switch (message.content)
{
case JupyterMessage.ExecuteResultContent executeResultContent:
Console.WriteLine($"[{executeResultContent.execution_count}] -
{executeResultContent.data[MimeTypes.TextPlain]}");
break;
case JupyterMessage.StreamContent streamContent:
Console.WriteLine(streamContent.text);
break;
default:
break;
}
}
}
如果程序成功运行,您将看到以下输出:
Connecting to kernel Python 3
Connected
Hello from Jupyter
Press enter to exit
主类说明
以下是 jupyter.net client 库的主要类:
JupyterClientBase
是主类,它实现了连接到内核、获取可用内核、执行代码等方法。
它有两个版本可供您使用:一个阻塞版本(JupyterBlockingClient
)和一个非阻塞版本(JupyterClient
)。简而言之,在阻塞版本中,执行代码的函数会等待直到代码执行完成;相反,非阻塞版本会立即返回并在完成后引发事件。这种架构与 Jupyter 客户端的官方实现(https://pypi.ac.cn/project/jupyter-client/)相似。我发现查看其代码有助于更好地理解 Jupyter 客户端应如何工作。
KernelManager
类用于发现和连接到 Jupyter 内核,它在库内部使用。
Notebook
类可用于读取或保存 Jupyter notebook 文件。
JupyterMessage
类用于处理内核和客户端之间交换的消息。
查找内核并连接到它
在接下来的部分中,我将提供有关 Jupyter 框架的一些技术细节。让我们先看看 Jupyter 客户端如何连接到内核。
每个内核都由一个这样的 JSON 文件定义:
{
"argv": [
"python",
"-m",
"ipykernel_launcher",
"-f",
"{connection_file}"
],
"display_name": "Python 3",
"language": "python"
}
这种结构称为 Kernelspec
,并且在此处定义:here。
在我的计算机上,这些文件位于文件夹 C:\Users\Andrea\AppData\Local\Programs\Python\Python37\share\jupyter\kernels\ 中,但检索此信息的更好方法是运行 jupyter-kernelspec.exe list
(它位于 Python 目录的 Scripts 子文件夹中,并在 KernelManager.GetKernels()
函数中使用)。
kernelspec
的 args
成员定义了启动内核的命令行。一旦内核启动,它应该会创建一个名为 connection file 的另一个文件,该文件标识内核实例并包含连接到它所需的所有信息(请参阅 https://jupyter-client.readthedocs.io/en/latest/kernels.html#connection-files 和 KernelManager.StartKernel()
函数的代码)。
下面是一个 connection file 的示例:
{
"shell_port": 64656,
"iopub_port": 64665,
"stdin_port": 64659,
"control_port": 64662,
"hb_port": 64675,
"ip": "127.0.0.1",
"key": "5f5313c7-fb04d880c4e5756a645e1a97",
"transport": "tcp",
"signature_scheme": "hmac-sha256",
"kernel_name": ""
}
connection file 包含五个用于与内核通信的 ZMQ 套接字端口号,以及一个用于创建消息签名(添加到所有消息中)的密钥。
ZeroMQ 套接字
ZeroMQ 套接字是用于与内核通信的工具。在 https://zguide.zeromq.cn 上可以找到对 ZMQ 套接字的良好描述。
ZeroMQ(也称为 ØMQ、0MQ 或 zmq)看起来像一个可嵌入的网络库,但它作为一个并发框架。它提供了可以在各种传输(如进程内、进程间、TCP 和多播)中承载原子消息的套接字。您可以使用扇出、发布/订阅、任务分发和请求-响应等模式将套接字 N 对 N 连接起来。它的速度足够快,可以作为集群产品的底层。它的异步 I/O 模型使您能够构建可扩展的多核应用程序,这些应用程序构建为异步消息处理任务。它拥有多种语言 API,并在大多数操作系统上运行。ZeroMQ 来自 iMatix,并且是 LGPLv3 开源的。
正如您所读到的,ZeroMQ 提供了不同的通信模式。以下是 Jupyter 中使用的模式:
- 请求/响应:响应套接字等待来自任何请求套接字请求。然后响应套接字可以向请求套接字提供答复。
- 发布/订阅:发布套接字用于发布消息,其他订阅套接字可以订阅它以接收这些消息。
- Router/Dealer:这是请求/响应模式的一个更复杂的版本,其中 ROUTER 可以被视为 REQUEST 套接字的一个异步版本,而 DEALER 可以被视为 RESPONSE 的一个异步版本。异步的意思是,DEALER 可以同时处理来自不同节点的多个请求。然而,对于本文的目的,将其视为等同于请求/响应就足够了。
下图显示了内核和客户端可以用于通信的 ZeroMQ 套接字。在 https://jupyter-client.readthedocs.io/en/stable/messaging.html 上可以找到更详细的描述。
ZeroMQ 套接字有两个主要的 C# 实现:
在这个项目中,我使用了 ZeroMQ
,但对于我们的目的,它们看起来是等效的。
Jupyter 协议
通过这些套接字交换的消息使用 JSON 格式。消息有以下字段:
Header
:包含消息的唯一标识符、包含用户名的字符串、会话标识符、时间戳、消息类型和协议版本。Parent_header
:它是父单元格的 header 的副本(如果存在)。例如,代码执行响应消息的父消息是代码执行请求消息。Metadata
:与消息关联的附加元数据。规范中并未明确说明如何使用此信息,在 jupyter.net client 中,元数据未使用。Content
:消息的内容,取决于消息类型。Buffers
:对于支持协议二进制扩展的实现,是二进制数据缓冲区的列表。在 jupyter.net 中,此信息未使用。
此时可用的消息类型有:
execute_request
:客户端用于请求内核执行特定操作。execute_reply
:内核用于通知客户端请求的操作已完成。status
:内核用于向客户端传达其状态。display_data
:内核用于通知客户端有需要显示给用户的数据。execute_result
:内核用于向客户端传达计算结果。input_request
:内核用于通知客户端需要用户输入。execute_input
:内核用于向所有连接的客户端广播正在执行的代码。error
:内核用于通信计算过程中发生的错误。
有关更多信息,请参阅 https://jupyter-client.readthedocs.io/en/stable/messaging.html#messages-on-the-shell-router-dealer-channel。
在 jupyter.net client 中,创建了 JupyterMessage
类来处理这些消息。如图所示,有一个 abstract
类用于处理内容,该类有不同的子类,具体取决于消息类型。
客户端和服务器之间的交互遵循此模式:
- 客户端通过
shell
套接字向内核发送一个execute_request
消息。 - 内核在
iopub
套接字上发布一个status
消息,表明它正在忙。 - 内核在
iopub
套接字上发布任何需要的display_data
/execute_result
/error
消息,或者在stdin
套接字上发布任何execure_input
消息。 - 内核在
shell
套接字上发送一个execute_reply
消息,表明计算已完成。 - 内核在
iopub
套接字上发布一个status
消息,表明它已准备好处理下一个请求。
可用命令
Jupyter 协议提供了客户端可以在 execute_request
消息中使用的以下命令:
- Execute:执行一些代码。
- Introspection:提供代码信息(例如,变量的类型,但这取决于内核提供什么信息)。
- Completion:提供一个
string
来完成当前代码。 - History:提供最近执行语句的列表。
- Code completeness:指示当前代码是否可以按原样执行,或者客户端是否应要求用户输入更多行。
- Kernel info:提供内核信息。
- Kernel shutdown:关闭内核。
- Kernel interrupt:中断当前计算。
对于这些命令中的每一个,JupyterClient
/ JupyterBlokingClient
类都提供了一个相应的方法。
执行代码
下面的序列图说明了客户端和内核之间执行代码所交换的消息。
客户端通过 shell
套接字发送一个类型为 execute_request
的 ZeroMQ
消息,其中包含要执行的代码。
内核在 IOPub
套接字上发送一个类型为 status
的消息,指示它正在忙,然后通过发送一个类型为 execute_input
的消息(包含收到的代码的副本和一个标识该语句的渐进编号)来确认消息已收到。
然后,它通过在 IOPub
套接字上发送一个类型为 execute_result
的消息来发送执行结果。有些代码可能不会产生 execute_result
,而是内核可能会发送一个 stream
或 display_data
消息。
然后,它在 shell
套接字上发送一个类型为 execute_reply
的消息,表明执行已完成。
最后,它在 IOPub
套接字上发送一个 status
消息,表明它已准备好处理下一个请求。
命令行客户端
下面,我将提供一个可用于与 Jupyter 内核交互的命令行客户端代码。它是 Jupyter 控制台应用程序(https://github.com/jupyter/jupyter_console)的 C# 版本。
using JupiterNetClient;
using JupiterNetClient.Nbformat;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
class Program
{
private const string Prompt = ">>> ";
private const string PromptWhite = "... ";
private static JupyterBlockingClient client;
private static Dictionary<string, KernelSpec> kernels;
static void Main(string[] args)
{
client = new JupyterBlockingClient();
kernels = client.GetKernels();
if (kernels.Count == 0)
throw new Exception("No kernels found");
//Connecting to the first kernel found
Console.WriteLine($"Connecting to kernel {kernels.First().Value.spec.display_name}");
client.StartKernel(kernels.First().Key);
DisplayKernelInfo(client.KernelInfo);
client.OnOutputMessage += Client_OnOutputMessage;
client.OnInputRequest += Client_OnInputRequest;
//Mainlook asks code to execute and executes it.
Console.WriteLine("\n\nEnter code to execute or Q <enter> to terminate:");
MainLoop(client);
//terminating the kernel process
Console.WriteLine("SHUTTING DOWN KERNEL");
client.Shutdown();
}
下面是 MainLoop
过程:
private static void MainLoop(JupyterBlockingClient client)
{
//Using the component ReadLine, which has some nice features
//like code completion and history support
ReadLine.HistoryEnabled = true;
ReadLine.AutoCompletionHandler = new AutoCompletionHandler(client);
var enteredCode = new StringBuilder();
var startNewCode = true;
var lineIdent = string.Empty;
while (true)
{
enteredCode.Append(ReadLine.Read
(startNewCode ? Prompt : PromptWhite + lineIdent));
var code = enteredCode.ToString();
if (code == "Q")
{
//When the user types Q we terminates the application
return;
}
else if (string.IsNullOrWhiteSpace(code))
{
//No code entered, do nothing
}
else
{
//Asking the kernel if the code entered by the user
//so far is a complete statement.
//If not, for example because it is the first line of a function definition,
//we ask the user to enter one more line
var isComplete = client.IsComplete(code);
switch (isComplete.status)
{
case JupyterMessage.IsCompleteStatusEnum.complete:
//the code is complete, execute it
//the results are given on the OnOutputMessage callback
client.Execute(code);
startNewCode = true;
break;
case JupyterMessage.IsCompleteStatusEnum.incomplete:
lineIdent = isComplete.indent;
enteredCode.Append("\n" + lineIdent);
startNewCode = false;
break;
case JupyterMessage.IsCompleteStatusEnum.invalid:
case JupyterMessage.IsCompleteStatusEnum.unknown:
Console.WriteLine("Invalid code: " + code);
startNewCode = true;
break;
}
}
if (startNewCode)
{
enteredCode.Clear();
}
}
}
AutoCompletionHandler
类由 Readline
组件使用,以支持自动完成。
private class AutoCompletionHandler : IAutoCompleteHandler
{
private readonly JupyterBlockingClient _client;
public AutoCompletionHandler(JupyterBlockingClient client)
{
_client = client;
}
public char[] Separators { get; set; } = new char[] { };
public string[] GetSuggestions(string text, int index)
{
//asking the kernel to provide a list of strings to complete the current line
var result = _client.Complete(text, text.Length);
return result.matches
.Select(s => text.Substring(0, result.cursor_start) + s)
.ToArray();
}
}
这是用于显示输出消息的回调。
private static void Client_OnOutputMessage(object sender, JupyterMessage message)
{
switch (message.content)
{
case JupyterMessage.ExecuteInputContent executeInputContent:
Console.WriteLine($"Executing
[{executeInputContent.execution_count}] - {executeInputContent.code}");
break;
case JupyterMessage.ExecuteResultContent executeResultContent:
Console.WriteLine($"Result
[{executeResultContent.execution_count}] -
{executeResultContent.data[MimeTypes.TextPlain]}");
break;
case JupyterMessage.DisplayDataContent displayDataContent:
Console.WriteLine($"Data {displayDataContent.data}");
break;
case JupyterMessage.StreamContent streamContent:
Console.WriteLine($"Stream {streamContent.name} {streamContent.text}");
break;
case JupyterMessage.ErrorContent errorContent:
Console.WriteLine($"Error {errorContent.ename} {errorContent.evalue}");
Console.WriteLine(errorContent.traceback);
break;
case JupyterMessage.ExecuteReplyContent executeReplyContent:
Console.WriteLine($"Executed
[{executeReplyContent.execution_count}] - {executeReplyContent.status}");
break;
default:
break;
}
}
这是用于请求用户输入的_回调_。
private static void Client_OnInputRequest
(object sender, (string prompt, bool password) e)
{
var input = e.password
? ReadLine.ReadPassword(e.prompt)
: ReadLine.Read(e.prompt);
client.SendInputReply(input);
}
最后,这是用于显示内核信息的_方法_:
private static void DisplayKernelInfo(JupyterMessage.KernelInfoReplyContent kernelInfo)
{
Console.WriteLine("");
Console.WriteLine(" KERNEL INFO");
Console.WriteLine("============");
Console.WriteLine($"Banner: {kernelInfo.banner}");
Console.WriteLine($"Status: {kernelInfo.status}");
Console.WriteLine($"Protocol version: {kernelInfo.protocol_version}");
Console.WriteLine($"Implementation: {kernelInfo.implementation}");
Console.WriteLine($"Implementation version: {kernelInfo.implementation_version}");
Console.WriteLine($"Language name: {kernelInfo.language_info.name}");
Console.WriteLine($"Language version: {kernelInfo.language_info.version}");
Console.WriteLine($"Language mimetype: {kernelInfo.language_info.mimetype}");
Console.WriteLine($"Language file_extension: {kernelInfo.language_info.file_extension}");
Console.WriteLine($"Language pygments_lexer: {kernelInfo.language_info.pygments_lexer}");
Console.WriteLine($"Language nbconvert_exporter:
{kernelInfo.language_info.nbconvert_exporter}");
}
Notebook 格式
Jupyter notebook 是一个 Json 文件,如下所示:
{
"metadata": {
"kernel_info": {
"name": "Python 3"
},
"language_info": {
"name": "python",
"version": "3.7.2"
}
},
"nbformat": 4,
"nbformat_minor": 2,
"cells": [{
"execution_count": 1,
"outputs": [{
"execution_count": 1,
"data": {
"text/plain": ["4"]
},
"output_type": "execute_result"
}],
"cell_type": "code",
"source": ["2+2"],
}]
}
它有一个 metadata
对象,包含有关内核和所用语言的信息。客户端负责确保打开的任何 notebook 文件都与正在使用的内核兼容。
然后有一个单元格元素列表,这些元素可以是以下类型之一:
- Code:包含要运行的代码,并且可能包含执行的输出。
- Markdown:包含使用 markdown 语法格式化的文本。
- Raw:包含用于 nconvert 实用程序的数据的特殊单元格。
以下示例读取一个 notebook 并将其内容打印到屏幕上:
using JupiterNetClient.Nbformat;
using System;
class Program
{
static void Main(string[] args)
{
var nb = Notebook.ReadFromFile(@"test.ipynb"); //TODO: change the name of the file
Console.WriteLine($"Langauge: {nb.metadata.language_info.name}
{nb.metadata.language_info.version}");
Console.WriteLine($"Kernel: {nb.metadata.kernel_info.name}");
Console.WriteLine($"Notebook format: {nb.nbformat}.{nb.nbformat_minor}");
Console.WriteLine("\nContent:\n");
foreach (var cell in nb.cells)
{
switch (cell)
{
case MarkdownCell markdownCell:
Console.WriteLine(markdownCell.source);
break;
case CodeCell codeCell:
Console.WriteLine(codeCell.source);
foreach (var output in codeCell.outputs)
{
Console.Write(" " + output.output_type + ": ");
switch (output)
{
case StreamOutputCellOutput streamOutputCellOutput:
Console.WriteLine($"{streamOutputCellOutput.name}
{streamOutputCellOutput.text}");
break;
case DisplayDataCellOutput displayDataCellOutput:
Console.WriteLine
(displayDataCellOutput.data[MimeTypes.TextPlain]);
break;
case ExecuteResultCellOutput executeResultCellOutput:
Console.WriteLine
(executeResultCellOutput.data[MimeTypes.TextPlain]);
break;
case ErrorCellOutput errorCellOutput:
Console.WriteLine($"{errorCellOutput.ename}
{errorCellOutput.evalue}");
break;
}
}
break;
case RawCell _:
Console.WriteLine($"(raw cell)");
break;
}
}
Console.ReadLine();
}
}
以下程序显示如何创建一个简单的 notebook:
using JupiterNetClient;
using JupiterNetClient.Nbformat;
using System;
using System.Linq;
class Program
{
static void Main(string[] args)
{
var client = new JupyterBlockingClient();
//Getting available kernels
var kernels = client.GetKernels();
if (kernels.Count == 0)
throw new Exception("No kernels found");
//Connecting to the first kernel found
Console.WriteLine($"Connecting to kernel {kernels.First().Value.spec.display_name}");
client.StartKernel(kernels.First().Key);
Console.WriteLine("Connected");
//Creating a notebook and adding a code cell
var nb = new Notebook(client.KernelSpec, client.KernelInfo.language_info);
var cell = nb.AddCode("print(\"Hello from Jupyter\")");
//Setting up the callback so that the outputs are written on the notebook
client.OnOutputMessage += (sender, message) =>
{ if (ShouldWrite(message)) cell.AddOutputFromMessage(message); };
//executing the code
client.Execute(cell.source);
//saving the notebook
nb.Save("test.ipynb");
Console.WriteLine("File test.ipynb written");
//Closing the kernel
client.Shutdown();
Console.WriteLine("Press enter to exit");
Console.ReadLine();
}
private static bool ShouldWrite(JupyterMessage message) =>
message.header.msg_type == JupyterMessage.Header.MsgType.execute_result
|| message.header.msg_type == JupyterMessage.Header.MsgType.display_data
|| message.header.msg_type == JupyterMessage.Header.MsgType.stream
|| message.header.msg_type == JupyterMessage.Header.MsgType.error;
}
从 C# 应用程序执行 Python 脚本并获取结果
您可以使用 python.net client 从您的 C# 应用程序中运行 Python 脚本。与 IronPython 或 Python for .NET (pythonnet) 等库不同,使用 jupyter.net client(以及 Python 内核),Python 代码在单独的进程中执行,因此更安全,因为它不会导致应用程序崩溃,并且最终可以在远程计算机上执行。您还可以轻松更新 Python 内核,而无需重新编译 C# 应用程序。
下面是一个示例,展示了如何使用 python.net client 运行在 .py 文件中编写的函数并获取结果。
using JupiterNetClient;
using JupiterNetClient.Nbformat;
using System;
using System.Linq;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
var client = new JupyterBlockingClient();
//Getting available kernels
var kernels = client.GetKernels();
if (kernels.Count == 0)
throw new Exception("No kernels found");
//Connecting to the first kernel found
Console.WriteLine($"Connecting to kernel {kernels.First().Value.spec.display_name}");
client.StartKernel(kernels.First().Key);
Console.WriteLine("Connected\n");
//Loading a script containing the following function:
//def do_something(a):
// return a ** 2
client.Execute("%run script.py");
//Creating an event handler that stores the result
//of the computation in a TaskCompletionSource object
var promise = new TaskCompletionSource<string>();
EventHandler<JupyterMessage> hanlder = (sender, message) =>
{
if (message.header.msg_type == JupyterMessage.Header.MsgType.execute_result)
{
var content = (JupyterMessage.ExecuteResultContent)message.content;
promise.SetResult(content.data[MimeTypes.TextPlain]);
}
else if (message.header.msg_type == JupyterMessage.Header.MsgType.error)
{
var content = (JupyterMessage.ErrorContent)message.content;
promise.SetException(new Exception
($"Jupyter kenel error: {content.ename} {content.evalue}"));
}
};
client.OnOutputMessage += hanlder;
//calling the function do_something
client.Execute("do_something(2)");
//removing event handler, since the TaskCompletionSource cannot be reused
client.OnOutputMessage -= hanlder;
//getting the result
try
{
Console.WriteLine("Result:");
if (promise.Task.IsCompleted)
Console.WriteLine(promise.Task.Result);
else
Console.WriteLine("No result received");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
finally
{
//Closing the kernel
client.Shutdown();
Console.WriteLine("Press enter to exit");
Console.ReadLine();
}
}
}
预期的输出是:
Connecting to kernel Python 3
Connected
Result:
4
Press enter to exit
历史
- 2019年11月3日:初始版本