0%

林軒田老師的機器學習基石(Machine Learning Foundation)又開始在Coursera上開課囉!雖然老師已經在youtube上公開所有的課程影片還有課程Slide了,不過在Coursera上面跟著每周的課程,也滿有上課的感覺!剛好也針對上課的內容,希望在有時間之餘,隨手寫下一些其中自己覺得印象深刻的部份來加深對機器學習的瞭解。在第一講裡面談到了機器學習的背景,機器學習的過程是從資料出發,經過計算後可以得到某種表現的增進。比如說我們透過股市的資料,經過計算後讓電腦自動投資,來賺到更多的錢。那什麼時候該使用機器學習,其中有提到三個關鍵:

  1. 資料或問題有潛藏的模式(Pattern)可以學習,並得到某種表現的增進
  2. 具有存在的規則,但我們無法得知該如何(或是簡單的)定義
  3. 需要有資料,因為機器學習需要從資料來學習潛在的規則

上面共有四個問題,讓我們思考倒底什麼時候可以使用機器學習(答案是3)。

  1. 預測小孩下次什麼時候哭,因為是沒有pattern所以無法使用機器學習
  2. 要判斷一個graph裡面是否有cycle,其實寫程式就可以判斷,而不用使用到機器學習
  3. 判斷是否要核信用卡,其中包含了pattern,而且不容易被定義,且可以使用歷史資料來學習
  4. 核能是否會毀滅地球,這個問題太有爭議性而且也沒有足夠的資料來學習

在看機器學習的流程前,機器學習有幾個重要的定義:
x: 機器學習的輸入資料,會透過這些輸入作學習
y: 我們想要機器學習告訴我們的結果
f: 目標函數,也可以說是x和y之間的pattern,他定義了x和y之間實際上的關係,因為f是未知的,所以這也是後們想透過機器學習來學到的
D: 即資料,每一組資料包含了x和他相對應的y。d={(x1, y1)…..}
g: 我們希望透過機器學習學到f,但事實上我們並不一定真的能得學到f。所以我們希望機器學習可以學到一個假說(hypothesis)的函數g,而且和f越接近越好

上面可以看到機器學習的流程,透過資料餵給機器學習的演算法來學習到一個函式g,並且可以和f可以很像,這裡的f即是指資料的一種隱藏pattern,而且是未知的,而函式g會從hypothesis的集合內選出一個最符合資料的hypothesis。

機器學習常常被和很多不同領域搞混或是分不出清楚,在課程也有說明機器學習和不同領域的關係

Machine Learning vs Data Mining

資料探勘簡單來說是指,期望從資料庫中大量的資料出發,並從中找到一些有趣的發現來回答問題。以超市來說,會希望透過銷售的資料來找到一個人買了一樣商品後,是否也會買另一樣商品(像是之前的尿布與啤酒都市傳說)。但如果在資料探勘中想要找到有趣的事情,是指要找到一個假說G來作預測的話,那這裡的資料探勘就等同於機器學習。但資料探勘並非都會專注在作預測,有時可能只是想找到一些關聯性。

所以機器學習和資料探勘兩個領或可以說是密不可分的,甚至兩者是可以互補的。比如說可以透過資料探勘找到有趣的性質可以幫助機器學習找到好的假說(可以用在摘取好的特徵值),或是透過機器學習的假說來幫助資料探勘找到有趣的發現。

Machine Learning vs AI

人工智慧: 希望電腦能夠作出智慧的表現,來完成一件事情(開車、下棋、預測)

有很多的方法可以達到不同的人工智慧任務,以下棋來說,傳統的人工智慧作法會設計演算法,透過分析一個game tree(下某一步棋的好處和壞處)來讓電腦自動下棋。如果是使用機器學習方法的話,就是設計演算法來可以告訴機器怎麼下棋會贏怎麼下會輸,然後讓機器分析後自己決定怎麼下(Alpha GO)。所以可以說,機器學習是實現人工智慧的一種方法。

Machine Learning vs Statistics

統計:使用資料來推論原本不知道的事情(ex: 丟銅板的機器)

