Tuesday, September 7, 2021

網際 Python 程式學習環境

對於初學者而言, 在瀏覽器中直接學習 Python 有許多好處. 使用者無需安裝解譯系統, 甚至也不需要近端的編輯器, 直接透過瀏覽器上網後, 就可以開始練習.

以雙連桿的簡單平面機械手臂逆運動學方程式運算為例, 使用者一旦了解如何推導 IK 方程式, 就可以直接到 網際 Python 程式區 執行測試.

Github Gist

根據 Github Gist 的說明, 每一個 Gist 都是倉儲, 可以 fork 與 clone, 也可以分為 public 或 private, 當使用者在各自的帳號下建立 Gist 後, 可以透過 url 擷取, 且每一個不同版本各有一個版次號.

例如:

two_link_ik.py 目前共有兩個版次號, 也就是 two_link_ik.py 舊版 以及 two_link_ik.py 新版.

至於在 網際 Python 程式區中, 可以將此 Gist url 所對應的程式碼, 設定為 button 按鈕, 使用者一旦按下某一按鈕, Brython 就會將程式碼放入 editor 中, 讓使用者按下 run 按鈕後執行.

Brython 程式配置

ace.py 包含 traceback.py, 參考來源為 editor.py.

import sys
import time
import traceback
import javascript

from browser import document as doc, window, alert

if hasattr(window, 'localStorage'):
    from browser.local_storage import storage
else:
    storage = None

class cOutput:

    def __init__(self, target):
        self.target = doc[target]

    def write(self, data):
        self.target.value += str(data)

class Editor():

    def __init__(self, editor_id, console_id, container_id, storage_id):
        self.editor_id = editor_id
        self.console_id = console_id
        self.container_id = container_id
        self.storage_id = storage_id
        self.output = ''

        try:
            self.editor = window.ace.edit(self.editor_id)
            session = self.editor.getSession()
            session.setMode("ace/mode/python")

            self.editor.setOptions({
             'enableLiveAutocompletion': True,
             'enableSnippets': True,
             'highlightActiveLine': False,
             'highlightSelectedWord': True,
             'autoScrollEditorIntoView': True,
             # 'maxLines': session.getLength() 可以根據程式長度設定 editor 列數
             # 設定讓使用者最多可以在畫面中顯示 20 行程式碼
             'maxLines': 20,
             'fontSize': '12pt'
            })
        except:
            from browser import html
            self.editor = html.TEXTAREA(rows=20, cols=70)
            doc[self.editor_id] <= self.editor
            def get_value(): return self.editor.value
            def set_value(x): self.editor.value = x
            self.editor.getValue = get_value
            self.editor.setValue = set_value

    def run(self, *args):
        sys.stdout = cOutput(self.console_id)
        sys.stderr = cOutput(self.console_id)
        doc[self.console_id].value = ''
        src = self.editor.getValue()
        if storage is not None:
           storage[self.storage_id] = src

        t0 = time.perf_counter()
        try:
            #ns = {'__name__':'__main__'}
            # 以 self.editor_id 名稱執行程式
            #ns = {'__name__': self.editor_id}
            # execute program under the __main__ name
            ns = {'__name__':'__main__'}
            exec(src, ns)
            state = 1
        except Exception as exc:
            traceback.print_exc(file=sys.stderr)
            state = 0
        self.output = doc[self.console_id].value

        print('' % ((time.perf_counter() - t0) * 1000.0))
        return state

    def show_console(self, ev):
        doc[self.console_id].value = self.output
        doc[self.console_id].cols = 60
        doc[self.console_id].rows = 10

    def clear_console(self, ev):
        doc[self.console_id].value = ""

    def clear_container(self, ev):
        doc[self.container_id].clear()

    # load a Python script
    def load_script(self, evt):
        _name = evt.target.value + '?foo=%s' % time.time()
        self.editor.setValue(open(_name).read())

beditor.py

import ace
import json
from browser import document as doc

class gist():
    """gist class to get program from Python gist"""
    def __init__(self, id, filename):
        self.id = id
        self.filename = filename

    def get_file(self):
        url = 'https://api.github.com/gists/' + self.id
        # info is string
        info = open(url).read()
        gist = json.loads(info)
        return gist["files"][self.filename]["content"]

