iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 8
0
Software Development

從零開始學Python系列 第 8

[Day 08] 從零開始學Python - 程式結構與流程語法:如果對手太弱太簡單,那不是很爽嗎?(下)

註:本文同步刊載在Medium,若習慣Medium的話亦可去那邊看呦!

在上一篇文章中我們講了if...elif...else, while, for, 以及range,
我們也提到了像range()這樣子的方法,是用來生成一個可迭代的Python物件。
而只要可迭代的話,我們通常就可以將其轉成一個list,如上一篇文章那樣。
不管是if, while, for等,都可以使用多層的架構,
對於while跟for來說,其處理方式也是類似多層if的概念,
同時外層內層的東西不同也是可以的。
例如:

while xxx:
    ooo
    rrr
    while yyy:
        uuu
        vvv
    www

這個雙重迴圏,執行的順序應該是:
xxx成立時,進入迴圏->ooo->rrr->
yyy成立時,進入迴圏->uuu->vvv->
內層迴圏完成一次,重新檢查yyy,若成立則繼續運行,
否則就會執行完www後,回到xxx檢查。
(所以說內層的執行完,才會輪到外層)

今天我們要介紹一個在Python中很好用且常用的東西,
它也用到了for...in...的方法,名字叫做list comprehension
可以用來快速簡單地生成list,
中文目前沒有統一的叫法,可以叫成串列生成式/串列表達式/串列解析式都行。
其基本形式像這樣:
[算式 for 單項 in 迭代項目]

舉例來說,假設我們要一個從0~9的串列,我們可以這樣寫:

