Python 的 eval 函式雖然強大,但安全性一直備受爭議。如何在教學中引入 eval,讓學生理解其功能和安全風險,是程式設計教育中值得探討的議題。本文從一個簡單的乘法程式設計案例出發,逐步引導學生思考程式設計的各個導向,包含使用者介面、錯誤處理、程式碼可讀性等。過程中,我們會看到 eval 函式如何簡化程式碼,並探討其在教育場景下的應用價值。同時,也提供一個更健全的乘法程式設計方法,讓學生學習如何設計更完善的程式。

設計課的一堂課

蘇科爾斯基先生從他的桌子旁站起來,走到教室前面的電腦控制檯前。這是他在斯莫維爾高中教高階電腦科學課的第四天,他接替了正在休病假的潘·瓊斯老師。面對著一群面無表情的學生,他開始了今天的課程。

「假設大家都完成了第一個作業,我稍後會逐一檢查每位同學的程式。但現在,我希望有同學能夠示範自己的程式給全班同學看。有沒有人願意自願上台?」

沒有人舉手。蘇科爾斯基先生拿起他的成績記錄簿,掃視著學生的名單。

「羅傑,請在1到26之間選一個數字。」

「好,7。」羅傑回答。

「7,也就是……安娜。安娜,請你上來示範你的程式。」

安娜是一個聰明的準畢業生。在瓊斯老師的指導下,她表現出色。瓊斯老師對教學的熱情和對程式設計的熱愛得到了所有學生的讚賞。然而,蘇科爾斯基先生則不同。他從未當過高中老師,這一點從他佈置的第一個作業中就可以看出來。

安娜帶著她的筆記型電腦走到控制檯前。她連線了視訊線,然後輸入了她程式的名字:mult。螢幕上出現了一個問號。她輸入了2。又出現了一個問號。她輸入了3。螢幕上顯示出了答案6,隨後又出現了一個問號。安娜一言不發地回到她的座位上。從她的舉止中可以明顯看出,她認為這個作業是浪費時間。

程式碼解析

def mult():
    num1 = int(input("請輸入第一個數字:"))
    num2 = int(input("請輸入第二個數字:"))
    result = num1 * num2
    print("結果是:", result)

mult()

內容解密:

這段程式碼定義了一個名為mult的函式,用於計算兩個數字的乘積。首先,它會提示使用者輸入兩個數字,並將輸入的內容轉換為整數型別。然後,將兩個數字相乘,並將結果列印出來。在主程式中呼叫mult()函式來執行這個乘法運算。

設計思考

這個程式看似簡單,但在設計時卻需要考慮多個因素。例如,如何處理使用者的輸入?如果使用者輸入的不是數字怎麼辦?如何給出清晰的使用者提示?這些都是在設計程式時需要考慮的問題。

def safe_mult():
    try:
        num1 = float(input("請輸入第一個數字:"))
        num2 = float(input("請輸入第二個數字:"))
        result = num1 * num2
        print("結果是:", result)
    except ValueError:
        print("無效的輸入,請輸入數字。")

safe_mult()

內容解密:

這段改進後的程式碼加入了錯誤處理機制,使用try-except結構來捕捉ValueError,即當使用者輸入無法轉換為數字的內容時,程式不會當機,而是給出友善的錯誤提示。這樣可以提高程式的健壯性和使用者經驗。

教學反思

蘇科爾斯基先生面對的學生們可能已經習慣了瓊斯老師的教學風格,因此對於蘇科爾斯基先生的教學方法和作業佈置感到不適應。這提醒我們,在教學過程中,瞭解學生的背景和需求是非常重要的。同時,這也反映出在軟體開發中,瞭解使用者的需求和習慣同樣重要。

程式設計課上的深刻教訓

在程式設計的課堂上,學生Anna面臨了一次深刻的學習經歷。她的老師Mr. Sokolsky對她的程式提出了嚴格的批評和改進建議,讓她明白了一個好的程式不僅要能正確執行任務,還需要具備使用者友好的介面和處理特殊情況的能力。

從簡單的乘法程式談起

Anna最初提交的程式只是一個簡單的乘法程式,能夠接受兩個數字並列印出它們的乘積。Mr. Sokolsky認為這個程式雖然完成了基本功能,但缺乏使用者友好的介面和額外的功能。例如,程式沒有標題和使用者說明,輸入提示不明確,而且程式似乎會無限執行,沒有離開機制。

