Advanced Materials of Python

Table of Contents

Hits

1. Function II

1.1. Function的特性

1.1.1. Fnction為物件

1: def yell(text):
2:     return text.upper() + '!'
3: 
4: print(yell('hello'))
5: bark = yell # assign給其他變數
6: print(bark('woof'))
HELLO!
WOOF!

1.1.2. Function可被放在資料結構中

1: def yell(text):
2:     return text.upper() + '!'
3: 
4: bark = yell
5: 
6: funcs = [bark, str.lower, str.capitalize]
7: 
8: for func in funcs:
9:     print(func('hey there!'))
HEY THERE!!
hey there!
Hey there!

1.1.3. Function可做為參數傳給其他function

Function是物件,所以除了能指定給變數,更可以當成其他function的參數。

 1: def yell(text):
 2:     return text.upper() + '!'
 3: 
 4: bark = yell
 5: 
 6: def greet(func):
 7:     greeting = func('Hi, I am a Python Program')
 8:     print(greeting)
 9: 
10: greet(bark)
HI, I AM A PYTHON PROGRAM!

這種能接續其他function做為參數的格式也稱為高階function(higher-order function)。Python內建的map function即為higher-order的經典範例,它接受一個function object與一個可走訪物件(iterable)做為參數,然後會套用該function到iterable object內的每一元素,產出一系列結果:

1: list = map(function, iterable object)

例如:

1: def yellp(text):
2:     return text.upper() + '!'
3: 
4: bark = yell
5: 
6: list1 = ['hello', 'hey', 'hi']
7: print(list(map(bark, list1)))

1.1.4. Function可構成巢狀結構

在function內定義function,稱之為巢狀function(nested function)或內部function(inner function)

1: def speak(text):
2:     def whisper(t):
3:         return t.lower() + '...'
4:     return whisper(text)
5: 
6: print(speak('Hello, world'))
hello, world...

每次呼叫speak時,它會先定義內部function,而這個inner function在speak外並不存在。若一定要在speak之外存取內部function whisper,則要先把內部function傳回給父function的呼叫者:

 1: def get_speak_func(volume):
 2:     def whisper(text):
 3:         return text.lower() + '...'
 4:     def yell(text):
 5:         return text.upper() + '!'
 6:     if volume > 0.5:
 7:         return yell
 8:     else:
 9:         return whisper
10: 
11: print(get_speak_func(0.3))
12: print(get_speak_func(0.7))
<function get_speak_func.<locals>.whisper at 0x101986af0>
<function get_speak_func.<locals>.yell at 0x101986a60>

1.1.5. Inner function可記住父function的參數狀態

Function還可「捕捉並保留父function的部份狀態」:

 1: def get_speak_func(text, volume):
 2:     def whisper():
 3:         return text.lower() + '...'
 4:     def yell():
 5:         return text.upper() + '!'
 6:     if volume > 0.5:
 7:         return yell
 8:     else:
 9:         return whisper
10: 
11: func = get_speak_func('Hello, world', 0.7)
12: print(func())
HELLO, WORLD!

這種會捕捉並記住外部參數的function,稱之為詞法閉包(lexical closuer),或簡稱閉包(closure)。閉包會記住外圍程式範圍裡的變數值,即便其程式流程已經離開該變數所在的範圍也一樣。

1.1.6. 物件也能像function一樣被呼叫

在Python中,function皆為object,反之object不見得是function,不過,我們能讓不是function的object變成可呼叫(callable),許多情況下,甚至可以把callable object當成function,在後面加上小括號來呼叫它,甚至能傳入參數。要使object變為callable,方法是在類別加入__call__:

1: class Addr:
2:     def __init__(self, n):
3:         self.n = n
4:     def __call__(self, x):
5:         return self.n + x
6: 
7: plus_3 = Addr(3) #建立Addr物件,屬性n=3
8: print(plus_3(4))
7

1.2. lambda

lambda提供了可用來宣告小型匿㢱function的快速宣告方式:

函式名稱 = lambda 參數:運算式

這種語法也稱為function expression,這與用def宣告的function一樣

1: add = lambda x, y : x + y
2: 
3: print(add(5, 3))
8

那麼,以lambda定義function的優勢為何?

1: print((lambda x, y : x + y)(5, 3))
8

在許多場合中,使用lambda來定義臨時性的function就比較方便,但labmda function有個語法上的限制:只能含有一條運算式。

lambda function使用時機: 任何需要快速用上function object的地方,例如sorted():

1: sorted(iterable object, key=function)

其中參數key可輸入一個function, sorted()會依據function的傳回值來排序物件的元素,如果忽略key參數,則會依據原始元素(或者元素的第一個值)來排序。

1: tuples_list = [(1, 'd'), (2, 'b'), (4, 'a'), (3, 'c')]
2: print(sorted(tuples_list))
3: 
4: print(sorted(tuples_list, key=lambda x: x[1])) #指定key以tuple元素的第二子元素來排序
5: print(sorted(range(-5, 6), key=lambda x: x**2)) #指定依平方值來排序
6: # 上述方式只是為展示lambda用法,更精簡的方式為:
7: print(sorted(range(-5, 6), key=abs))
[(1, 'd'), (2, 'b'), (3, 'c'), (4, 'a')]
[(4, 'a'), (2, 'b'), (3, 'c'), (1, 'd')]
[0, -1, 1, -2, 2, -3, 3, -4, 4, -5, 5]
[0, -1, 1, -2, 2, -3, 3, -4, 4, -5, 5]

使用lambda的缺點:不易閱讀

1.3. decorator

Python的修飾器(decorator)可用來間接修改callable object(function, method, class)的行為,但不直接改動object本身,其用途包括:

  • 加入日誌(logging)
  • 存取權限管控與身份驗證
  • 加入監測function及衡量執行時間
  • 限制function執行頻率
  • 用快取暫存function的執行結果

1.3.1. 修飾無參數function

 1: def uppercase(func):
 2:     def wrapper():
 3:         original_result = func()
 4:         modified_result = original_result.upper()
 5:         return modified_result
 6:     return wrapper
 7: 
 8: @uppercase #以uppercase修飾greet
 9: def greet():
10:     return 'Hello'
11: 
12: print(greet())
HELLO

1.3.2. 多重修飾

 1: def strong(func):
 2:     def wrapper():
 3:         return f'<strong> { func() } </strong>'
 4:     return wrapper
 5: 
 6: def emphasis(func):
 7:     def wrapper():
 8:         return f'<em> { func() } </em>'
 9:     return wrapper
10: 
11: @strong
12: @emphasis
13: def greet():
14:     return 'Hello!'
15: 
16: print(greet)
17: print(greet())
<function strong.<locals>.wrapper at 0x10fd3ab80>
<strong> <em> Hello! </em> </strong>

唯一要注意的是:若套用修飾器的層級過多會影響程式執行效能。