機器學習中的假說G實際上是一個推論的結果,F則是一個我們不知道的事情,所以統計可以說是實現機器學習的一種方法。我們可以透過很多傳統統計的工具來實現機器學習,將這些工具借過來,使用機器學習的角度來看他。

參考資料:
Machine Learning Foundation 01

隱馬可夫模型(Hidden Markov Model, HMM)是一種具有隱含未知參數的馬可夫鏈(Markov Chain),隱馬可夫模型常被使用在許多AI與Machine Learning的應用。既然隱馬可夫模型是一種馬可夫鏈,一開始先來簡單介紹一下什麼是馬可夫鏈

馬可夫鏈是指從一個狀態轉移到另一個狀態的隨機過程,且下一個狀態只能由當前狀態決定,根據機率分布從一個狀態轉移到另一個狀態,或是保持目前狀態,不同狀態間改變的機率稱為轉移機率(Transition Probability)。

以上面的例子來說明,這是一個具有兩個狀態的馬可夫鏈,S1是雨天(Rainy),S2是晴天(Sunny)。其中S1可以轉移到S2也可以保持在S1,S2可以轉移到S1,也可以保持在S2。使用天氣變化這個例子來說明的話,可以表示成如果今天是雨天,明天可能是晴天也可能維持雨天。

如果我們有一個地區的天氣變化歷史紀錄,就可以統計出此地區晴天和雨天的轉移機率,並算出未來是晴天或是雨天的機率。假設已經知道今天是雨天,我們就可以從上面的樹狀範例可以輕易的計算出後天是雨天的機率是0.61,也可以使用矩算運算來算出後天是雨天和晴天的機率。

再來開始談談什麼是隱馬可夫模型,在隱馬可夫模型被隱藏起來不可見的就是狀態本身,雖然狀態被隱藏起來看不到了,但是卻可以透過「觀察序列」來間接的透露出狀態的訊息。

以wiki上面的天氣例子來說明,假設我本來每天都可以直接看到我居住地區的天氣狀態,有一天我搬家了沒辦法直接觀察原本地區的天氣狀態。雖然我無法直接看到原本地區的天氣狀態,但是我知道住在原本那個地區的朋友,在雨天的時候會有10%機率出門散布、40%機率出門購物,和50%在家打掃;在雨天的時候會有60%機率出門散布、30%機率出門購物,和10%在家打掃。

隱馬可夫模型讓我們可以透過觀察「看得到的」現象去推測另一個「看不到的」現象。以上面舉的例子來說,雖然我無法得知該地區的天氣狀況,但是我就可以透過觀察朋友每天的連續行為,來推測該地區的天氣狀況,並且以上面的方式來呈現一個隱馬可夫模型。

通常隱馬可夫模型有三大重要的問題:

  1. 已知HMM模型,找出一個特定輸出序列的機率
  2. 已知HMM模型,找出一個特定輸出序列的隱藏狀態序列
  3. 未知HMM模型,找到最可能的狀態轉移和輸出機率

不同的演算法都有相對應的演算法可以使用,後續有機會再陸續介紹

參考資料:
Wiki - Hidden_Markov_model
演算法筆記 - HiddenMarkovModel
隱馬可夫模型:探索看不到的世界的數學工具
馬可夫鏈

Python中的decorator是一種對python語法的轉換寫法,使用decorator可以更方便的對函式作修改,也可以提高程式的可讀性。但是decorator只是一種語法糖,使用decorator並不會對語法產生改變,只是讓程式寫法可以更加的簡潔,並且讓開發者可以更方便的使用。

下面開始來說明一下,如何使用decorator來實作在函式的修改:

假設我們寫了一個執行sql的函式:

1
2
def select_execute(sql):    
return 'execute select sql query!'

如果我們想要對這個執行sql的函式,用一個debug模式來包起來,並在debug模式印出執行的sql與執行的時間,這時可以透過closure來處理函式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def debug_query(func):    
def debug_execute(sql):
exec_msg = func(sql)
exec_time = datetime.now()
debug_msg = """
Execute: {sql}
Result: {exec_msg}
Execute Time: {exec_time}"""
return debug_msg.format(**locals())
return debug_execute

>>> sql = 'SELECT * FROM TABLE'
>>> select_execute(sql) # 不使用debug模式來執行select sql
execute select sql query!

