0%

談到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),甚至更快速(重使用最佳化過後的執行計畫)

很特別的可以直接當作一個函式的參數來使用。如果當作函式的參數,可以用來限定*之後不可以使用位置引數,但可以使用關鍵字引數,這樣的使用方法被稱為Keyword-Only Arguments

以下以位置引數(Positional Argument)與關鍵字引數(Keyword Argument)中的計算小考平均成績為例說明:

1
2
def total_score(eng_score, math_score, eng_weight=0.5, math_weight=0.5):    
return eng_scoreeng_weight + math_scoremath_weight

在這個範例中,可以使用以下4種方式來呼叫函式都是沒問題的

1
2
3
4
5
6
7
8
>>> total_score(80, 90)  # 省略且不指定有預設值的參數
85
>>> total_score(eng_score=80, eng_weight=0.2, math_score=90, math_weight=0.8) # 使用關鍵字引數,可隨意安排順序
88
>>> total_score(80, 90, math_weight=0.8, eng_weight=0.2) # 混用位置和關鍵字引數
88
>>> total_score(80, 90, 0.2, 0.8) # 單純使用位置引數
88

但如果今天在原本的函式中,把*加入到第三個參數,這樣在第四種單純使用位置引數來作函式呼叫就會引發TypeError,因為在之後不能使用位置引數

1
2
3
4
5
def total_score(eng_score, math_score, * , eng_weight=0.5, math_weight=0.5):    
return eng_scoreeng_weight + math_score*math_weight

>>> total_score(80, 90, 0.2, 0.8) # 單純使用位置引數
TypeError: total_score() takes 2 positional arguments but 4 were given

今天如果要設計成防止呼叫者使用任何位置引數,就可以將*做為第一個參數,這樣就可以強迫叫者全部使用關鍵字引數,若使用任何的位置引數,都會引發TypeError

1
2
3
4
5
6
7
8
9
10
11
def total_score(*, eng_score, math_score, eng_weight=0.5, math_weight=0.5):    
return eng_scoreeng_weight + math_score*math_weight

>>> total_score(80, 90) # 省略且不指定有預設值的參數
TypeError: total_score() takes 0 positional arguments but 2 were given

>>> total_score(80, 90, math_weight=0.8, eng_weight=0.2) # 混用位置和關鍵字引數
TypeError: total_score() takes 0 positional arguments but 2 positional arguments (and 2 keyword-only arguments) were given

>>> total_score(80, 90, 0.2, 0.8) # 單純使用位置引數
TypeError: total_score() takes 0 positional arguments but 4 were given

PEP 3102 – Keyword-Only Arguments

我們可以透過拆開運算符,來達到建立具有接收不固定位置引數和不估定關鍵字引數的函式。假設我們今天要建立一個,可以讀取不固定引數數量的函式,就會很有用。

舉例來說,當我們要建立一個能將所有引數平方相加函式,可以使用下列寫法:

1
2
3
4
5
6
7
8
9
10
11
12
def sum_of_power(args):
result = 0
for arg in args:
result += arg ** 2
return result

>>> sum_of_power(1, 2, 3, 4)
30
>>> sum_of_power([2, 2, 2])
12
>>> sum_of_power(*[3, 4, 5, 6][2:]))
61

有*前綴的argv表示函式接收的引數是一個元組(type(args)會得到<class ‘tuple’=””>),像是把傳入的多個引數packing成一個元組,再透過拆開運算符來將元組拆開成多個位置引數作處理。而關鍵字引數也可以應用在位置引數之後,假如我們要讓相加的引數改成可指定的次方向,可在函式加上關鍵字引數:

1
2
3
4
5
6
7
8
9
10
def sum_of_power(args power=2):
result = 0
for arg inargs:
result += arg * power
return result

>>> sum_of_power(1, 2, 3, 4) # 僅指定位置引數
30
>>> sum_of_power(1, 2, 3, 4, power=1) # 同時指定位置引數和關鍵字引數
10