逐步改程式式

在Mr. Sokolsky的指導下,Anna對她的程式進行了多次改進。第一次改進中,她新增了離開機制,允許使用者輸入“X”來離開程式。Mr. Sokolsky進一步建議,可以在輸入第一個數字時就提示使用者輸入“X”來離開,這樣可以減少使用者的輸入負擔。

增加預設精確度和錯誤處理

在第二次改進中,Anna的程式仍然存在一些問題,例如無法正確處理分數輸入。Mr. Sokolsky建議新增預設精確度設定,讓程式預設輸出小數點後兩位,除非結果是整數。同時,他也建議允許使用者輸入“P”來更改預設精確度。

此外,Mr. Sokolsky測試了程式的錯誤處理能力。當他輸入非數字字串“happy”時,程式能夠正確回應“Not a number. Please re-enter a number.”。當他直接按下Enter鍵而不輸入任何內容時,程式也能正確處理。

進一步的改進和設計哲學

Mr. Sokolsky進一步要求Anna改程式式的輸出格式,讓它能夠顯示輸入的兩個數字、乘法符號、等於符號和乘積,以便使用者檢查輸入是否有誤。

在課堂上,Mr. Sokolsky分享了他的設計哲學:首先開發一個基本可用的版本,然後再逐步增加特殊情況的處理。他強調,大多數學生只知道兩種錯誤:編譯錯誤和邏輯錯誤,但實際上有四種錯誤,包括程式碼可讀性錯誤和功能性錯誤。

四種錯誤型別

  1. 編譯錯誤和邏輯錯誤:大多數學生熟悉這類別錯誤,涉及程式碼無法編譯或邏輯不正確。
  2. 程式碼可讀性或風格錯誤:這類別錯誤影響程式碼的可維護性和可讀性。
  3. 功能性錯誤:涉及程式功能是否符合需求。
  4. 隱含錯誤:可能未被立即發現的潛在問題。

結語

透過這次教學經歷,Anna和其他學生們學到了如何設計一個好的程式,不僅要滿足基本功能需求,還要具備良好的使用者經驗和錯誤處理能力。Mr. Sokolsky的教學方法雖然嚴格,但有效地提高了學生的程式設計能力和軟體開發思維。

def multiply_numbers():
    print("簡單乘法計算器")
    print("輸入數字進行乘法運算,或輸入 'X' 離開,或輸入 'P' 更改預設精確度。")
    
    precision = 2
    while True:
        user_input = input("請輸入第一個數字:")
        if user_input.upper() == 'X':
            break
        elif user_input.upper() == 'P':
            try:
                precision = int(input("請輸入新的預設精確度(小數點後位數):"))
                continue
            except ValueError:
                print("無效的精確度值,已還原預設精確度 2。")
                precision = 2
                continue
        
        try:
            num1 = float(user_input)
        except ValueError:
            print("無效的數字,請重新輸入。")
            continue
        
        user_input = input("請輸入第二個數字:")
        if user_input.upper() == 'X':
            break
        try:
            num2 = float(user_input)
        except ValueError:
            print("無效的數字,請重新輸入。")
            continue
        
        product = num1 * num2
        if product.is_integer():
            print(f"{num1} × {num2} = {int(product)}")
        else:
            print(f"{num1} × {num2} = {product:.{precision}f}")

if __name__ == "__main__":
    multiply_numbers()

內容解密:

此Python函式multiply_numbers實作了一個簡單的命令列乘法計算器。它首先顯示一個標題和說明,然後進入一個迴圈,持續接受使用者輸入直到使用者選擇離開。

  • 程式使用while True迴圈來持續接受使用者輸入。
  • 使用者可以輸入數字進行乘法運算,輸入 ‘X’ 離開,或輸入 ‘P’ 更改預設的小數點後精確度。
  • 當使用者選擇更改精確度時,程式會提示輸入新的精確度值,並更新precision變數。如果輸入無效,則還原預設精確度2。
  • 程式使用try-except塊來處理無效的數字輸入,並給予適當的錯誤訊息。
  • 對於有效的數字輸入,程式計算乘積並根據結果是否為整數來決定輸出的格式。如果結果是整數,則直接輸出整數;否則,按照設定的精確度輸出小數。
  • 輸出格式包含第一個數字、乘法符號、第二個數字、等於符號和乘積,便於使用者檢查輸入是否有誤。