>>> func = debug_query(select_execute) # 使用debug模式來執行select sql
>>> func(sql)
Execute: SELECT * FROM TABLE
Result: execute select sql query!
Execute Time: 2017-05-20 16:09:08.953792

從上面的範例可以看到,先將執行sql的函式傳入外部函式,並在內部函式捕捉後,加上debug要印的部份進行改寫,這樣就可以將執行sql的函式包裝成debug模式。下面是使用decorator的寫法

1
2
3
4
5
6
7
8
@debug_query
def select_execute(sql):
return 'execute select sql query!'

>>> select_execute(sql)
Execute: SELECT * FROM TABLE
Result: execute select sql query!
Execute Time: 2017-05-20 16:09:08.953792

只要透過@和改寫的函式名稱,就可以簡化函式傳遞的語法,讓程式碼可以更加簡潔並達到相同效果

decorator也可以支援帶參數,如果今天要簡單的多印出一行debug宣告,並指定是哪一種query type作說明,可以透過多加上一層outter函式來傳入參數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def debug_describe(query_type):    
def debug_query(func):
def debug_execute(sql):
exec_msg = func(sql)
exec_time = datetime.now()
descirbe = 'It is {} debug mode'.format(query_type)
debug_msg = """
{descirbe}
Execute: {sql}
Result: {exec_msg}
Execute Time: {exec_time}"""
return debug_msg.format(**locals())
return debug_execute
return debug_query

>>> func = debug_describe('select')(select_execute) # 不使用decorator寫法
>>> func(sql)
It is select debug mode
Execute: SELECT * FROM TABLEResult: execute select sql query!
Execute Time: 2017-05-20 16:09:08.962798

@debug_describe('select')
def select_execute(sql):
return 'execute select sql query!'

>>> select_execute(sql) # 使用decorator寫法It is select debug mode
Execute: SELECT * FROM TABLE
Result: execute select sql query!
Execute Time: 2017-05-20 16:09:08.962798

參考資料:
PythonDecorators
Understanding Decorators in Python
Python Decorator 四種寫法範例 Code

generator本身也是iterator,和iterator不同的地方在於generator會透過function宣告的方式,但有別於一般的function直接return回傳值,generator的function會在”有需要”的時候,使用yield來回傳值。為什麼稱為”有需要”的時候呢?因為generator在每次呼叫next()回傳值後,會記下所有的資料並保留最後一次執行狀態,直到下一次呼叫next()才會再繼續執行未結束的狀態。我們可以看一下下面這個範例:

1
2
3
def transform(data):    
for char in data:
yield chr(ord(char) + 1) # 透過yield回傳值後留存狀態

在每一次呼叫next()時,generator才會開始執行,並且每次執行到yield回傳值後停止,並保留執行狀態直到下一次呼叫next()才繼續執行。當generator終止後呼叫next()則會發生StopIteration。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> char = transform('python')
>>> next(char)
q
>>> next(char)
z
>>> next(char)
i
>>> next(char)
p
>>> next(char)
p
>>> next(char)
o
>>> next(char)
Traceback (most recent call last):
File <stdin>, line 1, in
next(char)StopIteration

也可以使用for loop來呼叫generator

1
2
3
4
5
6
7
8
9
for char in transform('python'):     
print(char)

q
z
u
i
p
o

使用generator最大的好處在於可以有效的處理一次性使用大量使用Memory的情況,將要一次性處理的過程拆成generator分次執行。例如當需要讀取很大的檔案處理時(甚至是多個很大的檔案)。而且使用generator可以降低iterator在loop的維度,讓程式碼可以更加的乾淨。

1
2
3
4
5
6
7
8
9
def get_line_title(file_name):    
for file in file_name:
for line in open(file, mode='r', encoding='utf-8'):
if line.istitle():
yield line

>>> lines = get_line_title(file_name)
>>> for line in lines:
>>> print(line)

上面的範例會產生generator並逐次判斷每一行內容,並回傳所有的標題。其中會用到兩個for loop和一個邏輯判斷,我們可以改寫成下面這樣,把讀取的部份和邏輯判斷的部份拆開來。

1
2
3
4
5
6
7
8
9
10
def get_line(files):    
for file in files:
for line in open(file, mode='r', encoding='utf-8'): yield line