有兩個*前綴的kwargv表示接收的引數是一個字典(type(kwargs)會得到<class ‘dict’=””>),像是把傳入的多個關鍵字引數packing成一個字典,再透過拆開運算符來將字典拆開成多個關鍵字引數作處理。假設今天要針對不同會員設定多個會員詳細資料,可以用下列寫法:

1
2
3
4
5
6
7
8
9
def personal_detail(account, **kwargs):    
print('Account detail: ' + account)
for key in kwargs.keys():
print(key + ': ' + kwargs[key])

>>> personal_detail('a1234@gmail.com', first_name='Tom', last_name='Lin')
Account detail: a1234@gmail.com
first_name: Tom
last_name: Lin

呼叫此函式時,只允許傳入一個位置引數,或是可以用關鍵字引數方式,來額外提供多個資訊。

參考資料:
位置引數(Positional Argument)與關鍵字引數(Keyword Argument)
透過*和**來對群集資料Unpacking
Python docs - Arbitrary Argument Lists

Python函式參數所接收的引數(Argument),主要可以分成兩種,分別為位置引數(Positional Argument)和關鍵字引數(Keyword Argment),以下我們先來看個例子,說明什麼是位置引數

1
2
3
4
5
def total_score(eng_score, math_score, eng_weight, math_weight):    
return eng_scoreeng_weight + math_scoremath_weight

>>> total_score(80, 90, 0.5, 0.5)
85

假設今天我們寫了一個簡單的函式來計算小考的平均成績,共有英文和數學兩科,兩科各有一個權重比例來計算出綜合成績。這個函式要傳入4個引數,所以在呼叫時我們必須提供所有的引數(即4個),而傳入的引數會被設成相應位置上參數的值。

eng_score被設成80、eng_weight被設成0.2、math_score被設成90、math_weight被設成0.8,這就是位置引數

函式接收的參數,也可以指定一個預設值。假設預設的計算分數的權重都是固定一半一半,我們可以在函式撰寫時就給予一個預設的權重值。有預設值的參數為選用參數,在沒有傳入引數的情況下,python會使用預設值;而沒有預計值的參數為必要參數,在呼叫函式時必須傳入所有必要參數

1
2
def total_score(eng_score, math_score, eng_weight=0.5, math_weight=0.5):    
return eng_scoreeng_weight + math_scoremath_weight

若要改變參數的預設值,我們也可以直接使用額外的位置引數,就可以取代原來的預設值,或者也可以使用關鍵字引數來傳遞引數。以下為幾個呼叫的範例:

1
2
3
4
5
6
7
8
>>> total_score(80, 90)  # 省略且不指定有預設值的參數
85
>>> total_score(eng_score=80, eng_weight=0.2, math_score=90, math_weight=0.8) # 使用關鍵字引數,可隨意安排順序
88
>>> total_score(80, 90, math_weight=0.8, eng_weight=0.2) # 混用位置和關鍵字引數
88
>>> total_score(80, 90, 0.2, 0.8) # 單純使用位置引數
88

其中可以看到使用關鍵字引數的好處在於,關鍵字引數可以讓函式呼叫變得具彈性,即針對自己所需的選用參數來指定引數;甚至也可以讓函式呼叫更加具可讀性,特別在傳入布林引數時。例如在排序時決定要不要作逆向排序,sorted(text, reverse=True)比起sorted(text, True)可讀性來的更高

有兩個要注意的地方,第一個是函式語法不允許具預設值的參數之後接非預設值的參數,會引發SyntaxError。例如:

1
2
def total_score(eng_score, eng_weight=0.5, math_score, math_weight=0.5):
>>> SyntaxError

再來是傳入函式的引數,若是位置引數和關鍵字引數混用的話,位置引數務必放在關鍵字引數的前面,否則也是會引發SyntaxError。所以total_score(math_weight=0.8, eng_weight=0.2, 80, 90)是不行的

