## 從 Closure 到 Late Binding:
### Python 變數作用域與執行行為探討

---
## Speaker Information
### 曾昱翔
+ Software engineer
+ ArchLinux enthusiast
+ Working for [KCI Global](https://www.kci-global.com.tw/zh-tw/)
<!-- .slide: style="text-align: left" -->
----

---
## 考慮下面程式輸出
```python=
def func(i):
return i
lst = [lambda: i for i in range(5)]
lst2 = [func(i) for i in range(5)]
lst3 = [lambda: func(i) for i in range(5)]
for i in lst:
print(i())
for i in lst2:
print(i)
for i in lst3:
print(i())
```
----
## 答案如下
```python=
def func(i):
return i
lst = [lambda: i for i in range(5)]
lst2 = [func(i) for i in range(5)]
lst3 = [lambda: func(i) for i in range(5)]
for i in lst:
print(i()) # 4 4 4 4 4
for i in lst2:
print(i) # 0 1 2 3 4
for i in lst3:
print(i()) # 4 4 4 4 4
```
### Why?
<!-- .element: class="fragment" data-fragment-index="1" -->
---
## Lambda?
```python
lst = [lambda: i for i in range(5)]
```
+ 上面程式意義為:「建立 5 個 lambda 函式,每個函式執行時<span style="color: aqua;">回傳變數 i 的值</span>」。
+ 其中變數 i 為同一個變數,因次會印出 i 最後的值,也就是 4 。
----
## Function
```python
lst2 = [func(i) for i in range(5)]
```
+ 上面程式則為:「將 `func(i)` 的回傳值放入 `lst2` 中」。
+ 故函式會在宣告 `List` 時,直接被呼叫。
----
## PEP 規範
根據 [PEP 227 – Statically Nested Scopes](https://peps.python.org/pep-0227/#implementation) 中提到關於 `closure` 及 `nested functions` 的相關實作如下:
```
Implementation
The implementation for C Python uses flat closures [1]. Each def or lambda expression that is executed will create a closure if the body of the function or any contained function has free variables. Using flat closures, the creation of closures is somewhat expensive but lookup is cheap.
The implementation adds several new opcodes and two new kinds of names in code objects. A variable can be either a cell variable or a free variable for a particular code object. A cell variable is referenced by containing scopes; as a result, the function where it is defined must allocate separate storage for it on each invocation. A free variable is referenced via a function’s closure.
The choice of free closures was made based on three factors. First, nested functions are presumed to be used infrequently, deeply nested (several levels of nesting) still less frequently. Second, lookup of names in a nested scope should be fast. Third, the use of nested scopes, particularly where a function that access an enclosing scope is returned, should not prevent unreferenced objects from being reclaimed by the garbage collector.
```
----
## PEP 227 重點節錄
根據前一頁提及的 [PEP 227](https://peps.python.org/pep-0227/#implementation) 重點節錄如下:
> 1. `The implementation for C Python uses flat closures.`
> CPython 使用 `flat closures` 這種方式來實作閉包功能(即所有需要捕捉的變數會被打平成一個陣列或表格)。
> 2. `Each def or lambda expression that is executed will create a closure if the body … has free variables.`
> 如果一個 `def` 或 `lambda` 有用到外部變數(自由變數),就會建立 `closure` 。
> 3. `A variable can be either a cell variable or a free variable for a particular code object.`
> 一個變數可以是:
> + cell variable(`co_cellvars`): 函式定義,被內部函式引用。
> + free variable(`co_freevars`): 函式<span style='color: aqua;'>引用</span>外部函式的變數。
> 4. ` A cell variable is referenced by containing scopes ... A free variable is referenced via a function’s closure.`
> 自由變數透過 `closure` 保存的 `reference` 來<span style='color: aqua;'>引用</span>。
----
## 結論
```python
lst = [lambda: i for i in range(5)]
```
#### 根據上頁 PEP 規範可以如下說明原本的程式:
1. Lambda function 內有引用到外部變數 `i` (因為 `i` 不是當成 `argument` 傳入,而是引用外部變數)。
2. 因為有用到外部變數,所以建立 `flat closure` 在有需要的時候索引外部變數。
3. 此時對於 lambda 內而言,`i` 是 `自由變數` ,透過 `closure` 中所記住的 `reference` 在呼叫時存取。
4. 外部 `i` 會執行 `for` 迴圈內的敘述,從 0 累加到 4 ,最後停在 4 。
5. 呼叫 lambda function 的時候會回傳 `reference i` ,進而發生 `late binding` 。
<!-- .element: style="font-size: 24px;" -->
---
## 如何證明
根據 [Python 3.13 官方文件](https://docs.python.org/3.13/reference/datamodel.html) 內提及,可以關注幾個 `attributes`:
<!-- .element: style="font-size: 30px;" -->
+ `function.__code__`:
- `codeobject.co_cellvars`:包含函式內定義的變數的 `Tuple`。
- `codeobject.co_freevars`:包含所有引用的 `自由變數` 的 `Tuple`。
+ `function.__closure__`:`None` 或一個包含所有引用的 `自由變數` 的 `Tuple`。
<!-- .element: style="font-size: 26px;" -->
----
## 設計實驗
```python=
def print_vars(func):
print(f'{func.__name__} freevars: {func.__code__.co_freevars}')
print(f'{func.__name__} cellvars: {func.__code__.co_cellvars}')
print(f'{func.__name__} closures: {func.__closure__}')
def outer():
def inner():
b = a
a = 123
print_vars(outer)
print_vars(inner)
outer()
```
上面程式在 `outer` 內宣告一個 `cellvar` `a`,接著在 `inner` 內存取外部變數 `a`,
此時應會建立 `closure` 並且紀錄 `a` 在 `inner` 的 `flat closure` 內。
<!-- .element: style="font-size: 26px;" -->
----
## 驗證結果
```
outer freevars:
outer cellvars: ('a',)
outer closures: None
inner freevars: ('a',)
inner cellvars: ()
inner closures: (<cell at 0x6ffdfa99c100: int object at 0x6ffdfb4e5310>,)
```
+ 根據程式結果可以看出, `a` 在 `outer` 內宣告,
並根據 `PEP 227` 在呼叫時為其分配空間。
+ `inner` 中用到外部變數 `a` ,所以建立 `closure` 並且
紀錄在 `freevars` 的 `Tuple` 中,在需要的時候會進行引用。
<!-- .element: style="font-size: 26px;" -->
----
## 回歸原問題
```python
lst = [lambda: i for i in range(5)]
for idx, func in enumerate(lst):
print(f"lst[{idx}]: id: {id(func)}, closure: {func.__closure__}")
```
上面的驗證步驟也可以套用到原本的問題上,結果如下:
<!-- .element: style="font-size: 26px;" -->
```
lst[0]: id: 137640870293920, closure: (<cell at 0x7d2f0332bdf0: int object at 0x7d2f03ee4430>,)
lst[1]: id: 137640870737248, closure: (<cell at 0x7d2f0332bdf0: int object at 0x7d2f03ee4430>,)
lst[2]: id: 137640870737408, closure: (<cell at 0x7d2f0332bdf0: int object at 0x7d2f03ee4430>,)
lst[3]: id: 137640870737568, closure: (<cell at 0x7d2f0332bdf0: int object at 0x7d2f03ee4430>,)
lst[4]: id: 137640870737728, closure: (<cell at 0x7d2f0332bdf0: int object at 0x7d2f03ee4430>,)
```
可以看出,每個 `lambda` 都不一樣,但 `closure` 內引用的都是同一個 `int` 物件。
<!-- .element: style="font-size: 26px;" -->
所以最後輸出都一樣
----
## 結論
從前面的官方文件和實驗可以總結如下:
+ 當 `Function` 內有引用外部變數時會建立 `closure`。
+ CPython 使用 `flat closure` 實作,
執行期間可以透過 `__closure__` 查看 `Tuple` 內紀錄的變數。
+ 自由變數在呼叫時才存取,即 `late binding` ,可能發生預料之外的行為。
<!-- .element: style="font-size: 26px;" -->
{"title":"從 Closure 到 Late Binding: Python 變數作用域與執行行為探討","contributors":"[{\"id\":\"c74c9513-d06f-4364-9ff5-7d632f117e30\",\"add\":6971,\"del\":646,\"latestUpdatedAt\":1757005962553}]","slideOptions":"{\"transition\":\"slide\"}","description":"上面程式意義為:「建立 5 個 lambda 函式,每個函式執行時<span style=\"color: red;\">回傳變數 i 的值</span>」。"}