def get_title(lines):
return (line for line in lines if line.istitle())

>>> all_line = get_line(file_name)
>>> for title in get_title(all_line):
>>> print(title)

如果善用generator可以有效的管理Memory的使用,也可以讓程式碼更加的有可讀性!

參考資料:
Python Docs - Generators
Iterators & Generators
Introduction to Python Generators

python中有些資料結構是可以透過for來loop裡面所有的item的,像是list, tuple, dict, str還有file都可以。在使用上其實for迴圈會呼叫物件中的iter(),此函式會回傳一個Iterator(迭代器),Iterator會透過next函式來一次一次的取得下一個item,並在沒有下一個item的時候拋出StopIteration來告訴for迴圈迭代結束。

像這種有按照Iteration Protocol定義iternext的物件又被稱為Iterable

1
2
3
4
5
>>> for item in [1, 2, 3, 4]
>>> for item in (1, 2, 3, 4)
>>> for item in {'one': 1, 'two': 2}
>>> for item in 'python'
>>> for line in open('file')

如果希望自己定義的物件是Iterable,只要透過在class內實作iternext(讓python的build-in函式iter()和next()可以呼叫)就可以達成。

假設我們建立了一個物件可以設定一個字串,並在每次iteration的時候透過ascii回傳每個字元的下一個字元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Transform:    
def init(self, data):
self.data = data
self.i = 0
self.n = len(data)

def iter(self):
return self

def next(self):
if self.i == self.n:
raise StopIteration
item = chr(ord(self.data[self.i]) + 1)
self.i = self.i + 1
return item

宣告一個Transform物件,並用for來讀取每一個值

1
2
3
4
5
6
7
8
9
>>> t = Transform('python')
>>> for item in t: # for迴圈自動叫iter(),並用next()來作iteration
>>> print(item)
q
z
u
i
p
o

宣告一個Transform物件,並呼叫iter並用next來讀取每一個值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> t = Transform('python')
>>> t2 = iter(t)
>>> next(t2)
q
>>> next(t2)
z
>>> next(t2)
u
>>> next(t2)
i
>>> next(t2)
p
>>> next(t2)
o
>>> next(t2) # 當沒有下一個item之前,會產生StopIteration
Traceback (most recent call last):
File <stdin>, line 1, in
next(t2)
StopIteration

除了透過定義iternext讓自己的物件為iterable,也可以透過在class中定義getitem來達成。兩者只要有定義其中一種,都可以讓for loop來依序讀取物件中的item

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Transform:    
def init(self, data):
self.data = data
self.i = 0
self.n = len(data)

def getitem(self, i):
return chr(ord(self.data[i]) + 1)

>>> t = Transform('python')
>>> for item in t: # 透過定義__getitem__也可以讓物件為iterable,並可以使用在for loop
>>> print(item)
q
z
u
i
p
o

參考資料:
Python docs - Iterators
Python docs - Iterator types
Python docs - Iterable

tuple在python中是一種不可變動(immutable)的資料結構,相較於list是可變動的(mutable),兩者共同都可以使用index來讀取值,但因為tuple為不可變動,所以一經宣告即無法修改其中的值。

1
2
3
4
5
>>> drink = ('Black Tea', 'regular', 'sugar free')  # 我要一杯紅茶,正常冰量不加糖
>>> drink[0] # 可以使用index來讀取tuple中的item
Black Tea
>>> drink[0] = 'Green Tea' # tuple為不可變動,所以無法透過assign來修改其中的值
TypeError: 'tuple' object does not support item assignment

tuple很適合用來儲存一些不需要頻繁變動的資料,但是在使用上要使用index來讀取tuple中item的值,若是tuple中的item數量少也許還沒問題,但如果tuple中的item數量很多,甚至是開發一段時間又沒寫註解,往往最後這種index的讀取就變成一種魔術數字(magic number),造成維護上的困難。

namedtuple是tuple的擴展,因為namedtuple在使用上可以透過類似讀取欄位(field)名稱的方式,來取用tuple不同的item資料,所以有時候namedtuple就很適合用來代替tuple儲存資料。如果有需要頻繁讀取tuple中的item,於是你開始想建立class和物件來操作,但是資料欄位又沒有到這麼複雜到需要額外建class,這種情況很適合。