>>> list(range(9))
[0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> [i for i in range(9)]
[0, 1, 2, 3, 4, 5, 6, 7, 8]

串列生成式的意義在於每次從迭代的項目當中取出一個單項,
接下來將這個單項透過算式進行加工處理,才放到list當中。
所以除了基本的方法以外,我們也可以用if條件式來進行過濾,只要在後面加上if條件式即可。

>>> [2 * i for i in range(9) if i % 2 == 0] # 取0~8當中能整除2的數,每個都先乘以2再加到list
[0, 4, 8, 12, 16]

另一方面,我們也可以利用多重的for in來製造出雙層的list或者組合性質的東西。
我們前面提到過list裡面的element也可以是list,
如果我們今天要表達一組4 * 3的格子,格子裡面一開始只放零的話,
可以寫成:

>>> [[0 for i in range(3)] for j in range(4)] # 一層一層看就會理解等於[0, 0, 0]重複4次
[[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]

如果我想要把所有的座標(從(0, 0)起算)的組合寫到一個list呢?

>>> combo = [(row, col) for row in range(4) for col in range(3)] # 留意4跟3的順序
>>> combo
[(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2), (3, 0), (3, 1), (3, 2)]

(註:像這樣子在存取每一個格子都需要指定兩層index的串列,
我們稱之為二維串列,代表它有兩個維度。)

字典和集合也有生成式,其概念和列表生成式一致,
只是字典的話就會是{key:value for 單項 in 迭代項} (必須給出key跟value的部分)
集合的話除了外面是大括號外,和列表生成式一致。

接下來我們要來談一個程式語言很重要的東西:函式(function)
還記得前面我們有做過計算圓面積的練習對吧?
計算圓面積明明是一個不會改變的公式,
但我們還是要反覆改動代入值,這樣顯得有點麻煩。
為了不要浪費時間在做相同的事情上,
我們可以將寫好的一段程式碼(比如剛剛講的算面積),定義成函式,
就像數學的函式那樣,公式已經寫好了,只差將要算的東西代入進去就好。

一個函式的程式架構一般如下:

def 函式名(變數1, 變數2, 變數3, ...):
    xxx
    yyy
    ...
    return ooo

def代表define(定義),後面接你的命名,
括號內是放入你需要從呼叫函式的對象那邊得到的變數(或者稱為參數),
也可以都不放,表示你不需要從外界傳入;
接著就在這個函式內按照一般程式碼的流程來撰寫,
最終如果這個函式需要回報一些訊息的時候,則需要進行return(回傳)

我們拿圓面積計算為例:

def area(r):
    pi = 3.14
    return 3.14 * r ** 2 # 會將計算完的結果輸出到呼叫它的地方

由於Python是腳本式語言,
一個函式必需要先被定義或引入後,
才能夠被使用,所以我們必須在使用(或稱呼叫)函式前,
先將它定義完,在下一行開始才能夠使用它。

# area(1) 不能放在這邊,因為我們還沒有定義area函式!
def area(r):
    pi = 3.14
    return 3.14 * r ** 2
	
print('半徑長為1的圓,其面積為:' + str(area(1)))
print('半徑長為3的圓,其面積為:' + str(area(3)))

執行結果如下:

C:\Users\Desolve>python fromzero.py
半徑長為1的圓,其面積為:3.14
半徑長為3的圓,其面積為:28.26

假設我們今天不滿足圓周率pi只用3.14的話,
除了用math模組的pi外(引入模組這部分之後再講),
那麼就必須將pi也一併設定為可以拿進來用的變數。
但是一般來說,像pi這類型的常數,
我們會希望**「不特別講的話就按照預設的來」
這時候我們可以利用在
給入的變數的後面加上等號及一個值**,
一旦這個變數沒有被指定,就會使用預設值
此外,很多時候乍看之下,我們會容易不曉得一個函式中,
每一個位置先後要代入什麼才是對的順序,
為此,Python提供我們可以指定變數名稱的方式,
只要輸入時有帶上變數名稱,即便順序和原先函式定義給的順序不一樣,
Python依舊可以正確地幫我們對應上。

def area(r, pi=3.14):
    return pi * r ** 2

print('半徑長為1的圓,其面積為:' + str(area(1, 3.14159))) # 指定圓周率
print('半徑長為3的圓,其面積為:' + str(area(3))) # 不指定圓周率,使用預設值3.14
print('半徑長為3的圓,其面積為:' + str(area(pi=3.141, r=3))) # 順序會被自動對上

結果如下:

C:\Users\Desolve>python fromzero.py
半徑長為1的圓,其面積為:3.14159
半徑長為3的圓,其面積為:28.26
半徑長為3的圓,其面積為:28.269

同樣的,在函式內部也可以定義函式,
一樣的概念,要有定義過的才能被呼叫使用,
同時,可以接受參數,但也可以直接用其外層得到的變數。

def printAll(r, pi=3.14):
	def area():
		return pi * r ** 2
	def perimeter():
		return 2 * pi * r
	# 下面{}的用法是所謂的format,可以將多個變數按照順序放置到{}中
	print('半徑 = {}的圓,其周長 = {},面積 = {}'.format(r, area(), perimeter()))
	
printAll(3, 3.14159)

執行結果如下:

C:\Users\Desolve>python fromzero.py
半徑 = 3的圓,其周長 = 28.27431,面積 = 18.849539999999998

我們前面已經提過,
變數就像是將一個標籤(變數名稱)貼在一個裝有東西(資料)的箱子。
那麼,你家的急救箱和我家的急救箱
雖然名字都是急救箱,但應該是指不一樣的東西。

在Python或其他程式語言中也一樣,
一個名稱,有屬於它存在的適用範圍,我們稱之為命名空間。
如果在最外面(也就是沒有在函式中)命名一個變數的時候,
任何人應該都看得到這個變數的存在,並且可以自由使用它,
我們稱之為全域變數

那麼在函式內呢?
如果今天在公共的地方有一個急救箱,你又自己去買了一個急救箱回來,
那麼在命名時,你在你家稱呼的急救箱,自然是你自己買的那個,而非公共的。
(稱為區域(local)變數)
除非你都用公共的,那麼自然你指的還是那個公共的急救箱(函式內可以取得全域變數)
如果打定主意要用全域變數,且有要進行修改的話,請在函式內使用global這個關鍵字。

fak = 'global' # First-Aid Kit

def home1():
	print(fak) # 直接取得全域的變數,不做修改

def home2():
	fak = 'h2' # 定義一個local的變數,所以修改到的變數跟全域的fak無關
	print(fak)
	
def home3():
	global fak # 告訴Python現在要用的就是全域的那個fak
	fak = 'h3'
	print(fak)

print('Before:')
print(fak)
print('\nhome1:')
home1()
print('After home1:')
print(fak)

print('\nhome2:')
home2()
print('After home2:')
print(fak)

print('\nhome3:')
home3()
print('After home3:')
print(fak)

所以我們可以看到這三者之間的差異,
一個只印出全域變數,一個只使用區域變數,第三個則是不但使用了全域變數且修改了它。

C:\Users\Desolve>python fromzero.py
Before:
global

home1:
global
After home1:
global

home2:
h2
After home2:
global

home3:
h3
After home3:
h3

註:其實還有更細節的部分,但講太多恐怕對初學者容易造成混淆,
我們還是先講這兩種就好,其他的在碰到狀況,或有興趣的話,
再請讀者搜尋變數範圍的部分。

那麼我們來做個練習吧!

  1. 請使用兩個迴圏,將1~10之間的偶數兩兩相乘並放到一個空的list中。
    (所以這個list應該會有2 * 2, 2 * 4, 2 * 6, 2 * 8, 2 * 10, 4 * 2, 4 * 4, ..., 10 * 10)
  2. 請改用列表生成式來完成1的問題。
  3. 請用while, if else等,寫出一個猜數字的遊戲,遊戲的答案為37,
    請在開始時提示使用者猜1~100範圍中的數字,
    並依據使用者的答案,逐步將範圍縮小,直到猜中答案,則印出恭喜訊息並離開迴圏。
    (先不考慮使用者會亂輸入的問題,並且要告訴使用者這次猜的比答案大還是小)

那就明天見啦!


上一篇
[Day 07] 從零開始學Python - 程式結構與流程語法:如果對手太弱太簡單,那不是很爽嗎?(上)
下一篇
[Day 09] 從零開始學Python - 例外處理、遞迴:誰用三生浮世的煙火,換你一次長憶的交錯
系列文
從零開始學Python30

尚未有邦友留言

立即登入留言