class editor():
    """ace brython editor"""
    def __init__(self, script, editor_id, console_id, container_id, storage_id):
        self.script = script
        self.editor_id = editor_id
        self.console_id = console_id
        self.container_id = container_id
        self.storage_id = storage_id
        self.Ace = ace.Editor(editor_id=self.editor_id
                        , console_id=self.console_id
                        , container_id=self.container_id
                        , storage_id=self.storage_id)

    # 所有的 button 程式碼中, 選擇一個執行 setValue(), 可以顯示在對應 id 的程式編輯區
    def setValue(self):
        self.Ace.editor.setValue(self.script)

    def prog(self, ev):
        '''Ace = ace.Editor(editor_id="kw_editor"
                        , console_id="kw_console"
                        , container_id="kw__container"
                        , storage_id="kw_py_src" )
        '''
        '''
        Ace = ace.Editor(editor_id=self.editor_id
                        , console_id=self.console_id
                        , container_id=self.container_id
                        , storage_id=self.storage_id)
        '''
        self.Ace.editor.setValue(self.script)
        self.Ace.editor.scrollToRow(0)
        self.Ace.editor.gotoLine(0)

    def run(self, ev):
        self.Ace.run(ev)

    def show_console(self, ev):
        self.Ace.show_console(ev)

    def clear_console(self, ev):
        self.Ace.clear_console(ev)

    def clear_container(self, ev):
        self.Ace.clear_container(ev)

程式引用

<h2>Gist</h2>
<h4>以下讓使用者將 Python 範例程式存入 <a href="https://docs.github.com/en/github/writing-on-github/editing-and-sharing-content-with-gists/creating-gists">Github Gist</a> 後, 經由程式 pubic Gist URL 連結, 以按鈕放入編輯器中執行:</h4>
<!-- 導入 brython 程式庫 -->
<script src="/static/brython.js"></script>
<script src="/static/brython_stdlib.js"></script>
<!-- 啟動 Brython -->
<script>// <![CDATA[
window.onload=function(){
brython({debug:1, pythonpath:['/static/','./../downloads/py/']});
}
// ]]></script>
<p><!-- 導入 FileSaver 與 filereader --></p>
<p>
<script type="text/javascript" src="/static/ace/FileSaver.min.js"></script>
<script type="text/javascript" src="/static/ace/filereader.js"></script>
</p>
<p><!-- 導入 ace --></p>
<p>
<script type="text/javascript" src="/static/ace/ace.js"></script>
<script type="text/javascript" src="/static/ace/ext-language_tools.js"></script>
<script type="text/javascript" src="/static/ace/mode-python3.js"></script>
<script type="text/javascript" src="/static/ace/snippets/python.js"></script>
</p>
<p><!-- 請注意, 這裡使用 Javascript 將 localStorage["py_src"] 中存在近端瀏覽器的程式碼, 由使用者決定存檔名稱--></p>
<p>
<script type="text/javascript">// <![CDATA[
function doSave(storage_id, filename){
    var blob = new Blob([localStorage[storage_id]], {type: "text/plain;charset=utf-8"});
    filename = document.getElementById(filename).value
    saveAs(blob, filename+".py");
}
// ]]></script>
</p>
<p><!-- 印出版次與關鍵字程式 --></p>
<p>
<script type="text/python3">// <![CDATA[
from browser import document as doc
from browser import html
import ace
# 清除畫布
def clear_bd(ev):
    bd = doc["brython_div"]
    bd.clear()
