6.進階主題#

生成式(comprehension)#

建立list, dict, set時,有一個十分常用的技巧稱作生成式(comprehension)。 生成式的語法比起使用迴圈簡潔許多,執行速度也比使用迴圈快。

list comprehension#

例如想建立一個內含1~10數字元素的list,該怎麼做?

可能想到的做法是先建立一個空的list,然後利用range()來append每個元素進去:

a_list = []

for i in range(1, 11):
    a_list.append(i)

print(a_list)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

但一個符合python風格的寫法是透過生成式:

a_list = [i for i in range(1, 11)]

print(a_list)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

生成式有許多靈活的應用方法:

例如,可以對每個元素做加工:

a_list = [i**2 for i in range(1, 11)]

print(a_list)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

也可以對元素做篩選:

a_list = [i for i in range(1, 11) if i % 2 == 0]

print(a_list)
[2, 4, 6, 8, 10]

dictionary comprehension#

一個常用生成式來建立字典的時機是結合zip():

keys = ['a','b','c']
values = [1, 2, 3]

a_dict = {k: v for k, v in zip(keys, values)}

print(a_dict)
{'a': 1, 'b': 2, 'c': 3}

或是當你想要交換key跟value的對應關係時:

reverse_a_dict = {v: k for k, v in a_dict.items()}

print(reverse_a_dict)
{1: 'a', 2: 'b', 3: 'c'}

set comprehension#

set也有生成式,寫法如下:

