[Python] 如何用Python提供資料介接層給後端演算法與前端UI (二)

  • 7310
  • 0
  • 2018-09-07

一樣是公司需求,因為AI Team都是使用Python開發演算法程式,所以要使用演算法就一定要可以執行Python,但是前端的UI使用的程式語言不可能都能完整與Python整合,所以有了兩種替代方案,第一種是架設一個Web service,而且就是使用Python來架設,如此就可以在Web Service這一層直接呼叫其他部門開發的Python程式;第二種是透過command line的方式(後述)。這一篇接著講的是如何運用第二種方式提供UI一個類似運算中心的feature。

承接第一篇文章的目的,某一支程式要跟另一隻程式做溝通的方式還有call out,就是在command line的模式下呼叫,並且雙方定義好input以及output,就可以達到溝通的目的。但是這樣的方式通常會有版本控管的問題,因為這種做法是執行某支特定檔案,當UI是採取Windows form的方式在User端執行,那麼要怎麼換掉那隻檔案就是一個問題。比如說前端UI不變,但是想撤換後端做運算的Python程式時,就必須要有一個更新的機制提供給User自動或手動更新,在架構上沒辦法像第一篇的Web Service切得這麼乾淨,可以各自發展、各自維護。

另外一個問題就是,User端一定要安裝Python的執行環境,這個通常也會是最令人頭痛的,除非跟infra說好做系統時預設要將Python環境做進去,而且如果UI想換新版本的Python時(例如Python 2.7 to 3.X),也必須跟infra協調重做一堆User的系統。針對時效性以及浪費的成本這兩點來評估,還是會比較推薦採用分層的做法。

廢話講了很多,先進入主題吧,筆者在這邊會以C#傳統的Windows form來當作例子。首先要確定執行環境中已經安裝好Python,如果不確定,可以先打開cmd,然後輸入python看一下OS認不認得這個指令:
OK之後打開Visual Studio,建立一個新的Windows Form專案,Form裡面只要放一個TextBox(Multiline = true)以及Button就可以了。然後點兩下Button進去編輯Click事件:以下是Form1.cs的完整內容:

using System;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        /// <summary>
        /// 委派方法
        /// </summary>
        /// <param name="text"></param>
        delegate void SetTextCallback(string text);

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            //利用實體檔案來當作兩種程式的溝通管道
            double timeStamp = ((TimeSpan)DateTime.Now.Subtract(DateTime.MinValue)).TotalSeconds;
            //實體檔案名稱
            string fileName = string.Format("{0}.txt", timeStamp);

            //起一個Process執行Python程式
            Process pyProc = new Process();
            pyProc.EnableRaisingEvents = true;
            pyProc.StartInfo.UseShellExecute = false;
            pyProc.StartInfo.WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory;
            pyProc.StartInfo.FileName = "python";
            pyProc.StartInfo.CreateNoWindow = true;
            //將必要的參數丟進Python,其中test.py的路徑是放在與此UI執行路徑的同一個資料夾中
            pyProc.StartInfo.Arguments = string.Format("test.py --name \"Albert\" --content \"{0}\" --timestamp \"{1}\"",
                //AppDomain.CurrentDomain.BaseDirectory,
                DateTime.Now.ToString("yyyy/MM/dd hh:mm:ss"),
                timeStamp);

            //定義Process結束事件處理器
            pyProc.Exited += (obj, args) =>
            {
                //ExitCode: 0 表示Process執行無錯誤,順利結束
                if (pyProc.ExitCode == 0)
                {
                    //讀出實體檔案內容,並設定給textBox1.Text(這邊請自行處理無檔案時的例外)。
                    //這裡會用委派是因為Process是開另一個執行序去處理的,
                    //所以有thread-unsafe的問題,使用委派是一種解決方式(另一種是使用BackgroundWorker)
                    //請參考:https://docs.microsoft.com/zh-tw/dotnet/framework/winforms/controls/how-to-make-thread-safe-calls-to-windows-forms-controls
                    this.SetText(System.IO.File.ReadAllText(fileName));
                    File.Delete(fileName);
                }
                else
                {
                    this.SetText(string.Format("TimeStamp: {0} 執行錯誤", timeStamp));
                }
            };
            
            bool result = pyProc.Start();
        }
        
        /// <summary>
        /// 定義委派方法,用來改變畫面上的控制項
        /// </summary>
        /// <param name="text"></param>
        private void SetText(string text)
        {
            if (this.textBox1.InvokeRequired)
            {
                SetTextCallback d = new SetTextCallback(SetText);
                this.Invoke(d, new object[] { text });
            }
            else
            {
                this.textBox1.Text = text;
            }
        }
    }
}

關於程式碼裡面的comment,簡單說就是使用前端指定的檔案名稱來當做Python的回傳資料暫存區,等UI讀取完檔案內容之後就把檔案砍掉,就這麼簡單。其中比較特別的是,如果在pyProc.Exited事件處理器中,直接設定this.textBox1.Text = System.IO.File.ReadAllText(fileName),會在執行階段產生Exception,因為這樣對Windows Form的控制項來說是一種thread-unsafe的操作,所以必須透過委派或是背景工作的方式來操作控制項。

接下來是test.py的完整內容:

# -*- coding: utf-8 -*-

#Python內建的參數處理package
import argparse

#產生一個針對參數的parser
def MakeArgParser():
    parser = argparse.ArgumentParser()

    #定義每一個參數的名稱、資料型態、預設值、說明等
    parser.add_argument('--name', type=str, default=None, help='Name to call')
    parser.add_argument('--content', type=str, default=None, help='Content to say')
    parser.add_argument('--timestamp', type=str, default=None, help='The File Name of Result')

    return parser

if __name__ == '__main__':
    parser = MakeArgParser()

    #使用parser將command line模式收到的參數轉成args物件
    args = parser.parse_args()

    #這邊可以再轉呼叫AI Team他們所撰寫的演算法程式,取得結果後轉成json格式的字串寫入txt中

    #將需要回傳的資料寫在指定名稱的txt檔案中
    with open(args.timestamp + '.txt', mode='w') as resultFile:
        resultFile.write("Hello {}, it is {} now!".format(args.name, args.content))

這一支Python程式相對來說更簡單,只有將參數吃進來,再把一段變動的字串寫進指定名稱的txt檔案中。重點在取得args之後,就可以呼叫其他部門提供的演算法程式,取得分析或預測之後的結果,將結果轉成json格式存進檔案中(這一段可以利用pandas.DataFrame.to_json來做,很簡單),前端取得檔案內容之後也可以很便利的將json反序列化成為物件集合來使用。

相對於Web service,這種直接call out的方式對於Python程式碼來說是沒有隱私可言的,所以建議想要保護自己智慧結晶的同學們還是採用Web Service來做整合應用。

Python Version:
3.6.6