# Brython 3.3.4 內建的 container 名稱為  'container' 且 turtle 輸出為 svg 必須使用 div 訂定 id
Ace = ace.Editor(editor_id="kw_editor", console_id="kw_console", container_id="kw__container", storage_id="kw_py_src" )
# 從 gist 取出程式碼後, 放入 editor 作為 default 程式
url = "https://gist.githubusercontent.com/mdecourse/379f02862e9dfd95dbc5241d4faa2ad4/raw/e3dc77e68bbfb8f00eef2e78d3c8d2323b0f17da/clock.py"
data = open(url).read()
Ace.editor.setValue(data)
Ace.editor.scrollToRow(0)
Ace.editor.gotoLine(0)
# 執行程式, 顯示輸出結果與清除輸出結果及對應按鈕綁定
doc['kw_run'].bind('click', Ace.run)
doc['kw_show_console'].bind('click', Ace.show_console)
doc['kw_clear_console'].bind('click', Ace.clear_console)
doc['clear_bd'].bind('click', clear_bd)
// ]]></script>
</p>
<p><!-- 用來顯示程式碼的 editor 區域 --></p>
<div id="kw_editor" style="width: 600px; height: 300px;"></div>
<p><!-- 以下的表單與按鈕與前面的 Javascript doSave 函式以及 FileSaver.min.js 互相配合 --></p>
<!-- 存擋表單開始 --><form><label>Filename: <input id="kw_filename" placeholder="input file name" type="text">.py</label> <input onclick="doSave('kw_py_src', 'kw_filename');" type="submit" value="Save"></form><!-- 存擋表單結束 -->
<p></p>
<!-- 執行與清除按鈕開始 -->
<p><button id="kw_run">Run</button> <button id="kw_show_console">Output</button> <button id="kw_clear_console">清除輸出區</button><button id="clear_bd">清除繪圖區</button><button onclick="window.location.reload()">Reload</button></p>
<!-- 執行與清除按鈕結束 -->
<p></p>
<!-- 程式編輯檔案 -->
<div style="width: 100%; height: 100%;"><textarea autocomplete="off" id="kw_console"></textarea></div>
<!-- 程式輸入位置 -->
<div id="brython_div"></div>
<div class="col-md-0" height="1" id="graphics-column" width="1"></div>
<!-- 開始建立程式範例以及對應按鈕 -->
<p><button id="snake">Snake</button><button id="clock">Clock</button><button id="two_link_ik">Two link IK</button></p>
<!-- clock 開始 -->
<script type="text/python3">// <![CDATA[
# 導入 document 與 beditor
from browser import document
import beditor
# 設定按鈕 id 名稱 name 與 原始碼對應 url
name = "clock"
url = "https://gist.githubusercontent.com/mdecourse/379f02862e9dfd95dbc5241d4faa2ad4/raw/e3dc77e68bbfb8f00eef2e78d3c8d2323b0f17da/clock.py"
# 利用 open 與 read 取回原始碼
src = open(url).read()
# 將原始碼放入 editor
e = beditor.editor(src, "kw_editor", "kw_console", "kw__container", "kw_py_src")
# 設定 id 為 name 的元件按下後會執行 e.prog, 意即將原始碼放入 editor 中
document[name].bind('click', e.prog)
// ]]></script>
<!-- clock 結束 -->
<p><!-- snake 開始 -->
<script type="text/python3">// <![CDATA[
from browser import document as doc
# 導入位於 static 目錄下的 beditor.py 
import beditor
# 利用 beditor.py 中的 editor 類別建立案例, 對應到 snake
snake_url = "https://gist.githubusercontent.com/mdecourse/d306a1f57e53bfd6466eaae20bcb9439/raw/2160a12b9fec9707a120a383ed5d38b9b78a02cf/snake.py"
snake_src = open(snake_url).read()
snake = beditor.editor(snake_src, "kw_editor", "kw_console", "kw__container", "kw_py_src")
# id 為 "snake" 的按鈕點按時, 執行 snake 物件中的 prog 方法
doc["snake"].bind('click', snake.prog)
// ]]></script>
</p>
<p><!-- snake 結束--></p>
<!-- two link robot ik starts -->
<script type="text/python3">// <![CDATA[
# based on https://mde.tw/cd2021/content/W14-W15.html
from browser import document as doc
import beditor
url = "https://gist.githubusercontent.com/mdecourse/b58d23e73ff57c9ab1334f2e01cdc6e0/raw/2a0393fff3f621bc0267f2102697e586f99b8282/two_link_ik.py"
src = open(url).read()
two_link_ik = beditor.editor(src, "kw_editor", "kw_console", "kw__container", "kw_py_src")
doc["two_link_ik"].bind('click', two_link_ik.prog)
// ]]></script>
<!-- two link robot ik ends -->

No comments:

Post a Comment