1.3.3. 修飾有傳入參數的function

若要修飾的function有帶參數,則wrapper()要改為wrapper(*args, **kwargs),這分別用來收集所有傳入的positional與keyword類參數,以tuple及dict形式儲存在arfs與kwargs變數裡,然後wrapper利用*與**將收集到的參數unpack後傳給原始func,如此就不會限制到參數個數。

 1: def trace(func):
 2:     def wrapper(*args, **kwargs):
 3:         print(f'trace: callable function {func.__name__}, with arguments: {args}, {kwargs}')
 4:         original_results = func(*args, **kwargs)
 5:         print(f'trace: function {func.__name__} reutrn results: {original_results}!')
 6:         return original_results
 7:     return wrapper
 8: 
 9: @trace
10: def say(name, line):
11:     return f'{name}, {line}'
12: 
13: print(say('James', 'hi'))
trace: callable function say, with arguments: ('James', 'hi'), {}
trace: function say reutrn results: James, hi!
James, hi

幫function加上效能監測功能

 1: import time
 2: def eval_time(func):
 3:     def wrapper(*args, **kwargs):
 4:         start = time.perf_counter()
 5:         res = func(*args, **kwargs)
 6:         finish = time.perf_counter()
 7:         print(f'Finished in {round(finish-start, 2)} second(s)')
 8:         return res
 9:     return wrapper
10: 
11: @eval_time
12: def sigma(n):
13:     sum = 0
14:     for i in range(n):
15:         sum = sum + i
16:     return sum
17: 
18: print(sigma(100000000))
Finished in 5.3 second(s)
4999999950000000

1.4. *args and **kwargs

*與**能讓function接受數量不定的額外參數,讓module與class能提供更有彈性的存取介面:

 1: def foo(required, *args, **kwargs):
 2:     print(required)
 3:     if args:
 4:         print(f'args: {args}')
 5:     if kwargs:
 6:         print(f'kwargs: {kwargs}')
 7: 
 8: foo('1. Hi')
 9: foo('2. Hi', 1, 2, 3)
10: foo('3. Hi', 1, 2, 3, Key1='value1', key2=456)
1. Hi
2. Hi
args: (1, 2, 3)
3. Hi
args: (1, 2, 3)
kwargs: {'Key1': 'value1', 'key2': 456}
  • *args接收額外的位置型參數(positional paramenters)(沒有鍵的參數),將之放入一個tuple。
  • **kwargs接收額外的關鍵字參數(keyword parameters)(有鍵的參數),將之放入一個dict。

1.5. Unpack function parameters

*與**也可用來unpack function的args與kwargs,在呼叫function時,若在iterable object前加上*,就會unpack這個物件,將元素當成個別的positional parameters傳入參數;若加上**,則可unpack keyword parameters。

1: def print_vector(x, y, z):
2:     print(f'<{x}, {y}, {z}>')
3: 
4: tuple_vector = (1, 0, 1)
5: print_vector(tuple_vector[0], tuple_vector[1], tuple_vector[2])
6: print_vector(*tuple_vector)
7: 
8: dict_vector = { 'y': 4, 'z': 5, 'x': 3}
9: print_vector(**dict_vector)
<1, 0, 1>
<1, 0, 1>
<3, 4, 5>

1.6. Return or not

  • return 與return None效果一樣
  • 如果沒有傳回值,則可省略return,例如,一個只負責print的function並無任值可傳回,寫return會很奇怪。

2. Class v.s. Object

2.1. TODO 什麼是class? 什麼是object?

把這個看完: https://youtu.be/JeznW_7DlB0:

Everything in Python is object.

1: a = 2022
2: b = 'TNFSH'
3: 
4: def c():
5:     print('TNFSH')
6: 
7: print(type(a))
8: print(type(b))
9: print(type(c))
<class 'int'>
<class 'str'>
<class 'function'>

由上面的例子,我們可以發現:在Python裡,幾乎所有我們用到的變數、函數都是class。
當我們寫下

1: a = 2022

其實是在說,我們有一個變數,它是一種int class,它的值是2022。這個變數就是一個object: 某種屬於某一類class的實體。
一種常見的說法是:class是一張藍圖,依照這張藍圖蓋出的房子有同樣的架構,有同樣的功能,但是可能在外觀或顏色上會有些許差異,就好比一樣是整數物件,每個變數的值可能會有所不同。

不同的class有其特定的功能與限制,例如:

1: x = 1
2: y = 'TNFSH'
3: print(x+y)

執行上述程式會得到以下錯誤訊息:

  File "<stdin>", line 3, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

這裡告訴我們:int與str這兩種class的object不支援+這個運算。我們再來看另一個例子:

1: x = 'tnfsh'
2: y = 1
3: print(x.upper())
4: print(y.upper())
TNFSH

上述程式會產生如下錯誤訊息:
#+begin_src shell -r -n :results output :exports both
Traceback (most recent call last):
File “<stdin>”, line 4, in <module>
AttributeError: ’int’ object has no attribute ’upper’
#+end_src<<sh
意思是:int這種class的object沒有一種叫做upper的屬性。這就是我們上面所說的,不同的class有其特定的功能與限制。

2.2. 建立自己的class

1: class Dog:
2:     def bark(self):
3:         print('汪汪汪')
4: 
5: a = Dog()
6: print(type(a))
7: a.bark()
<class '__main__.Dog'>
汪汪汪

在上述範例中,我們建立了一個叫Dog的class,然後再建立一個屬於Dog這個class的object。
從這個object的type,可以看出這是一個叫<class ’main.Dog’>的類別,這裡的__main__意思是這個class被定義(或撰寫)在目前這支Python程式中,當然我們也可以另外開一個新的.py檔專問來儲存這個class。

2.3. == v.s. is

  • ==: equal
  • is: identical
1: a = [1, 2, 3]
2: b = [1, 2, 3]
3: c = a
4: print(a == b)
5: print(a is b)
6: print(a == c)
7: print(a is c)
True
False
True
True

2.4. Shallow copy v.s. Deep copy

簡單來說,淺與深的區別1

  • 淺複製僅複製容器中元素的地址
  • 深複製完全複製了一份副本,容器與容器中的元素地址都不一樣

2.4.1. shallow copy

 1: xs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
 2: ys = list(xs) # shallow copy
 3: print(xs == ys) # 二者內容相同
 4: print(xs is ys) # 但為不同物件
 5: 
 6: xs.append([10, 11, 12])
 7: print(xs)
 8: print(ys)
 9: 
