iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 7
5
Software Development

活用python- 路遙知碼力,日久練成精系列 第 7

Day7- 如果你願意一層一層一層 的剝開繁瑣邏輯 (化簡冗長的if-else教學)

路遙知碼力,日久練成精- 精簡的程式碼不光是靠好用的內建函數拼湊出來,亦是對程式邏輯有足夠的理解的展現。

if- else 程式碼可說是工程師使用頻率很高,
卻也容易寫壞的一個例子,
過於多層的if-else嵌套,
容易使程式邏輯複輯,難以除錯
這一篇從語法及邏輯出發,教大家如何避免寫出冗長的if-else。
什麼?你說if-else有什麼難的,你第一個程式早就學過了?

首先來看昨天的課後習題的答案吧(昨日題目傳送門)。
參考解答:

def isLegal(s):
    return s.replace('_','').isalnum() and not s[0].isdigit()

(注意:因為題目已經指明參數s是非空字串,可不必加上len(s)>0這個條件
看到這裡,有做昨天習題的朋友不禁要疑惑,
答案當真那麼簡單嗎?
你心中想的答案會不會是比較像底下這樣:

def isLegal(s):
    if s.replace('_','').isalnum() and not s[0].isdigit():
        return True
    else:
        return False

我們由淺入深來開始:
例如希望寫一個函數判斷一個整數是否是偶數,
常會看到經驗不足的人順手寫出以下程式:

def isEven(n):
    if n%2 == 0:
        return True
    else:
        return False

看起來是沒有什麼問題,
但其實n%2 == 0本身是一個條件判斷式,
n%2 == 0 的值已經是True或是False了,
因此這段程式可以簡單用一句話來寫:

def isEven(n):
    return n%2 == 0

夠簡單吧?
以下舉幾個實例教大家化簡if-else,
希望能給大家一些啟發。

技巧1 – 使用三元運算式語法

例如現在我們想要判斷一個變數型態是否為整數,
我們可以用type()函數檢查一個變數的型態。
化簡前:

n = 5
T=""
if type(n)==int:
    T="n是整數"
else:
    T="n不是整數"

化簡後:

n = 5
T= "n是整數" if type(n)==int else "n不是整數"

這個給個說明,
三元運算式的語法為:

變數 = 值1 if 條件式 else 值2

如果條件式的值為True,變數就會等於值1
否則變數等於值2
注意使用三元運算子語法時,
if-else一定要成對出現。

技巧2 – 利用in合併多個條件式

例如現在給你一個單字,
要你判斷這個單字的字首是不是母音('a','e','i','o','u')。
化簡前:

word = "apple"
if word[0] == 'a' or word[0] == 'e' or word[0] == 'i' or word[0] == 'o' or word[0] == 'u':
    print("單字開頭為母音")

五個判斷式同樣以word[0]這個字來做等式判斷,
實在太冗長了。
可以用in做改寫會簡單許多。

化簡後:

word = "apple"
if word[0] in 'aeiou':
    print("單字開頭為母音")

in這個關鍵字除了平時較常用的與for搭配的語法外(例如for i in range(3):),
亦可作為條件判斷,
A in B即是判斷物件A是否在容器B裡,
例如:

>>> 1 in [1,2,3]
True
>>> "a" in "apple"
True

技巧3 – 利用列表或字典化簡大量if-else if

範例7-1 十二生肖

民國1年是鼠年,
想要寫一個程式判斷今年是什麼年(鼠、牛、虎、兔、…)。
化簡前:

year = int(input())
if year%12 == 1:
    print("今年是鼠年")
elif year%12 == 2:
    print("今年是牛年")
elif year%12 == 3:
    print("今年是虎年")
# …
#省略部分程式碼
# …
elif year%12 == 11:
    print("今年是狗年")
elif year%12 == 0:
    print("今年是豬年")

哇,光是判斷十二生肖就寫了12個if-elif了,
真是非常不簡潔呢,
我們看看如何簡化它。

化簡後:

year = int(input())
S = ["豬","鼠","牛","虎","兔","龍","蛇","馬","羊","猴","雞","狗"]
print("今年是"+S[year%12]+"年")

改成用列表取index的方式來做,
把if-elif全部省略了,
是不是非常簡潔易讀呢?

範例7-2 撲克花色

假設我們想把英文字母與撲克牌的花色做轉換,
且看以下程式:
化簡前:

letter = input()
suit = ""
if letter == 's':
    suit = "黑桃"
elif letter == 'h':
    suit = "紅心"
elif letter == 'd':
    suit = "方塊"
elif letter == 'c':
    suit = "梅花"

這時便很適合用python的字典化簡它。
化簡後:

letter = input()
D = {'s':"黑桃", 'h':"紅心", 'd':"方塊", 'c':"梅花"}
suit = D[letter]

技巧4 – 化簡多重嵌套if else

不知道大家有看過遊戲王,
或是玩過卡片對戰類型的遊戲嗎?
這類遊戲因為卡片效果複雜的交互作用,
若是寫成if-else邏輯可能常常也是很複雜的。

舉例來說,假設我們想要做一款雙人對戰的卡片對戰遊戲,
創造一張卡片名稱叫作「俄羅斯輪盤」,
效果為「對任意角色造成傷害,此卡會重複施放十次。」
那麼每次判定的邏輯大概是長什麼樣子呢?
首先是卡片對戰的邏輯,
通常是打到其中一方人物生命值歸零,
遊戲就會結束,
這樣卡片效果可能就會在施放十次之前結束。

以下是我們設計的效果判定的邏輯:

先施放卡片效果。
若我方生命值小於0,回傳"輸掉遊戲",否則
    若敵方生命值小於0,回傳"贏得遊戲",否則
        若卡片效果已施放十次,回傳"結束效果",否則
            回傳"繼續施放卡片效果"

這邊我們假設myHP, enemyHP, cnt, 都是可取得的全域變數,
分別代表「我方生命值」、「敵方生命值」、「卡片施放次數」,
試著把它寫成程式邏輯:

化簡前:

def judge():
    if myHP <= 0:
        return "輸掉遊戲"
    else:
        if enemyHp <= 0 :
            return "贏得遊戲"
        else:
            if cnt == 10:
                return "結束效果"
            else:
                return "繼續施放卡片放果"

可以看到程式邏輯花了三層的if-else嵌套來做,
不是那麼好讀。

化簡後:

def judge():
    if myHP <= 0:
        return "輸掉遊戲"
    
    if enemyHp <= 0 :
        return "贏得遊戲"
    
    if cnt == 10:
        return "結束效果"
    
    return "繼續施放卡片放果"

把多層if-else拆開後,整支程式就乾淨好讀多了。

技巧5 – Python高手才知道的and與or 特性

這是python中還蠻特別的特性,
不一定每個程式語言都有。
在python中,and, or 進行的邏輯運算,
不一定總是返回True或是False這兩個值,
而是返回它們實際進行比較的值之一。
記住四個大原則,

  1. 元素除了0、空(如: 空字串、空列表、…)、None、False以外都當成是True。
  2. and 運算若全為真,返回最右邊的真值,否則返回第一個假值
  3. or 運算若全為假,返回最右邊的假值,否則返回第一個真值
  4. 若同時有and, or時,and會優先計算 (就像數學算式同時有加減乘除時,先乘除後加減的道理一樣)
    實際看一些例子會比較明白:
>>> 1 and 2 and 3
返回3 (全為真值,返回最右邊的真值)
>>> False or []
返回 [] (全為假值,返回最右邊的假值)
>>> '0' or 0
返回 '0' ('0'不是空字串,是真值)
>>> 1 and {} and False and 3
返回 {} (返回第一個假值)
>>> 1 or 0 and 2
返回 1 (原式相當於 1 or (0 and 2) )
>>> 0 or 1 and 2
返回 2 (原式相當於 0 or (1 and 2) )

若你第一次看到這種機制,
可能會覺得奇怪,
為何規則2和規則3是這樣運作呢?
關鍵在and, or 的判斷中,
常常不需要檢查到最後一個條件即知道結果
這邊and, or 各上一個生活化的例子來說明,

and版本-優先返回第一個假值

假設媽媽下廚,
想要煮牛排給家人吃,
但是食材並不會自己從天上掉下來,
所以媽媽需要去超商買牛排肉。
媽媽會出門買牛排肉需要同時滿足下面三個條件:

  1. 今天是晴天 (因為媽媽覺得雨天出門穿雨衣很麻煩)
  2. 超商有營業
  3. 牛排肉特價20% (媽媽省吃儉用不喜歡買貴)
    邏輯可以寫成
if 今天是晴天 and 超商有營業 and 牛排肉特價20%:
    媽媽會出門買牛排肉

那麼如果第一個條件為假,
也就是今天下雨,
那麼已經可以確定媽媽是不會出門買肉的,
後面兩個條件根本不必檢查。

又或者今天雖然是晴天,
但是很不幸今天碰到國定假日,
超商沒有營業,
那麼牛排肉有沒有特價也就不必再檢查了,
這也就是為什麼and 總先返回第一個假值。

or版本-優先返回第一個真值

現今很多大學都設有英文畢業門檻,
意思是你必須任意選擇一種英文檢定考試通過,
才可以畢業。
假設有一間學校的規定需通過任一種測試方可通過英文畢業門檻:

  1. 參加全民英檢中級英文考試,通過初試
  2. 參加多益英文考試,得到700分以上
  3. 修習語言中心開設「進修英文」課程及格
    邏輯可以寫成
if 全民英檢中級初試通過 or 多益700分以上 or 通過校內進修英文課程:
    通過英文畢業門檻

那麼假設你已經通過全民英檢中級初試了,
學校便不必去檢查你有沒有考過多益英文考試,
或檢查你有沒有上過一門課叫做「進修英文」。
因為第一個條件滿足便確定通過門檻了,
這也就是為什麼or 總是返回第一個真值。

實例應用?

假設nums代表一個正整數的列表,
我們希望寫一個函數,
計算列表的數字總和。
若列表為空,則回傳-1。
直觀的寫法可能為

def sumOfList(nums):
    return sum(nums) if len(nums)!=0 else -1

若是and, or高級語法則長這樣:

def sumOfList(nums):
    return nums and sum(nums) or -1

今天就先學到這裡囉。

課後練習

習題: 判斷閏年

在曆法中,
若n是4的倍數但不是100的倍數,
或者n是400的倍數,
則n 為閏年。
我們希望實作一個函數輸入數字n,
回傳n是否為閏年,
實作如下:

def isLeapYear(n):
    if n%4==0:
        if n%100!=0:
            return True
        else:
            if n%400 ==0:
                return True
            else:
                return False
    else:
        return False

只是目前這個函數邏輯太複雜,
會讓讀者看的頭昏腦脹。
你能夠想到最精簡的寫法為何呢?


上一篇
Day6- 超完整python字串函數用法統整
下一篇
Day8- 第一屠龍刀- range函數與列表切片的融會貫通
系列文
活用python- 路遙知碼力,日久練成精30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
2
ccutmis
iT邦高手 2 級 ‧ 2019-09-11 07:49:21

原來是洋蔥啊,我還以為是PYTHON呢.../images/emoticon/emoticon82.gif

不明 檢舉
【**此則訊息已被站方移除**】
4
一級屠豬士
iT邦大師 1 級 ‧ 2019-09-11 12:18:18
def isLeapYear(n):
    year = int(n)
    return (year % 400 == 0) or ((year % 100 != 0) and (year % 4 == 0))
    

ylist = [2000, 1999, 2001, 2004, 1900]

xlist = [(x, isLeapYear(x)) for x in ylist]

import calendar as cal
xlist2 = [(x, cal.isleap(x)) for x in ylist]

>>> print(xlist)
[(2000, True), (1999, False), (2001, False), (2004, True), (1900, False)]

>>> print(xlist2)
[(2000, True), (1999, False), (2001, False), (2004, True), (1900, False)]
不明 檢舉
【**此則訊息已被站方移除**】
1
sixwings
iT邦研究生 4 級 ‧ 2019-09-19 09:31:44

我最近寫的主題也跟這個有關,寫程式到後面好像就越來越難駕馭邏輯了。
我目前的方向是朝「規則引擎」的方向走,希望大家以後寫程式不會再那麼痛苦了

不明 檢舉
【**此則訊息已被站方移除**】
sixwings iT邦研究生 4 級 ‧ 2019-09-19 10:21:18 檢舉

其實技術的東西一直在變化、寫教學的人也很多,所以我才想說寫一點不一樣的東西。

那個系列的定位算是個人(失敗)經驗分享吧,會帶到技術和工具,不過比較偏向「產品使用心得」的感覺 XD

不明 檢舉
【**此則訊息已被站方移除**】
0
ovenchang
iT邦新手 5 級 ‧ 2020-04-16 13:45:55
def isLeapYear(n):
    return True if not n%4 and n%100 or not n%400 else False

我要留言

立即登入留言