a_set = {i // 4 for i in range(30)}

print(a_set)
{0, 1, 2, 3, 4, 5, 6, 7}

裝飾器(decorator)#

裝飾器的主要作用是為了在不更動function原始程式碼的情況下,添加或改變function的行為。

一個印出function執行時間的範例如下:

from datetime import datetime

def my_timer(func):
    def wrapper(*args, **kwargs):
        start = datetime.now()
        print(f'{func.__name__} starts at {start}')
        result = func(*args, **kwargs)
        end = datetime.now()
        print(f'{func.__name__} ends at {end}')
        print(f'total execution time: {end - start}')
        return result
    return wrapper

當你要使用這個裝飾器時,只要在目標function的定義陳述式上面加上”@”以及裝飾器名稱即可:

import time

@my_timer
def lazy_square(number):
    time.sleep(0.5)
    return number**2

使用該function時就會被添加該function原本沒有的功能:

lazy_square(99)
lazy_square starts at 2024-03-28 07:30:24.868115
lazy_square ends at 2024-03-28 07:30:25.373174
total execution time: 0:00:00.505059
9801

裝飾器的使用時機在於當想一次對多個function添加一些行為時, 如果不使用裝飾器的話就必須一個一個function去修改程式碼。 除了很麻煩以外,也容易造成錯誤。

以上就是裝飾器的基本用法,但這樣的做法會有個小問題。

請看以下程式:

help(lazy_square)
Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

印出的結果是裝飾器中的wrapper()的名稱。

要解決這個問題必須使用python標準函式庫中的一個套件functools, 在內層的wrapper上面加上一個裝飾器@functools.wraps()

from datetime import datetime
import functools # 加這行

def my_timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = datetime.now()
        print(f'{func.__name__} starts at {start}')
        result = func(*args, **kwargs)
        end = datetime.now()
        print(f'{func.__name__} ends at {end}')
        print(f'total execution time: {end - start}')
        return result
    return wrapper

重新定義一次function。

import time

@my_timer
def lazy_square(number):
    time.sleep(0.5)
    return number**2

檢查修改後的結果:

help(lazy_square)
Help on function lazy_square in module __main__:

lazy_square(number)

名稱空間#

我們知道變數名稱是一個標籤,貼在盒子(物件)上面, 當我們呼叫變數時,python會去取用盒子裡面的資料。

但是如果有多個一樣的變數名稱呢?到底要取用哪個物件就會造成混淆。

Python透過名稱空間(namespace)去界定變數名稱的搜尋範圍, 不同名稱空間有不同優先順序,在哪個空間先找到變數名稱,就去取用該變數名稱對應到的物件。

名稱空間依序如下:

  • local

  • enclosing

  • global

  • built-in

說明#

Build-in namespace

Build-in namespace在python啟動時就會建立,直到python編譯器終止為止。

以下是python內建的變數名稱:

dir(__builtins__)
['ArithmeticError',
 'AssertionError',
 'AttributeError',
 'BaseException',
 'BaseExceptionGroup',
 'BlockingIOError',
 'BrokenPipeError',
 'BufferError',
 'BytesWarning',
 'ChildProcessError',
 'ConnectionAbortedError',
 'ConnectionError',
 'ConnectionRefusedError',
 'ConnectionResetError',
 'DeprecationWarning',
 'EOFError',
 'Ellipsis',
 'EncodingWarning',
 'EnvironmentError',
 'Exception',
 'ExceptionGroup',
 'False',
 'FileExistsError',
 'FileNotFoundError',
 'FloatingPointError',
 'FutureWarning',
 'GeneratorExit',
 'IOError',
 'ImportError',
 'ImportWarning',
 'IndentationError',
 'IndexError',
 'InterruptedError',
 'IsADirectoryError',
 'KeyError',
 'KeyboardInterrupt',
 'LookupError',
 'MemoryError',
 'ModuleNotFoundError',
 'NameError',
 'None',
 'NotADirectoryError',
 'NotImplemented',
 'NotImplementedError',
 'OSError',
 'OverflowError',
 'PendingDeprecationWarning',
 'PermissionError',
 'ProcessLookupError',
 'RecursionError',
 'ReferenceError',
 'ResourceWarning',
 'RuntimeError',
 'RuntimeWarning',
 'StopAsyncIteration',
 'StopIteration',
 'SyntaxError',
 'SyntaxWarning',
 'SystemError',
 'SystemExit',
 'TabError',
 'TimeoutError',
 'True',
 'TypeError',
 'UnboundLocalError',
 'UnicodeDecodeError',
 'UnicodeEncodeError',
 'UnicodeError',
 'UnicodeTranslateError',
 'UnicodeWarning',
 'UserWarning',
 'ValueError',
 'Warning',
 'ZeroDivisionError',
 '__IPYTHON__',
 '__build_class__',
 '__debug__',
 '__doc__',
 '__import__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'abs',
 'aiter',
 'all',
 'anext',
 'any',
 'ascii',
 'bin',
 'bool',
 'breakpoint',
 'bytearray',
 'bytes',
 'callable',
 'chr',
 'classmethod',
 'compile',
 'complex',
 'copyright',
 'credits',
 'delattr',
 'dict',
 'dir',
 'display',
 'divmod',
 'enumerate',
 'eval',
 'exec',
 'execfile',
 'filter',
 'float',
 'format',
 'frozenset',
 'get_ipython',
 'getattr',
 'globals',
 'hasattr',
 'hash',
 'help',
 'hex',
 'id',
 'input',
 'int',
 'isinstance',
 'issubclass',
 'iter',
 'len',
 'license',
 'list',
 'locals',
 'map',
 'max',
 'memoryview',
 'min',
 'next',
 'object',
 'oct',
 'open',
 'ord',
 'pow',
 'print',
 'property',
 'range',
 'repr',
 'reversed',
 'round',
 'runfile',
 'set',
 'setattr',
 'slice',
 'sorted',
 'staticmethod',
 'str',
 'sum',
 'super',
 'tuple',
 'type',
 'vars',
 'zip']

在定義變數時,要小心不要使用到這些變數名稱,否則會覆蓋掉。

global namespace

global namespace包含了在主程式中定義的變數名稱,所謂主程式可以先理解成就是正在使用中的jupyter notebook。

The Local and Enclosing Namespaces

至於local namespace就是function在執行時內部的變數名稱空間。

而enclosing namespace則是指當function是多層的時候, 例如雙層的function,外層function的namespace就是所謂的enclosing namespace。

請看下方釋例說明:

def outer():
    print('start outer function')
    namespace = 'outer'

    def inner():
        print('>> start inner function')
        namespace = 'inner'
        print(namespace)
        print('>> end inner function')

    inner()
    print(namespace)
    print('end outer function')

outer()
start outer function
>> start inner function
inner
>> end inner function
outer
end outer function

當我們呼叫outer()時,python會為outer建立新的namespace。 當outer內部呼叫inner()時,python也會為inner建立另一個獨立的namespace。 namespace彼此之間不會互相干擾,確保程式不會有意料之外的事情發生。

此時outer的namespace稱作enclosing namespace, 而inner的namespace則是local namespace。

範例#

以下範例靈感主要來自於Namespaces and Scope in Python – Real Python

範例一

x 定義在f()g()的外面,所以x定義在global namespace中。

x = 'global'

def f():
    def g():
        print(x)
    g()

f()
global

範例二

x 定義了兩次,一個定義在global namespace中,另一個則是在f()裡面,所以是enclosing namespace。

x = 'global'

def f():
    x = 'enclosing'

    def g():
        print(x)
    g()

f()
enclosing

範例三

x 定義了三次,一個在global namespace、一個在enclosing namespace,最後一個則是在g()裡面,所以是local namespace。

x = 'global'

def f():
    x = 'enclosing'

    def g():
        x = 'local'
        print(x)
    g()

f()
local

範例四

無法修改超出名稱空間範圍的變數。

例如我們定義了一個revise_x的function,裡面重新對x賦值, 但這邊的賦值行為只在local namespace中生效,並不會影響到global namespace。

x = 1

def revise_x():
    x = 2
    print(x)

revise_x()
print(x)
2
1

範例五

同樣地,在雙層的function中也是一樣的概念,裡面那層的賦值行為並不會影響到外面。

x = 1

def revise_x_outer():
    x = 2

    def revise_x_inner():
        x = 3
        print('local:', x)

    revise_x_inner()
    print('enclosing:', x)

revise_x_outer()
print('global:', x)
local: 3
enclosing: 2
global: 1

統整#

在取用變數時,python會從local → enclosing → global → build-in逐層搜尋變數名稱。 如果變數名稱都搜尋不到的話,就會丟出NameError,說明變數並不存在。 在修改變數時,變數只在local namespace中作用,並不會影響到外面的namespace。

參考:

Namespaces and Scope in Python – Real Python