下面馬上來介紹一下namedtuple的使用方法

1
2
3
4
5
6
7
8
9
10
from collections import namedtuple

>>> Drink = namedtuple('Drink', 'product, ice, sugar') # 宣告一種名稱(Drink)且包含三個欄位(product, ice, sugar)
>>> black_tea = Drink('Black Tea', 'regular', 'sugar free') # 我要一杯紅茶,正常冰量不加糖

Drink(product='Black Tea', ice='regular', sugar='sugar free')
>>> black_tea.product # 可以使用一開始定義的欄位名稱來讀取值
Black Tea
>>> black_tea[0] # 當然也可以使用index來讀取值
Black Tea

宣告namedtuple需要給定兩個參數,第一個是tuple的名稱(name),再來是這個tuple包含的欄位名稱(field_name)。欄位名稱使用字串格式代入所有需要的欄位(其中的,可加可不加)。以上面的例子來看,tuple名稱就是Drink,欄位名稱是product, ice和sugar三個。之後透過宣告的namedtuple來儲存資料,就可以使用欄位名稱來讀取item值了!

namedtuple可以和tuple一樣作unpack

1
2
3
4
5
>>> product, ice, sugar = black_tea
>>> sugar
sugar free
>>> ice
normal

也有支援直接從list和dict轉成namedtuple

1
2
3
4
5
6
7
>>> l = ['Bubble Tea', 'regular', 'regular']
>>> bubble_tea = Drink._make(l) # 使用_make來將list轉成namedtuple
Drink(product='Bubble Tea', ice='regular', sugar='regular')

>>> d = {'product': 'Bubble Tea', 'ice': 'regular', 'sugar': 'regular'}
>>> bubble_tea = Drink(**d) # 使用拆開映射運算符(double-star-operator)來拆開dict轉成namedtuple
Drink(product='Bubble Tea', ice='regular', sugar='regular')

將namedtuple轉成dict(此dict為collection裡面OrderDict)

1
2
>>> bubble_tea_dict = bubble_tea._asdict()
OrderedDict([('product', 'Bubble Tea'), ('ice', 'regular'), ('sugar', 'regular')])

如果今天已經存在一個namedtuple,需要使用現存的namedtuple來新增一個新的namedtuple也可以使用_filed來辦到

1
2
3
4
5
6
>>> Ingredient = namedtuple('Ingredient', 'tapioca_pearls, grass_jelly, flan')
>>> Order = namedtuple('Order', Drink._fields + Ingredient._fields)

# 我要一杯奶茶,正常冰半糖加布丁
>>> drink_order = Order('Milk Tea', 'regular', 'half sugar', False, False, True)
Order(product='Milk Tea', ice='regular', sugar='half sugar', tapioca_pearls=False, grass_jelly=False, flan=True)

在開發過程中,如果可以巧妙的使用namedtuple,可以讓程式更加有可讀性,也可以讓幫助下降日後的維護成本!

Python docs - namedtuple() Factory Function for Tuples with Named Fields

之前曾介紹過Python透過LEGB規則來參照變數,今天要介紹有兩個python的關鍵字可以打破這樣的規則

1. global

如果我們需要在函式內部修改global變數,可以使用global來宣告函式內的變數,再進一步作修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var = 5
def access_local():
var = 10
def access_global():
global var
var = 15

>>> print('global is ' + str(var))
global is 5
>>> access_local()
>>> print('global is ' + str(var))
global is 5
>>> access_global()
>>> print('global is ' + str(var))
global is 15

呼叫access_local的函數後,此時依照LEGB的規則,access_local內的var為local變數,因此對var作任何操作都不會影響到global變數。但在呼叫access_global函數後,此時函式內的var經過global宣告後即為global變數,此時對var作的操作就會影響到原本的global變數。

2. nonlocal

nonlocal和global類似,nonlocal用來讓宣告在內層巢狀函式中的變數,不再是local變數,而是轉為外層的enclosed函式的變數。

1
2
3
4
5
6
7
8
9
10
11
12
13
def target_value(x):
def compare(y):
nonlocal x
x = 3
return '> target' if y > x else '< target'
return compare