10: # 但若是修改二者相同的部份,如
11: xs[1][0] = 'X'
12: print(xs)
13: print(ys)
True
False
[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
[[1, 2, 3], ['X', 5, 6], [7, 8, 9], [10, 11, 12]]
[[1, 2, 3], ['X', 5, 6], [7, 8, 9]]

由上述例子中可發現,xs與ys其實共享同一份資料。

2.4.2. deep copy

 1: import copy
 2: xs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
 3: zs = copy.deepcopy(xs)
 4: 
 5: print(xs == zs)
 6: print(xs is zs)
 7: 
 8: xs[1][0] = 'X'
 9: print(xs)
10: print(zs)
True
False
[[1, 2, 3], ['X', 5, 6], [7, 8, 9]]
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

2.4.3. copy any object

不管想copy什麼object,包括自訂物件,解決之道亦是用copy module的copy()與deepcopy()。

 1: import copy
 2: 
 3: class Point:
 4:     def __init__(self, x, y):
 5:         self.x = x
 6:         self.y = y
 7:     def __repr__(self):
 8:         return f'Point({self.x}, {self.y})'
 9: 
10: a = Point(23, 42)
11: b = Point(23, 42)
12: #b = copy.copy(a)
13: print(a)
14: print(b)
15: print(a == b)
16: print(a is b)
Point(23, 42)
Point(23, 42)
False
False

2.5. ABC: Abstract Base Class

C++、Java等語言都有「介面」(interface)這種本身無法建立object、卻能當成建立object template的機制。Python則可藉由ABC來實作。藉由ABC來建立class,便是可確保繼承之sub-class均有正確的特定method。

 1: from abc import ABC, abstractmethod
 2: 
 3: class Base(ABC):
 4: 
 5:     @abstractmethod
 6:     def foo(self):
 7:         pass
 8: 
 9:     @abstractmethod
10:     def bar(self):
11:         pass
12: 
13: class Concrete(Base):
14:     def foo(self):
15:         pass
16: 
17: print(issubclass(Concrete, Base))
True

2.6. object/class/static method

 1: class MyClass:
 2: 
 3:     def objmethod(self): #object method
 4:         print(f'呼叫object method: {self}')
 5: 
 6:     @classmethod
 7:     def clsmethod(cls):
 8:         print(f'呼叫class method: {cls}')
 9: 
10:     @staticmethod
11:     def stcmethod():
12:         print(f'呼叫static method')
13: 
14: obj = MyClass()
15: obj.objmethod()
16: obj.clsmethod()
17: obj.stcmethod()
呼叫object method: <__main__.MyClass object at 0x1081f7940>
呼叫class method: <class '__main__.MyClass'>
呼叫static method
  • objmethod為一般object method,必須有self參數,該參數指向object本身
  • csmethod為class method,必項有cls參數,指向class本身
  • stcmethod為static method,沒有參數,無法修改物件或類別狀態,只能處理user傳入的參數

2.6.1. DEMO

 1: class Drink:
 2:     def __init__(self, name, price, counts):
 3:         self.name = name
 4:         self.price = price
 5:         self.counts = counts
 6: 
 7:     def costs(self):
 8:         return self.drink_costs(self.price, self.counts)
 9: 
10:     @classmethod
11:     def blacktea(cls):
12:         return cls('紅茶', 1, 25)
13: 
14:     @classmethod
15:     def coffee(cls):
16:         return cls('熱美式', 1, 50)
17: 
18:     @staticmethod
19:     def drink_costs(price, counts):
20:         return price * counts
21: 
22: drink1 = Drink.blacktea()
23: drink2 = Drink.coffee()
24: drink3 = Drink('義式', 60, 3)
25: print(f'{drink1.name}')
26: print(f'{drink2.price}')
27: print(f'{drink3.name!r} * {drink3.counts} = {drink3.costs()}')
紅茶
1
'義式' * 3 = 180

2.7. class variable v.s. instance variable

  • class variable: 宣告在classs定義裡,但宣告位置在所有method之外
  • instance variable: 屬於由class所建立的某個instance,其內容並非存在class中,而是由各別instance負責

2.7.1. 差異

 1: class Dog:
 2:     num_dogs = 0 #Demo class variable的適用時機
 3:     num_leg = 4
 4: 
 5:     def __init__(self, name):
 6:         self.name = name
 7:         self.__class__.num_dogs += 1
 8: 
 9: dd = Dog('DaiDai')
10: bd = Dog('Bad luck')
11: lk = Dog('Lucky')
12: bd.num_leg = 3
13: print(dd.num_leg)
14: print(bd.num_leg)
15: print(f'目前共有{Dog.num_dogs}隻狗')
4
3
目前共有3隻狗

由class variable num_dogs的示範也可看出,善用class variable可以在各個instance object間取得某程程度的連繫。

3. 資料型別 II

3.1. Dict

3.1.1. collections.ChainMap

collections.ChainMap可以把多個dict集結串成單一個dict,如

 1: import collections
 2: 
 3: dict1 = {'one':1, 'two': 2}
 4: dict2 = {'two':'貳', 'three': 'III', 'four': 4}
 5: chain = collections.ChainMap(dict1, dict2)
 6: 
 7: print(chain)
 8: print(chain['two'])
 9: chain['five'] = 5
10: print(chain)
ChainMap({'one': 1, 'two': 2}, {'two': '貳', 'three': 'III', 'four': 4})
2
ChainMap({'one': 1, 'two': 2, 'five': 5}, {'two': '貳', 'three': 'III', 'four': 4})

要留意的是:若對ChainMap做新增、修改、刪除,則只會影響裡面的第一個dict。

3.2. types.MappingProxyType

透過MappingProxyType class可以把dict變為唯讀。

1: from types import MappingProxyType
2: 
3: writable = {'one': 1, 'two': 2}
4: writable['three'] = 3
5: readOnly = MappingProxyType(writable)
6: readOnly['three'] = 4 #TypeError: 'mappingproxy' object does not support item assignment
7: readOnly['four'] = 4 #TypeError: 'mappingproxy' object does not support item assignment

3.3. array.array

若要和C語言程式交換資料,或是只需要一個儲存數值型態資料的空間,則可用array.array,優點是比list或tuple省空間。array.array的method與list大致相同,許多情況下甚至可直接交換二者的資料型別而無需修改相關程式碼。

1: import array
2: arr = array.array('f', (1.0, 3.5, 2.0, 6.1))
3: print(arr)
4: print(arr[1])
5: arr.append(2.1)
6: del arr[1]
7: print(arr)
array('f', [1.0, 3.5, 2.0, 6.099999904632568])
3.5
array('f', [1.0, 2.0, 6.099999904632568, 2.0999999046325684])

3.4. Record

同樣以儲存car record(color, mileage, automatic)來比較

3.4.1. list/tuple

  • 建立速度快、執行效率高,
  • 但沒有欄位名稱,建立資料時可能弄錯順序
  • 很難檢查兩個資料物件是否有一致的欄位
1: car1 = ['red', 3812, True] #list, 可變
2: car2 = ('blue', 3123, False) #tuple, 不可變
3: car3 = (343, 'black', True, 'James') #順序錯誤、欄位也不一致

3.4.2. dict

  • 可透過key/value來當成欄位名稱
  • 仍缺乏欄位監控機制,防止使用者在建立object時打錯或漏打欄位
1: Car = {
2:     'color': 'red',
3:     'mileage': 3213,
4:     'automatic': True
5: }
6: print(Car)
7: print(Car['color'])
8: Car['windshield'] = 'broken'
9: print(Car)
{'color': 'red', 'mileage': 3213, 'automatic': True}
red
{'color': 'red', 'mileage': 3213, 'automatic': True, 'windshield': 'broken'}

3.4.3. 自訂類別

  • 麻煩,在要__init__建構子內設定每個欄位,還要自己寫__repr__ method
1: class Car:
2:     def __init__(self, color, mileage):
3:         self.color = color
4:         self.mileage = mileage
5: 
6: car = Car('red', 1234)
7: car.mileage = 4343 #可修改欄位內容
8: car.automatic = True #可新增欄位

3.4.4. typing.NamedTuple

  • readonly
  • 欄位型別無強制性
 1: from typing import NamedTuple
 2: 
 3: class Car(NamedTuple):
 4:     color: str
 5:     mileage: float
 6:     automatic: bool
 7: 
 8: car = Car('red', 3123, True)
 9: print(car)
10: #car.mileage = 3123 # 不能變更欄位內容
11: #car.windshield = 'broken' #不能新增欄位
12: car2 = Car('blue', 'test', 1234)
13: print(car2)
Car(color='red', mileage=3123, automatic=True)
Car(color='blue', mileage='test', automatic=1234)

3.4.5. types.SimpleNamespace

  • 本質上是dict,把dict的key變成class attribute
  • 可以新增、修改、刪除attribute
  • 不若想動用到class,此為簡易替代品
  • 仍缺乏結構
1: from types import SimpleNamespace
2: 
3: car = SimpleNamespace(color = 'red',
4:                       mileage = 1341,
5:                       automatic = True)
6: 
7: car.color = 'blue'
8: print(car)
namespace(color='blue', mileage=1341, automatic=True)

3.4.6. dataclass (Python 3.7+)

  • 能自動建立__init__、__repr__等method
  • 可加入自訂method
  • 可繼承
  • 可設為read only(在@dataclass後加入參數 frozen=True)
 1: from dataclasses import dataclass
 2: 
 3: @dataclass
 4: class Car:
 5:     color: str
 6:     mileage: float
 7:     automatic: bool
 8: 
 9:     def mileage_km(self): #可自訂method
10:         return self.mileage * 1.609
11: 
12: car = Car('red', 3123, True)
13: print(dir(car))
14: print(car.mileage_km)
15: print(car.__repr__)
16: 
17: @dataclass
18: class ElectronicCar(Car): #繼承
19:     charge: float = 0.0
20: 
21: car2 = ElectronicCar('white', 3123, True, 1000)
22: print(car2)
23: 
['__annotations__', '__class__', '__dataclass_fields__', '__dataclass_params__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'automatic', 'color', 'mileage', 'mileage_km']
<bound method Car.mileage_km of Car(color='red', mileage=3123, automatic=True)>
<bound method __create_fn__.<locals>.__repr__ of Car(color='red', mileage=3123, automatic=True)>
ElectronicCar(color='white', mileage=3123, automatic=True, charge=1000)
slots

如果你的程式會建立同一類別的大量object,但不會增刪object attribute,則可以考慮使用__slots__來節省object占用的記憶體,同時加快存取速度。

 1: from dataclasses import dataclass
 2: 
 3: @dataclass
 4: class Car:
 5: 
 6:     __slots__ = ['color', 'mileage', 'automatic']
 7: 
 8:     color: str
 9:     mileage: float
10:     automatic: bool

__slots__不限於dataclass,在任何class均可使用

3.4.7. struct.struct

  • 與C的struct交換資料的管道
1: from struct import Struct
2: 
3: MyStruct = struct('1?f') #指定資料格式
4: data = MyStruct.pack(23, False, 42.0) #資料打包成二進位資料
5: print(data)
6: x = MyStruct.unpack(data)
7: print(x)

3.5. Set

  • Python建立set,除了用{}、也可以用set comprehension
  • 若要建立empty set,一定要call set()建構子,因為空的{}會被當成dict
1: vowels = {'a', 'e', 'i', 'o', 'u'}
2: squares = { x ** 2 for x in range(10) }
3: empty = set()
4: 
5: print(vowels)
6: print(squares)
7: print(empty)
{'a', 'o', 'i', 'u', 'e'}
{0, 1, 64, 4, 36, 9, 16, 49, 81, 25}
set()

3.5.1. frozenset

  • frozenset class為set的不可變版本,一旦建立就無法增刪修,只能查詢
  • 但frozenset為靜態iterable object,故為hashable object,能當成dict object的key或其他set的element,這是一般set無法做到的事
1: vowels = frozenset({'a', 'e', 'i', 'o', 'u'})
2: d = {vowels: 'hello'} # set變為dict的key
3: print(d)
4: 
{frozenset({'e', 'o', 'u', 'a', 'i'}): 'hello'}

3.5.2. collections.Counter

  • collections module底下的Counter實作了「多重集合」(multisets),能輸入多個iterable object(set, dict, string)並統整各元素的出現次數
  • Counter實際為dict的subclass,當我們把新的iterable object輸入Counter object時,該iterable object的element會新增為dict的key,而該element出現次數則為dict的value
 1: from collections import Counter
 2: 
 3: inventory = Counter()
 4: loot = {'sward': 1, 'bread': 3}  #裝備品名及數量
 5: inventory.update(loot)
 6: print(inventory)
 7: 
 8: more_loot = ['sward', 'coins']
 9: inventory.update(more_loot)
10: 
11: yet_more_loot = ['sward', 'bread', 'drink']
12: inventory.update(yet_more_loot)
13: print(inventory)
Counter({'bread': 3, 'sward': 1})
Counter({'bread': 4, 'sward': 3, 'coins': 1, 'drink': 1})

3.6. Stack

幾種實作Stack的結構

3.6.1. list

  • python內部以動態陣列來實作list,所以會配置比element所需更大的空間,視需要增減。故push與pop不見會需要調整大小,平均效率可達O(1)
  • 但為了達到這種效率,element必須從tail做push, pop
  • 若改由list的head做push,pop,則list內所有element均需位移,效率會降為O(n)
 1: a = []
 2: a.append('eating')
 3: a.append('sleeping')
 4: a.append('coding')
 5: print(a)
 6: a.pop()
 7: print(a)
 8: 
 9: #自head做push, pop
10: a.insert(0, 'drinking')
11: a.insert(0, 'playing')
12: a.pop(0)
13: print(a)
['eating', 'sleeping', 'coding']
['eating', 'sleeping']
['drinking', 'eating', 'sleeping']

3.6.2. collections.deque

  • 快速穩定的stack
  • 支援從head,tail做push,pop,效率均為O(1)
  • 若要取出中間element,效率為O(n)
 1: from collections import deque
 2: 
 3: a = deque()
 4: 
 5: a.append('eating')
 6: a.append('sleeping')
 7: a.append('coding')
 8: print(a)
 9: a.pop()
10: print(a)
11: 
12: #自中間取值,效能差
13: print(a[1])
14: 
deque(['eating', 'sleeping', 'coding'])
deque(['eating', 'sleeping'])
sleeping

3.6.3. queue.LifoQueue

  • 可用於multithreadin進行parallelism平行運算時的共享資料
  • 這些資料結構還提供了上鎖機制,好讓同一時間只有一個thread能從queue取出資料
  • queue module裡的LifoQueue class雖名為queue,但採Last In, First Out(LIFO),運作上與Stack相同
1: from queue import LifoQueue
2: s = LifoQueue()
3: s.put('eating')
4: s.put('sleeping')
5: s.put('coding')
6: print(s.get())
7: print(s.get())
coding
sleeping

3.7. Queue

幾種實作queue的資料結構

3.7.1. list

  • 非常慢,沒有效率

3.7.2. collections.deque

  • 快速穩定
 1: from collections import deque
 2: q = deque()
 3: q.append('eating')
 4: q.append('sleeping')
 5: q.append('coding')
 6: q.append('writing')
 7: print(q)
 8: print(q.popleft())
 9: print(q.popleft())
10: print(q)
11: 
deque(['eating', 'sleeping', 'coding', 'writing'])
eating
sleeping
deque(['coding', 'writing'])

3.7.3. queue.Queue

  • 和queue.LifoQueue一樣,內建上鎖機制,可以用來讓multithreading共享資料或任務

3.7.4. multiprocessing.Queue

  • multi-thread在python中其實不算真正的平行運算,各thread實際上是在同一個interpreter下執行,只用到一個core,藉由不斷切換的方式來達到平行運算的效果。

3.7.5. priority queue

  • 不遵循FIFO原則,而是以priority為順序考量
  • priority由totally-ordered key來決定

3.7.6. heapq

  • 借用list實作的binary tree(heap)結構
  • 可實作priority
  • 可於O(log n)的時間完成插入元素或取出最小元素
  • heapq的各note實際上是存在一個list中;node在list中是依binary tree的順序儲存
  • 借由binary tree所依據的sort key即可實作出priority queue
  • 限制之一是預設為由小到大排序

heapq.png

Figure 1: heapq

1: import heapq
2: 
3: q = []
4: heapq.heappush(q, (2, 'coding'))
5: heapq.heappush(q, (1, 'eating'))
6: heapq.heappush(q, (3, 'sleeping'))
7: while q:
8:     next_item = heapq.heappop(q)
9:     print(next_item)
(1, 'eating')
(2, 'coding')
(3, 'sleeping')

3.7.7. queue.PriorityQueue

  • 可用於multi-thread的heapq
    **

4. LOOP

4.1. enumerate()

  • 如果想保留for-each的寫法,但同時又希望能取得每個element的index
1: my_items = ['a', 'b', 'c']
2: print(list(enumerate(my_items)))
3: for i, item in enumerate(my_items):
4:     print(f'{i}: {item}')
[(0, 'a'), (1, 'b'), (2, 'c')]
0: a
1: b
2: c

4.2. zip()

  • 可用來同時走訪多個container
  • 可用來旋轉二維list
1: my_items = ['a', 'b', 'c']
2: my_no = [1, 2, 3]
3: print(list(zip(my_no, my_items)))
4: 
5: for no, item in zip(my_no, my_items):
6:     print(f'{no}: {item}')
[(1, 'a'), (2, 'b'), (3, 'c')]
1: a
2: b
3: c

4.2.1. 旋轉list

1: a = [1, 2, 3]
2: b = [4, 5, 6]
3: c = [7, 8, 9]
4: matrix = [a, b, c]
5: print(matrix)
6: print(list(zip(*matrix)))
7: print(list(zip(*reversed(matrix))))
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
[(1, 4, 7), (2, 5, 8), (3, 6, 9)]
[(7, 4, 1), (8, 5, 2), (9, 6, 3)]

4.3. comprehension

  • 即簡單的單行for-loop
  • 可以讓我們用單行程式產生出包含特定元素的list, set, dict

4.3.1. 語法

1: list = [運算式 for 變數 in iterable object]

4.3.2. list comprehension

1: squares = [x ** 2 for x in range(10)]
2: print(squares)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

4.3.3. comprehension過濾條件

1: list = [x ** 2 for x in range(10) if x % 2 == 0]
2: print(list)
[0, 4, 16, 36, 64]

4.3.4. set與dict comprehension

1: setA = [x ** 2 for x in range(10)]
2: print(setA)
3: dicA = {x: x ** 2 for x in range(10)}
4: print(dicA)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

4.4. list

4.4.1. slice

語法

1: list[index start: index end: step]
1: nums = [1, 2, 3, 4, 5, 6, 7, 8]
2: print(nums[::])
3: print(nums[1:6:2])
4: nums[1:6:2] = [10, 20, 30]
5: print(nums)
6: 
[1, 2, 3, 4, 5, 6, 7, 8]
[2, 4, 6]
[1, 10, 3, 20, 5, 30, 7, 8]

4.4.2. copy

1: l = list(range(6))
2: print(l)
3: cl = l # 指向同一list
4: dl = l[::] # shallow copy
5: l[3] = 10
6: print(cl)
7: print(dl)
[0, 1, 2, 3, 4, 5]
[0, 1, 2, 10, 4, 5]
[0, 1, 2, 3, 4, 5]

5. Python 套件管理

5.1. pip v.s. conda

5.1.1. pip or conda

  • Python 的一大優勢之一便是龐大的第三方函式庫,讓使用 python 的程式設計師可以方便的呼叫、進行如網路資料下載解析、資料的視覺化、甚或是大數據的複雜分析與人工智慧的相關套件。,
  • 目前用來管理這些龐大套件的工具主要有二:pip 與 Conda。

5.1.2. pip

  • Pip是Python Packaging Authority推薦、用於從Python Package Index安裝套件的工具,提供了對 Python 套件的搜㝷、下載、安裝、卸載的功能。
  • 若在 python.org 下載最新版本的 python,則已內建 pip 安裝套件。 Python 3.4+ 以上版本均已包括 pip
  • 該工具類似 Linux 下的 apt/yum 或 MAC 下的Homebrew

5.1.3. conda

  • Conda 是一個開源的跨平台工具軟體,它被設計作為 Python、R、Lua、Scala、C/C++、FORTRAN/ 與 Java 等任何程式語言的套件、依賴性以及工作環境管理員,特別受到以 Python 作為主要程式語言的資料科學團隊所喜愛。
  • 適用平台:Windows, macOS, Linux
  • 傳統 Python 使用者以 pip 作為套件管理員(package manager)、以 venv 作為工作環境管理員(environment manager),而 conda 則達成了「兩個願望、一次滿足」既可以管理套件亦能夠管理工作環境。2

5.1.4. In both cases:

5.1.5. difference between conda, anaconda, and miniconda

  • conda is both a command line tool, and a python package. 3
  • Anaconda 發行版會預裝很多套件,而 Miniconda 是最小的 conda 安裝環境, 一個乾淨的 conda 環境。
  • pip 只是運與安裝 python package,而 conda 用來安裝管理任何語言的包。
  • 不一定要安裝 Anaconda 或 Miniconda,也可透過 pip 直接安裝 conda

      pip install conda
    

5.2. conda 安裝與使用

5.2.1. 下載 v.s. 安裝

5.2.2. 安裝conda

for macOS

1: brew install --cask anaconda
2: export PATH="/usr/local/anaconda3/bin:$PATH"

5.2.3. 移除

  • Windows: uninstall
  • Linux/macOS:
    rm -rf ~/anaconda

5.3. python package 安裝(conda)

  • 安裝 package:
    conda install packageName

      conda install pandas
    
  • 移除 package:
    conda remove packageName

      conda remove pandas
    
  • 安裝特定版本 python
    conda install python=version

      conda install python=3.5
    
  • 了解目前系統可用套件

        conda list
    

5.4. Python 常用函式庫

5.4.1. 爬蟲

Scrapy:

scrapy.jpg

  • Scrapy,Python 開發的一個快速、高層次的 web 數據抓取框架,用於抓取 web 站點並從頁面中提取結構化的數據。Scrapy 用途廣泛,可以用於數據挖掘、監測和自動化測試4
  • Scrapy 吸引人的地方在於它是一個框架,任何人都可以根據需求方便的修改。它也提供了多種類型爬蟲的基類,如 BaseSpider、sitemap 爬蟲等。
beautifulsoup4:

bs.png

  • Beautiful Soup 是一個 Python 的函式庫模組,可以讓開發者僅須撰寫非常少量的程式
    碼,就可以快速解析網頁 HTML 碼,從中翠取出使用者有興趣的資料、去蕪存菁,降低網
    路爬蟲程式的開發門檻、加快程式撰寫速度。5
  • 而 Beautiful Soup 是基於 HTML DOM 的,會載入整個文檔,解析整個 DOM 樹,因此時間和內存開銷都會大很多,所以性能要低於 lxml。6
Selenium

selenium.jpg

  • 原為網頁測試工具,但由於可以直接以程式碼操控瀏覽器的特性,使其成為網路爬蟲必備
    的工具之一。7
  • Selenium 執行「真實的瀏覽器」來進行網站操作的自動化,它能夠直接獲取即時的內容,包括被 JavaScript 修改過的 DOM 內容,讓程式可以直接與網頁元素即時互動、執行 JavaScript 程式,因此也適用於前端採用 AJAX 技術的網站。8
  • Selenium 是許多 Web Testing 工具的核心,利用 Selenium 操作網頁表單資料、點選按鈕或連結、取得網頁內容並進行檢驗,可以滿足相當多測試的需求。

5.4.2. 網站

Django

django.jpg

  • Django (ˈdʒæŋɡoʊ jang-goh) 可以說是 Python 最著名的 Web Framework,一些知名的網站如 Pinterest, Instagram, Disqus 等等都使用過它來開發。9
  • 免費開放原始碼
  • 著重快速開發、高效能
  • 遵從 DRY ( Don’t Repeat Yourself ) 守則,致力於淺顯易懂和優雅的程式碼
  • 使用類似 Model–view–controller (MVC) pattern 的架構
  • 10 Popular Websites Built With Django
Flask

flask.png

  • Flask 是一個使用 Python 撰寫的輕量級 Web 應用程式框架,由於其輕量特性,也稱為
    micro-framework(微框架)。10
  • Flask 和 Django 不同的地方在於 Flask 給予開發者非常大的彈性(當然你也可以說是
    需要思考更多事情),可以選用不同的用的 extension 來增加其功能。
  • 相比之下,Django 雖然完善但技術選擇相對不彈性,不論是 ORM、表單驗證或是模版引
    擎都有自己的作法。
  • 沒有最好的框架,只有合適的使用情境。

5.4.3. 資料處理科學計算

Numpy

numpy.jpg

  • Numpy 底層以 C 和 Fortran 語言實作,所以能快速操作多重維度的陣列。11
  • 當 Python 處理龐大資料時,其原生 list 效能表現並不理想(但可以動態存異質資料),而 Numpy 具備平行處理的能力,可以將操作動作一次套用在大型陣列上。
  • 此外 Python 其餘重量級的資料科學相關套件(例如:Pandas、SciPy、Scikit-learn 等)都幾乎是奠基在 Numpy 的基礎上。
Scipy

scipy.png

  • 科學計算神器
  • Numpy 是以矩陣基礎做數據的數學運算,SciPy 就是以 Numpy 為基礎做科學、工程的運算處理的 package,包含統計、優化、整合、線性代數、傅立葉轉換圖像等較高階的科學運算。12
Pandas

pandas.png

  • 建構在 NumPy 之上,提供資料結構與資料處理工具,讓資料清理與分析更為快速與方便
  • 適合處理表格或異質資料 (NumPy 適合處理同質之數值陣列資料)

5.4.4. 視覺化

matplotlib

matplotlib.jpg

  • Matplotlib 就是 MATLAB+Plot+Library 的簡稱,因為是模仿 MATLAB 建立的繪圖庫,所以繪
    圖風格會與 MATLAB 有點類似。13
  • 為了提高處理大量資料的性能,Matplotlib 大量使用了 NumPy 和其相關的擴展代碼。 為了方便快速繪圖, Matplotlib 通過 pyplot 模組提供了一套和 MATLAB 類似的繪圖 API,只需要調用 pyplot 模組所提供的函數,就可以實現快速繪圖及設置圖表的各種細節。
  • 另一方面 Matplotlib 也適合互動式繪製圖表,可以很方便地處理二維和三維的圖表。
seaborn

seaborn.png

seabornPlots.jpg

  • 散點圖矩陣神器
  • Seaborn 是 Python 一個著名的數據視覺化的 package, 以 matplotlib 為基底的一個擴
    展包, 它提供了易於理解的統計圖和便利的繪圖指令14
  • seaborn 庫是對 matplotlib 庫更高級別的封裝,相當於提供了各種統計圖的模板15
ggplot

ggplot.jpg

  • R 語言視覺化神器的 Python 版本
  • ggplot2 是一個十分強大的 R 語言可視化包。它的核心理念是將繪圖與數據分離,數據相關
    的繪圖與數據無關的繪圖分離。16
  • 它是按圖層作圖的,一個語句做一個僅包含基礎作圖單元的圖層,然後通過不同圖層的疊
    加最後成圖。
plotly

plotly.png

  • 這個神器是個 js 庫,不過也有各種流行的語言介面
  • plotly 是一個能讓你畫出互動式圖表的一個開源套件,其基本操作是免費的,如果你想要使用網頁的版本,可以使用更多 plotly 的進階功能的話,就必須付費

5.4.5. 機器學習

scikit-learn

scikit-learn.png

  • 幾乎所有機器學習演算法都囊括
  • scikit-learn,又寫作 sklearn,是一個開源的基於 python 語言的機器學習工具包。它通過 NumPy, SciPy 和 Matplotlib 等 python 數值計算的庫實現高效的算法應用,並且涵蓋了幾乎所有主流機器學習算法。17
  • sklearn 中常用的 module 有分類、回歸、聚類、降維、模型選擇、預處理。
NLTK

NLTK.png

  • NLTK 全文是 “Nature Language Tool Kit” (NLTK),是 Python 中一個經典的、專門用於進行自然語言處理的工具。18
  • 雖然也能進行部份中文的處理,但是對於中文的支援度自然沒有英文來得好
  • 目前而言,繁體中文有兩個套件可以使用,一個是中研院開發的斷詞系統,另一套系統為
    jieba(結巴)。
  • 少女詩人小冰
TensorFlow

tensorflow.jpg

  • Tensorflow 最初為 Google Brian 所開發。在 2015 時,Google 將之開源,為現今重要的深
    度學習框架之一,它支援各式不同的深度學習演算法,並已應用於各大企業服務上,Ex:
    Google, Youtube, Airbnb, Paypal … 等。19
  • 此外,Tensorflow 也支援在各式不同的
    device 上運行深度學習 Ex: Tensorflow Lite 、 Tensorflow.js 等等。
  • Tensorflow 為目
    前最受歡迎的機器學習、深度學習開源專案。不管是 github fork 的數量、論文的使用次
    數以及熱門程度,均比其他的框架來的多20

深度學習套件

Keras

keras.png

  • 在 2015 年 TensorFlow 推出的同時,美國麻省理工學院(又是它,這學校開門就能賺錢)
    也推出一套能很容易被使用者透過 Python 寫 Deep learning 的應用程式介面 API ,叫
    做 Keras 。
  • Keras 只有介面喔! Keras 本身還是得透過 TensorFlow ,或者其
    他像是微軟的 CNTK 這類引擎當作底層,才能執行
    21
  • Keras 使用上比較接近人類的想法( TensorFlow 設計上沒有錯,只不過比較是針對電腦
    系統跟網路通訊的想法),所以透過 Python 呼叫 Keras 能夠很簡單地描述我們人類想
    要電腦達到 Deep Learning 要做的事情。目前在教學上, Keras 的普及率相當高
  • Keras 可以快速有方便運算的主要原因是,它已經將訓練模型的輸入層、隱藏層、輸出層,做好架構,使用者只需要加入並且填寫正確的參數 ex.神經元個數、activation function 的函式…等。22
PyTorch

pytorch.jpg

  • Numpy 的 GPU 版
  • PyTorch 為 Facebook 在 2017 年初開源的深度學習框架,其建立在 Torch 之上,且標
    榜 Python First ,為量身替 Python 語言所打造,使用起來就跟寫一般 Python 專案沒
    兩樣,也能和其他 Python 套件無痛整合。PyTorch 的優勢在於其概念相當直觀且語法簡
    潔優雅,因此視為新手入門的一個好選項;再來其輕量架構讓模型得以快速訓練且有效運
    用資源。23
  • 於 PyTorch 框架中,資料類型定義為張量(Tensor),張量可以是純量、向量或是更高維度的矩陣,而 torch 函式庫負責張量在 CPU/GPU 的運算。

5.5. python 執行環境建立與維護

  • 建立24

    conda create -n envName
    
  • 啟用

    conda activate envName
    
  • 退出

     conda deactiveate
    
  • 刪除

    conda env remove -n envName
    
  • 列出目前系統中所有的虛擬環境

    conda env list
    

5.6. 將執行環境匯入 jupyter

  • 於終端機(for windows: Anaconda prompt)下建立、啟用所需虛擬環境
  • 將環境滙至 jupyter kernel
python -m ipykernel install --user --name 虛擬環境名稱 --display-name "在jupyter中的名稱"
  • 啟動 jupyter

5.7. poetry

pip最主要的缺點在於套件的相依性。pip 在解決套-件之間的版本衝突時很容會遇到困難。它不具備先進的依賴解析算法,這可能導致不穩定的環境和不可預知的錯誤。更重要的是,移除套件時,pip 只會移除一個該套件,而不會移除其他相依的套件。也因此發展出針對套件相依性更好的工具,例poetry25

Poetry 是一個現代化的套件管理工具,它不僅可以幫助我們管理套件的依賴,還提供了一個虛擬環境的解決方案。Poetry 使用一個 toml 檔 pyproject.toml 文件來管理專案配置和依賴,這符合 – Specifying Minimum Build System Requirements for Python Projects 所提倡的現代 Python 專案的依賴項管理25

5.7.1. 安裝

for Python 3.8+

curl -sSL https://install.python-poetry.org | python3 -

5.7.2. 結合虛擬環境與專案

poetry config virtualenvs.in-project true

5.7.3. 建立新專案

假設專案名稱為 tnfshbot

poetry new tnfshbot
cd tnfshbot
poetry init

5.7.4. 修改pyproject.toml

1: [tool.poetry.dependencies]
2: python = "^3.12"

5.7.5. 安裝

當你手動添加了新的套件後(poetry add XXX),使用 poetry install 將會:

  • 安裝 pyproject.toml 中列出的所有套件,包括新添加的與其依賴項。
  • 如果 poetry.lock 文件存在,它將依照該文件中固定的版本來安裝依賴項。
  • 如果 poetry.lock 文件不存在,它將創建一個新的,其中固定了依賴的版本。
1: poetry install

會生成一個.venv資料夾

1: poetry run python --version

5.7.6. 新增tnfshbot/main.py

安裝新套件

1: poetry add pendulum

執行python

1: import pendulum
2: import sys
3: 
4: print("Hello World!")
5: print(sys.version)
6: print(pendulum.now())

執行方式為

1: poetry run python tnfshbot/main.py

5.7.7. 整個資料夾架構如下

.
├── README.md
├── poetry.lock
├── pyproject.toml
├── tests
│   └── __init__.py
└── tnfshbot
    ├── __init__.py
    └── main.py

5.7.8. pyproject.toml

精確版本

指定一個精確的版本,意味著只有該特定版本會被接受。例如:

1: [tool.poetry.dependencies]
2: numpy = "1.21.0"
版本範圍

可以使用比較運算符來指定一個版本範圍:

1: [tool.poetry.dependencies]
2: numpy = ">=1.21.0, <2.0.0"

在上面的範例中,任何版本從 1.21.0 (含) 到 2.0.0 (不含) 都是可以接受的。

星字號

使用星號(*)是允許任何兼容的版本:

1: [tool.poetry.dependencies]
2: numpy = "1.21.*"

在上面的範例中,任何 1.21 系列的版本都是可以接受的。如

Caret

使用Caret(^)可以指定一個允許任何相容的版本,但不會改變最左邊的非零數字:

1: [tool.poetry.dependencies]
2: numpy = "^1.21.0"

這將允許 1.21.0 以及任何更高但低於 2.0.0 的版本。

波浪號版本

使用波浪號(~)可以指定一個允許在特定範圍內的版本:

1: [tool.poetry.dependencies] numpy = "~1.21.0"

這會允許 1.21.0 以及任何更高但低於 1.22.0 的版本。

多版本指定

你也可以指定多個版本,並且只要符合其中一個條件就可以:

1: [tool.poetry.dependencies]
2: numpy = [ ">=1.20.0, <1.21.0", ">=1.22.0, <1.23.0" ]

5.7.9. poetry update

在添加新的依賴後使用 poetry update 將會26

  • 更新所有依賴到最新可用版本,並更新 poetry.lock 文件。
  • 它會安裝新添加的套件和更新所有其他依賴。
  • 這個指令超級強大,更新套件版本時也解析並更新依賴。有更新過 Python 套件的人常常都會受到依賴項衝突所困,用這個指令就能簡單解決了。
  • 可以指定要更新什麼套件,例如說 poetry update pendulum

5.7.10. poetry to jupyter kenrel

1: poetry run ipython kernel install --name=tnfshbot --user

6. TODO Multiprocessing

6.1. 執行緒 vs. 程序

程序(process)是系統資源分配的最小單位,而一個程序可以有很多個執行緒(thread)。

我們每次開啟一個python檔案,都是在開啟一個新的程序,而每個程序都有自己的記憶體空間,這些記憶體空間是獨立的,互不影響。那為什麼還需要去探討執行緒呢?因為執行緒是程序中最小的執行單位,當我們在一個程序中開啟多個執行緒時,這些執行緒會共享這個程序的記憶體空間,這樣就可以讓我們在同一個程序中同時執行多個任務。

6.2. 非執行緒的程式

為了對照執行緒的程式,我們先來看一個非執行緒的程式,這個程式會依序執行三個函式,並且計算總共花了多少時間來執行這三個函式。

 1: from time import sleep, perf_counter
 2: 
 3: def print_hello(): # 此func所需執行時間應為1秒
 4:     for i in range(10):
 5:         sleep(0.1)
 6:         print('Says HELLO!')
 7: 
 8: def print_message(message): # 此func所需執行時間應為1秒
 9:     for i in range(10):
10:         sleep(0.1)
11:         print(f'Print {message}!')
12: 
13: start = perf_counter()
14: 
15: print_hello()
16: print_hello()
17: print_message('FUNCETION 3')
18: 
19: end = perf_counter()
20: print(f'Time taken: {end - start:.2f} seconds')
21: print(f'All done!')
22: 
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Says HELLO!
Print FUNCETION 3!
Print FUNCETION 3!
Print FUNCETION 3!
Print FUNCETION 3!
Print FUNCETION 3!
Print FUNCETION 3!
Print FUNCETION 3!
Print FUNCETION 3!
Print FUNCETION 3!
Print FUNCETION 3!
Time taken: 3.11 seconds
All done!

6.3. 建立及執行執行緒

 1: from threading import current_thread, Thread as Thread
 2: from time import sleep, perf_counter
 3: 
 4: def print_hello(): # 此func所需執行時間應為1秒
 5:     for i in range(10):
 6:         sleep(0.1)
 7:         print(f'{current_thread().name} says HELLO!')
 8: 
 9: def print_message(message): # 此func所需執行時間應為1秒
10:     for i in range(10):
11:         sleep(0.1)
12:         print(f'{current_thread().name} says {message}!')
13: 
14: start = perf_counter()
15: 
16: thread1 = Thread(target=print_hello, name='Thread 1')
17: thread2 = Thread(target=print_hello, name='Thread 2')
18: thread3 = Thread(target=print_message, args=('This is Thread 3',), name='Thread 3')
19: 
20: thread1.start()
21: thread2.start()
22: thread3.start()
23: 
24: thread1.join()
25: thread2.join()
26: thread3.join()
27: 
28: end = perf_counter()
29: print(f'Time taken: {end - start:.2f} seconds')
30: print(f'Current thread: {current_thread().name}')
31: print(f'All done!')
32: 
Thread 1 says HELLO!
Thread 2 says HELLO!
Thread 3 says This is Thread 3!
Thread 3 says This is Thread 3!
Thread 1 says HELLO!
Thread 2 says HELLO!
Thread 3 says This is Thread 3!
Thread 2 says HELLO!
Thread 1 says HELLO!
Thread 3 says This is Thread 3!
Thread 2 says HELLO!
Thread 1 says HELLO!
Thread 2 says HELLO!
Thread 3 says This is Thread 3!
Thread 1 says HELLO!
Thread 2 says HELLO!
Thread 3 says This is Thread 3!
Thread 1 says HELLO!
Thread 2 says HELLO!
Thread 3 says This is Thread 3!
Thread 1 says HELLO!
Thread 2 says HELLO!
Thread 3 says This is Thread 3!
Thread 1 says HELLO!
Thread 2 says HELLO!
Thread 3 says This is Thread 3!
Thread 1 says HELLO!
Thread 2 says HELLO!
Thread 3 says This is Thread 3!
Thread 1 says HELLO!
Time taken: 1.05 seconds
Current thread: MainThread
All done!

上述程式中,我們建立了三個執行緒:

  • thread1 和 thread2,這兩個執行緒都會執行 print_hello 函式,
  • 第三個執行緒 thread3 則會執行 print_message 函式。

仔細觀察執行輸出,我們可以發現兩項重要的事項:

  1. 這三個執行緒都是 同時 執行的,所以我們可以看到它們的輸出是交錯在一起的
  2. 照理說這三個執行緒都需要 1 秒的時間來執行,但實際上卻只花了 1.05 秒,這是因為這三個執行緒是同時執行的,所以它們的執行時間是重疊的。

這就是為什麼我們需要使用執行緒的原因,因為它可以讓我們在同一個程序中同時執行多個任務,這樣就可以提高我們的程式效率。

7. TODO Interactive Plots in Jupyter Notebook

Footnotes:

Author: Yung-Chin Yen

Created: 2025-04-28 Mon 16:22