軟體設計的挑戰:從簡單的乘法程式談起

在電腦科學的課堂中,學生們經常被要求實作各種演算法,卻很少被鼓勵去思考軟體的介面和功能性。Sokolsky 先生卻打破了這個常規,他要求學生們設計一個能夠乘以兩個數字的程式,並且考慮到不同的輸入基數和精確度。

從錯誤型別談起

Sokolsky 先生首先提出了四種可能的錯誤型別:

  1. 語法錯誤:程式碼是否符合程式語言的語法規則?
  2. 執行時錯誤:程式在執行過程中是否會出現錯誤,例如除以零?
  3. 邏輯錯誤:程式的邏輯是否正確,例如是否正確處理了邊界情況?
  4. 介面錯誤:程式的介面是否直觀易用?是否提供了足夠的資訊?

增加功能

Sokolsky 先生接著問學生們,如何增加程式的功能,使其更具吸引力。其中一個學生建議允許使用者輸入帶有逗號的大數字,以提高可讀性。另一個學生建議允許使用者輸入分數,例如「1/3」。Anna 則建議允許使用者為每個輸入數字指定不同的基數。

使用 eval 函式

在討論如何實作分數輸入時,Sokolsky 先生介紹了 Python 的 eval 函式。這個函式可以解析和執行字串中的算術表示式。例如:

x = "int('23', 6) * 1/3"
print(eval(x))  # 輸出:5.0

內容解密:

  • eval 函式用於解析和執行字串中的 Python 表示式。
  • 在這個例子中,int('23', 6) 將六進位制的 23 轉換為十進位制的數字,然後乘以 1/3
  • eval 函式可以提高程式的靈活性,但也需要注意安全性,避免執行惡意的程式碼。

學生的反應

Anna 對這個問題很感興趣,並開始研究 eval 函式。Elizabeth 則依賴 Anna 的幫助,直接複製她的程式碼。Anna 注意到,很多同學似乎只是在等待他人的幫助,而不是自己動手寫程式碼。

工業界的軟體測試

作者隨後提到了一本名為《Testing Computer Software》的書籍,其中包含了超過 340 種常見的軟體錯誤。這些錯誤大多與介面和功能性有關。在工業界,設計和測試軟體需要考慮很多細節,例如輸入驗證、錯誤處理、使用者介面等。

工業界的設計考量

  1. 介面設計:使用者是否清楚程式的功能?是否有清晰的螢幕指示?如何停止程式?
  2. 功能性:如何處理錯誤輸入?是否允許使用者更正輸入?是否支援不同的輸入格式?

乘法程式設計經驗:從挫折到突破的五日旅程

在進行乘法程式的開發過程中,我經歷了長達五天的思考與程式碼重構。這段經歷讓我深刻體會到設計一個使用者友善且功能強大的程式所面臨的挑戰與成長。

初始設計與挫折

最初,我試圖編寫程式碼來實作從任意進位制轉換為十進位。然而,我很快發現我的設計並不夠直觀。例如,我曾經允許使用者輸入 ‘B’ 或 ‘b’ 來代替數字以變更進位制,但這使得介面變得不便。接著,我嘗試讓使用者輸入包含進位制的數字,如 (12,8),但這需要額外的括號和逗號,仍然不夠方便。

多次迭代與改進

在接下來的幾天裡,我不斷修改我的程式碼,以期達到更好的使用者經驗。我曾考慮讓使用者輸入一到三個數字,分別代表數字、進位制和所需的小數精確度。然而,這需要處理數字之間的分隔符號(如空格或逗號),並去除多餘的空格。

整個設計和編碼過程耗時數天,最耗時的其實是思考如何檢測無效的使用者輸入。程式碼的編寫變得既令人沮喪又耗時。

突破與解決方案

直到我靈機一動,想到使用 Python 的 eval 函式,結合 try/except 區塊和進位制轉換,所有的問題迎刃而解。測試程式碼後,我發現 eval 能夠完美地評估內建函式,如 sqrtlog,甚至忽略了多餘的空格。

最終,我的程式變得簡單易寫。使用者只需輸入兩個以星號分隔的運算式,即可獲得結果。

最終程式碼解析

以下是我的最終程式碼範例:

def requestAndMultiplyTwoNumbers():
    #
---
Initialize.
    from math import sqrt, log, log10
    precision = 2
    problemCounter = 0
    errorMsg = ''
    while True:
        msg = errorMsg + 'Enter expression * expression, P (precision), or X (exit).'
        data = input(msg) 
        #