func = target_value(10)
print(func(5))
> target

print(func.__closure__[0].cell_contents)
3

以之前介紹過的的Closure例子來說明,本來設定10當作x target,當內層函式去捕捉到enclosed的自由變數來比較時,會得到”< target”。本來捕捉到的自由變數只能讀取而無法修改,但是現在在內層函式透過nonlocal來宣告x變數,即可以將local變數轉為外層的enclosed變數,當自由變數x被修改成3之後,最後的比較結果會變數”> target”

參考資料:
python keyword list

談到closure,首先要先瞭解First-cass Function,所謂的first-class function是指函式可以被assign給變數或儲存於資料結構中,並且可以作為其他函數的參數,或是作為其他的函數的回傳值。而python裡的每個function都是first-class function,所以在python中可以作到以下的操作:

1
2
3
4
5
6
7
8
def max(m, n):    
return m if m > n else n

>>> max(5, 3)
5
>>> func = max
>>> func(7, 5)
7

我們可以把max這個函式assign給func,再使用func來實際使用max這個函式。

要建立一個closure,在python中可以透過建立巢狀函式(nested function)來實作,其中內部函式會依據LEGB規則參照Enclosed scope的外部函式變數,此變數又稱為自由變數(free variable)。自由變數將會和此函式同時存在,即使離開了創造此變數的環境/區域也可以被此函式使用不會消失。

1
2
3
4
5
6
7
8
9
10
11
12
13
def target_value(x):    
def compare(y):
return '> target' if y > x else '< target'
return compare

>>> func = target_value(10)
>>> func(5)
< target
>>> func2 = target_value(3)
>>> func2(5)
> target
>>> func.__closure__[0].cell_contents
10

上面是一個closure的例子,首先呼叫外部函式target_value將參數傳入,並回傳內部函式compare且assign給func,這是一個first-class function的實作。而每一次呼叫target_value後就會產生一個closure實例,每一個實例會binding到不同的自由變數x,它並不會因為離開了創造它的target_value函式而消失,反而在透過func呼叫compare時被closure補捉使用。python還有個__colsure__可以看到在enclosed scop所補捉到的自由變數

總結來說,並不是每個巢狀函式都是closure,巢狀函式需要補捉到enclosed scope的自由變數,使得此變數在脫離原本的環境仍可被內部函式使用,這樣才為closure。

在使用上closure並不是一定要有的設計方式,但是有時使用closure可以幫助有效的組織程式碼,並提升程式碼的可讀性。像是當我們有資料需要重複使用,但是又不想都塞在global,這時可以透過自由變數來存放再用closure捕捉來降低global變數的數量;另外也可以透過closure來隱藏變數,作出private的效果。

2016/11/14

捕捉到自由變數並不能作修改,如果需要修改自由變數,可透過nonlocal來宣告變數,並作進一步的操作

參考資料:
Wiki-First-Class Function
Wiki-Closure
認識 Lambda/Closure(3)Python 對 Lambda/Closure 的支援

python的namespace是一種”name-to-object”的對應關係,而在不同的namespace之間都有各自的作用域,就像是不同的module中雖然都定義了相同的名稱的函式,但卻不會互相影響。為了找到特定的”name-to-object”的namespace,python會採用scope的概念來找到特定object的所屬變數或是函式,而搜尋順序即是LEBG規則。

python的scope規則LEGB查找順序分別為 Local -> Enclosed -> Global -> Built-in

Local: 宣告於於function或是class內的name
Enclosed: 封閉的function,function被另一個function包起來,常見為Closure
Global: 最上層位於module的global name
Build-in: 內建module的name,例如print, input等等

以下舉幾個範例說明:

1. L - local scope
1
2
3
4
5
6
7
var = 'global'    
def L():
var = 'local'
print(var)

>>>> L() # local 當function有定義local scope時,會優先使用local scope
local
2. LE - local and Enclosed scope
1
2
3
4
5
6
7
8
9
var = 'global'
def LE():
var = 'enclosed'
def inner():
print(var)
inner()

>>>> LE() # 當function沒有local scope時,會往外找enclosed scope
enclosed
3. LEG - local, Enclosed and Global scope
1
2
3
4
5
6
var = 'global'
def LEG():
print(var)