在處理python的序列資料(tuple, list)或是映射資料(dict),並要把資料傳入function時,可使用*和**運算符來分別對兩種資料作unpacking並傳入函式

針對序列(tuple, list)可以使用拆開序列運算符(sequence unpacking operator),在變數前加上*前綴來達成

1
2
3
4
5
6
7
8
9
10
11
12
def product(a, b, c):
return a * b * c

T = (2, 5, 10)
product(T[0], T[1], T[2]) # 用切片讀出tuple資料傳入函式
product(*T) # 直接將tuple unpacking成3個資料項傳入函式
product(T[0], *T[1:]) # 先切出1個資料項,再拆開2個資料項

L = [2, 5, 10]
product(L[0], L[1], L[2]) # 用切片讀出list資料傳入函式
product(*L) # 直接將list unpacking成3個資料項傳入函式
product(L[0], *L[1:]) # 先切出1個資料項,再拆開2個資料項

上面的範列product函式會接受3個引數,透過*來直接對序列作unpacking,就能將序列內的資料項直接傳入函式,而不用先讀出來或是對序列作切片(slice)

針對映射資料(dict)則是使用拆開映射運算符(mapping unpacking operator),在變數前加上**前綴

1
2
D = {'a': 2, 'b': 5, 'c': 10}
product(**D)

上面範例會將dic以key-value拆開,在傳入函式後會將每個key的value分配給與key相同名稱的參數(parameter)。要注意的是,如果字典裡面的key和函式參數對應不起來,則會引發TypeError。

除非函式的參數有設定預設值(Default),例如以下範例函式參數d有預計值20,若字典沒有d,d在函式中就會帶入預設值而不會引發TypeError

1
2
3
4
5
def product(a, b, c, d=20):
return a * b * c * d

D = {'a': 2, 'b': 5, 'c': 10}
product(**D)

參考資料:
函式參數預設值請參考: 位置引數(Positional Argument)與關鍵字引數(Keyword Argument)
Python docs - unpacking argument lists

寫python在做字串格式化的時候,常常會使用format來達成。一般簡單的format用法,會使用大括弧加數字編號配合引數順序做取代

1
2
>>> str = "My name is {0}, and I'm {1} years old".format('Tom', 18)
"My name is Tom, and I'm 18 years old"

在Python3.1開始可以省略數字編號

1
2
>>> str = "My name is {}, and I'm {} years old".format('Tom', 18)
"My name is Tom, and I'm 18 years old"

或是使用欄位名稱來取代對應引數

1
2
str = "My name is {name}, and I'm {age} years old".format(name='Tom', age=18)
"My name is Tom, and I'm 18 years old"

format可以搭配dict一起使用,將dict傳入並取出其中的value來完成取代

1
2
3
>>> d = {'name': 'Tom', 'age': 18}
>>> str = "My name is {0[name]}, and I'm {0[age]} years old".format(d)
"My name is Tom, and I'm 18 years old"

既然可以使用dict來結合format做字串取代,所以我們可透過內建的locals()來取得一個儲存當前區域變數的dict,並把區域變數的值帶入fomat來取代對應引數

1
2
3
4
>>> name = 'Tom'
>>> age = 18
>>> str = "My name is {0[name]}, and I'm {0[age]} years old".format(locals())
"My name is Tom, and I'm 18 years old"

我們可以再進一步使用拆開映射(maping unpacking)運算來將dic拆開來傳入format,python就會自動填入對應的值

1
2
3
4
>>> name = 'Tom'
>>> age = 18
>>> str = "My name is {name}, and I'm {age} years old".format(**locals())
"My name is Tom, and I'm 18 years old"

任何的dict都可以使用,上面的例子可以改寫成

1
2
3
>>> d = {'name': 'Tom', 'age': 18}
>>> str = "My name is {name}, and I'm {age} years old".format(**d)
"My name is Tom, and I'm 18 years old"

參考資料:
Python Unpacking請參考: 透過*和**來對群集資料Unpacking