---
-
---
Check for 'X or x'.
        ch = data.strip()
        if ch in {'X', 'x'}:
            print (' Goodbye.')
            return
        #
---
-
---
Check for 'P or p.
        if ch in {'P', 'p'}:
            precision = requestPrecisionFromUser()
            errorMsg = ''
            continue
        #
---
-
---
Attempt to calculate an answer.
        try:
            answer = eval(data)
            if not isinstance(answer,(int, float)): 
                raise Exception
            errorMsg = ''
        except:
            errorMsg = '============ BAD INPUT ===========\n' + 'You entered --> ' + data +'.\n'
            continue
        #
---
-
---
Print the answer.
        problemCounter += 1
        if type(answer) == float:
            print(' ', str(problemCounter) + '. ', data, ' = ' , round(answer, precision), ' [decimal precision = ', precision, '.]', sep ='')
        else:
            print(' ', str(problemCounter) + '.', data, '=', answer)

內容解密:

  1. 初始化必要的數學函式匯入及變數設定,包括精確度 precision、問題計數器 problemCounter 和錯誤訊息 errorMsg
  2. 使用無限迴圈讓程式持續接受使用者輸入,直到使用者選擇離開。
  3. 檢查使用者輸入是否為 ‘X’ 或 ‘x’ 以離開程式,或 ‘P’ 或 ‘p’ 以變更輸出精確度。
  4. 使用 eval 函式評估使用者輸入的運算式。如果評估成功且結果為整數或浮點數,則列印結果;否則,捕捉例外並顯示錯誤訊息。
  5. 根據結果的資料型別(浮點數或整數),以不同的格式輸出結果,並顯示當前的十進位精確度。

心得與反思

這五天的經歷讓我明白到,設計改進往往源於對現有方案的不滿,只有在遇到挫折時,我們才會去尋找新的解決方案。這種經驗對於學生來說是寶貴的,但由於時間限制,通常難以在課堂上實踐。

為何要在程式設計中引入 eval 函式?

在網路上,有許多警告提醒開發者要遠離 Python 中的 eval 函式。然而,在某些特定情況下,eval 函式可以發揮其獨特的作用。本文將探討 eval 函式的危險性及其在程式設計教育中的價值。

eval 函式的危險性

eval("__import__('os').remove('e:filex.py')")

上述程式碼展示了 eval 函式的危險性:它能夠執行任意的 Python 程式碼,包括刪除檔案等惡意操作。然而,這種危險性主要來自於接受來自不可信來源的使用者輸入。在學校的程式設計問題中,學生通常是程式碼的唯一執行者,因此這種恐懼是沒有根據的。

內容解密:

  1. eval 函式的作用eval 能夠將字串當作 Python 程式碼執行。
  2. 危險性的來源:當 eval 接受使用者輸入且未經處理時,可能導致惡意程式碼的執行。
  3. 在教育場景中的安全性:由於學生通常控制自己的程式碼,因此在學校環境中使用 eval 是相對安全的。

引入 eval 的教育價值

引入 eval 可以讓學生了解其強大功能以及潛在的安全風險。這不僅能夠討論如何避免惡意程式碼的執行,還能探討人們為何會寫惡意程式碼。

對大型程式設計專案的方法論

在進行大型程式設計專案時,應遵循以下步驟:

  1. 預留足夠的時間:程式設計往往比預期花費更多時間。

  2. 專注於任務:避免分心,必要時採用結對程式設計。

  3. 理解問題:進行分析並制定程式規格,建構範例以找出關聯和洞察力。

  4. 選擇資料型別並設計程式:先實作基本功能,再逐步新增其他功能。

    • 內容解密:

      1. 最小化設計:先實作必須有的功能,形成初步可運作的程式。
      2. 預期設計的迭代:初始設計可能不佳,需要根據新洞察和困難進行調整。
  5. 撰寫程式碼

    • 使用逐步細化和自我檔案化的程式碼。
    • 使用斷言和錯誤陷阱進行測試。
    • 對每個關鍵功能進行測試。
  6. 根據需要重新設計:根據新洞察、編碼困難和使用者回饋進行調整。

  7. 測試整個程式:進行黑盒測試以找出遺漏的特殊情況。

  8. 重構整個程式:這是學習程式設計的地方。

  9. 反思錯誤和收穫