>>> LEG() # 當local和enclosed都沒有時,會往外找global
global
4. LEGB - local, Enclosed, Global and Build-in scope
1
2
3
4
5
6
7
8
9
10
def id(var):    
return 'global id()'

def LEGB(var):
print(id(var))
print(str(var))

>>> LEGB('Build-in str()') # 當有定義global function,會呼叫global function,否則會呼叫Build-in function
global id()
Build-in str()
5. NameError - name is not defined
1
2
3
4
5
6
>>> var = 'global'
>>> print(var2) # 當LEGB scope都找不到,會發生NameError
Traceback (most recent call last):
File “LEGB.py”, line 39, in &lt;module>
print(var2)
NameError: name 'var2' is not defined

以上幾個是python LEGB scope順序的範例,了解LEGB的觀念也有助於幫助自己在開發的時候,避免誤用scope產生許多bug

參考資料:
A Beginner’s Guide to Python’s Namespaces, Scope Resolution, and the LEGB Rule
Python docs - Scopes and Namespace

在寫sql的時候,一開始總是習慣帶入一支完整的sql,遇到要替換的地方就在程式用取代來處理。例如下面這個範例,為了insert不用的資料,用format來作字串替代完成一支sql

1
2
3
>>> sql = "INSERT INTO ALBUM(TITLE, ARTIST_ID) VALUES ('{0}',{1})"
>>> sql = sql.format('The Archive', '1')
INSERT INTO ALBUM(TITLE, ARTIST_ID) VALUES ('The Archive',1)

可以看到要insert的資料,如果是char型態會用’’括起來。但是當今天要取代的字串有特殊符號或是有引號處理起來就會很麻煩,像下面的例子在insert到db時就會造成syntax error

1
2
3
>>> sql = "INSERT INTO ALBUM(TITLE, ARTIST_ID) VALUES ('{0}',{1})"
>>> sql = sql.format("It's Time", '2')
INSERT INTO ALBUM(TITLE, ARTIST_ID) VALUES ('It's Time',2) # syntax error

所以再來要介紹一下用bind variable下SQL的好處,下面是一個oracle使用bind variable的方式,在執行SQL時,會將變數直接帶入:1和:2的位置,所以也不用考慮任何原本要處理的跳脫字元問題

1
2
sql = "INSERT INTO ALBUM (TITLE, ARTIST_ID) VALUES (:1, :2)"
cursor.execute(sql, ("It's Time", 2))

使用bind variable不止在處理SQL很方便之外,也可以避免有SQL injection的風險,甚至對操作DB的performance也有影響。

例如對Oracle DB下SQL時,oracle首先會在share pool找到這支SQL是否之前曾執行過,有的話會取得這支SQL之前處理過的執行計畫(Execute Plan)並開始執行這支SQL;如果找不到的話oracle會hard parsing這支SQL,並找到最佳的執行計畫後才執行SQL。所以如果能在share pool找到之前有執行過的執行計畫,SQL執行前就不用再經過hard parsing。

但是oracle在share pool尋找的時候,只有完全相同的SQL才會被認為有對應到。所以當我們在程式使用取代字串來完成SQL,在執行時每一支SQL都會被視為不同的SQL,自然每一次的SQL執行前都必須要經過hard parsing

1
2
INSERT INTO ALBUM(TITLE, ARTIST_ID) VALUES ('The Archive',1)  # 兩支SQL會被oracle視為不同的SQL
INSERT INTO ALBUM(TITLE, ARTIST_ID) VALUES ('Hear Me',3)

為了避免這個問題,可以使用bind variable來下SQL,因為採用bind variable的SQL會被視為同一支SQL

1
2
3
sql = 'INSERT INTO ALBUM(TITLE, ARTIST_ID) VALUES (:1, :2)'
cursor.execute(sql, ("It's Time", 2)) # 兩支SQL會被oracle視為相同的SQL
cursor.execute(sql, ("Hear Me", 3))

因此採用bind variable在操作SQL上不止處理起更方便(不用特別處理跳脫字元),也更安全(避免SQL injection),甚至更快速(重使用最佳化過後的執行計畫)