(第七、九節待補)
Python 3: Deep Dive (Part 1 - Functional)
Introduction
深入了解 Variables, Functions and Functional Programming, Closures, Decorators, Modules and Packages 以及Python 的內部機制
網址 :https://www.udemy.com/course/python-3-deep-dive-part-1/
程式碼:
7.Scopes, Closures and Decorators (待回頭補齊)
- Introduction
n = 10000000
start = time.perf_counter()
run_float(n)
end = time.perf_counter()
- Global and Local Scopes
- 依據變數(變量)有效的範圍,我們可以將變數區分為內建變數 (Built-in variable)、全域變數(Global variable) 及區域變數 (Local variable)
- Nonlocal Scopes
- Closures
- Closure Applications - Part 1
- Closure Applications - Part 2
- Decorators (Part 1)
- Decorator Application (Timer)
- Decorator Application (Logger, Stacked Decorators)
- Decorator Application (Memoization)
- Decorator Factories
- Decorator Application (Decorator Class)
- Decorator Application (Decorating Classes)
- Decorator Application (Dispatching) - Part 1
- Decorator Application (Dispatching) - Part 2
- Decorator Application (Dispatching) - Part 3
n = 10000000
start = time.perf_counter()
run_float(n)
end = time.perf_counter()
8. Tuples as Data Structures and Named Tuples
- Tuples as Data Structures
相比於 List 及 String 這類同屬序列的類型,Tuple 有著固定元素數量及順序的特性(Immutable),且可以包含不同型別的元素,因此適合拿來當作數據紀錄。
- Named Tuples
透過在 collections 套件中的 Nametuple 函式,我們可以建立帶有名字屬性的 Tuple(實際上是建立了繼承自 Tuple 的子類別,並實作了__repr__、__eq__ 以及轉換型別的method…等)
from collections import namedtuple
Point2D = namedtuple('Point2D', ('x', 'y')) # 透過套件
class Point3D: # 自定義類別
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
pt2d_1 = Point2D(10, 20)
pt2d_2 = Point2D(10, 20)
print(pt2d_1)
Point2D(x=10, y=20)
pt2d_1 == pt2d_2
True
isinstance(pt2d_1, tuple)
True
max(pt2d_1)
20
# 單純的自定義類別缺少許多應有方法
pt3d_1 = Point3D(10, 20, 30)
pt3d_2 = Point3D(10, 20, 30)
print(pt3d_1)
<__main__.Point3D at 0x27408e1fa90>
pt3d_1 == pt3d_2
False
isinstance(pt3d_1, tuple)
False
max(pt3d_1)
TypeError: 'Point3D' object is not iterable
# 宣告方式
Circle_1 = namedtuple('Circle', ['center_x', 'center_y', 'radius'])
Circle_2 = namedtuple('Circle', 'center_x center_y radius')
Circle_3 = namedtuple('Circle', 'center_x, center_y, radius')
Circle_4 = namedtuple('Circle', ['center_x', 'center_y', '_radius']) # 不得以底線開頭為名
ValueError: Field names cannot start with an underscore: '_radius'
Circle_5 = namedtuple('Circle', ['center_x', 'center_y', '_radius'], rename= True)
print(Circle_5._fields)
('center_x', 'center_y', '_2') # 但加上 rename 參數,函式會協助自動更名
circle_1 = Circle_1(10, 20, 100)
circle_2 = Circle_1(center_y=20, center_x=10, radius=100)
print(circle_1.radius, circle_1[2]) # 除了索引,也可以像類別或字典一樣透過屬性名稱取值
100 100
- Named Tuples - Modifying and Extending
由於 Tuple 的不變性,在更改元素內容或是新增元素時,我們可以利用 unpacking 及 ._replace 減輕重新宣告的繁瑣程式碼
from collections import namedtuple
Stock = namedtuple('Stock', 'symbol year month day open high low close')
djia = Stock('DJIA', 2018, 1, 25, 26_313, 26_458, 26_260, 26_393)
djia_new1 = djia._replace(year=2019, day=26) # 使用 ._replace 時必須給出參數名稱
Stock(symbol='DJIA', year=2019, month=1, day=26, open=26313, high=26458, low=26260, close=26393)
djia_new2 = Stock(*djia[:-1] , 99999) # 利用 unpacking 傳入參數們
# djia_new2 = Stock._make([*(djia[:-1] + (99999,))]) # 與前一行功能相等,利用 ._make 傳入參數的序列
Stock(symbol='DJIA', year=2018, month=1, day=25, open=26313, high=26458, low=26260, close=99999)
new_fields = Stock._fields + ('previous_close',) # 利用 ._fields 取出原 NamedTuple 的屬性名稱
StockExt = namedtuple('StockExt', new_fields)
djia_extend = StockExt(*djia , (12345,))
StockExt(symbol='DJIA', year=2018, month=1, day=25, open=26313, high=26458, low=26260, close=26393, previous_close=12345)
- Named Tuples - DocStrings and Default Values
由於 NamedTuple 是類別,我們可以幫類別及屬性加上 DocStrings,並指定預設值,而指定預設值有兩種做法:
from collections import namedtuple
Point2D = namedtuple('Point2D', 'x y color')
Point2D.__doc__ = 'Represents a 2D coordinate'
Point2D.x.__doc__ = 'x-coordinate'
Point2D.y.__doc__ = 'y-coordinate'
help(Point2D)
class Point2D(builtins.tuple)
| Represents a 2D coordinate
|
| Method resolution order:
| Point2D
| builtins.tuple
...
---------
| Data descriptors defined here:
|
| x
| x-coordinate
|
| y
| y-coordinate
|
| color
| Alias for field number 2
|
| ----------------------------------
# Using prototype to make default values
Point_zero = Point2D(x=None, y=0, color='black')
p1 = Point_zero._replace(x= 5)
print(p1)
Point2D(x=5, y=0, color='black')
# Using __defaults__
Point2D.__new__.__defaults__ = 0,'black', # 此處的預設值會對齊參數的尾部進行分配
p2 = Point2D(5)
print(p2)
Point2D(x=5, y=0, color='black')
- Named Tuples - Application - Returning Multiple Values
由於 Nametuple 是一個類別,相對於回傳 Tuple ,我們可以直觀地用 NT.attribute1 取得屬性的值,而非使用切片 T[0],這大大改善了程式的可讀性
from random import randint
from collections import namedtuple
def random_color():
red = randint(0, 255)
green = randint(0,255)
blue = randint(0, 255)
return red, green, blue
red = random_color()[0] # 僅能用索引取值,後續維護數字索引對應的意義會很煩躁
Color = namedtuple('Color', 'red green blue')
def random_color():
red = randint(0, 255)
green = randint(0,255)
blue = randint(0, 255)
return Color(red, green, blue)
red = random_color().red # 可用屬性名稱取值,提升可讀性
- Named Tuples - Application - Alternative to Dictionaries
Namedtuple 在功能上與字典類似,但最為關鍵的差異在於它為Immutable,可讀性為其次,而這兩種資料結構的相似用法及轉換方式如下:
from collections import namedtuple
data_dict = dict(key1=100, key3=300, key2=200)
Data = namedtuple('Data', sorted(data_dict.keys()))
data_NT = Data(**data_dict) # 利用 ** 運算子解開字典
print(data_NT)
Data(key1=100, key2=200, key3=300)
print(data_dict.get('key1', None), data_dict.get('key4', None))
100 None
# 使用 getattr 取得屬性數值,語法與 dict.get 類似
print(getattr(data_NT, 'key1', None), getattr(data_NT, 'key4', None))
100 None
相比於 List 及 String 這類同屬序列的類型,Tuple 有著固定元素數量及順序的特性(Immutable),且可以包含不同型別的元素,因此適合拿來當作數據紀錄。
透過在 collections 套件中的 Nametuple 函式,我們可以建立帶有名字屬性的 Tuple(實際上是建立了繼承自 Tuple 的子類別,並實作了__repr__、__eq__ 以及轉換型別的method…等)
from collections import namedtuple
Point2D = namedtuple('Point2D', ('x', 'y')) # 透過套件
class Point3D: # 自定義類別
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
pt2d_1 = Point2D(10, 20)
pt2d_2 = Point2D(10, 20)
print(pt2d_1)
Point2D(x=10, y=20)
pt2d_1 == pt2d_2
True
isinstance(pt2d_1, tuple)
True
max(pt2d_1)
20
# 單純的自定義類別缺少許多應有方法
pt3d_1 = Point3D(10, 20, 30)
pt3d_2 = Point3D(10, 20, 30)
print(pt3d_1)
<__main__.Point3D at 0x27408e1fa90>
pt3d_1 == pt3d_2
False
isinstance(pt3d_1, tuple)
False
max(pt3d_1)
TypeError: 'Point3D' object is not iterable
# 宣告方式
Circle_1 = namedtuple('Circle', ['center_x', 'center_y', 'radius'])
Circle_2 = namedtuple('Circle', 'center_x center_y radius')
Circle_3 = namedtuple('Circle', 'center_x, center_y, radius')
Circle_4 = namedtuple('Circle', ['center_x', 'center_y', '_radius']) # 不得以底線開頭為名
ValueError: Field names cannot start with an underscore: '_radius'
Circle_5 = namedtuple('Circle', ['center_x', 'center_y', '_radius'], rename= True)
print(Circle_5._fields)
('center_x', 'center_y', '_2') # 但加上 rename 參數,函式會協助自動更名
circle_1 = Circle_1(10, 20, 100)
circle_2 = Circle_1(center_y=20, center_x=10, radius=100)
print(circle_1.radius, circle_1[2]) # 除了索引,也可以像類別或字典一樣透過屬性名稱取值
100 100
由於 Tuple 的不變性,在更改元素內容或是新增元素時,我們可以利用 unpacking 及 ._replace 減輕重新宣告的繁瑣程式碼
from collections import namedtuple
Stock = namedtuple('Stock', 'symbol year month day open high low close')
djia = Stock('DJIA', 2018, 1, 25, 26_313, 26_458, 26_260, 26_393)
djia_new1 = djia._replace(year=2019, day=26) # 使用 ._replace 時必須給出參數名稱
Stock(symbol='DJIA', year=2019, month=1, day=26, open=26313, high=26458, low=26260, close=26393)
djia_new2 = Stock(*djia[:-1] , 99999) # 利用 unpacking 傳入參數們
# djia_new2 = Stock._make([*(djia[:-1] + (99999,))]) # 與前一行功能相等,利用 ._make 傳入參數的序列
Stock(symbol='DJIA', year=2018, month=1, day=25, open=26313, high=26458, low=26260, close=99999)
new_fields = Stock._fields + ('previous_close',) # 利用 ._fields 取出原 NamedTuple 的屬性名稱
StockExt = namedtuple('StockExt', new_fields)
djia_extend = StockExt(*djia , (12345,))
StockExt(symbol='DJIA', year=2018, month=1, day=25, open=26313, high=26458, low=26260, close=26393, previous_close=12345)
由於 NamedTuple 是類別,我們可以幫類別及屬性加上 DocStrings,並指定預設值,而指定預設值有兩種做法:
from collections import namedtuple
Point2D = namedtuple('Point2D', 'x y color')
Point2D.__doc__ = 'Represents a 2D coordinate'
Point2D.x.__doc__ = 'x-coordinate'
Point2D.y.__doc__ = 'y-coordinate'
help(Point2D)
class Point2D(builtins.tuple)
| Represents a 2D coordinate
|
| Method resolution order:
| Point2D
| builtins.tuple
...
---------
| Data descriptors defined here:
|
| x
| x-coordinate
|
| y
| y-coordinate
|
| color
| Alias for field number 2
|
| ----------------------------------
# Using prototype to make default values
Point_zero = Point2D(x=None, y=0, color='black')
p1 = Point_zero._replace(x= 5)
print(p1)
Point2D(x=5, y=0, color='black')
# Using __defaults__
Point2D.__new__.__defaults__ = 0,'black', # 此處的預設值會對齊參數的尾部進行分配
p2 = Point2D(5)
print(p2)
Point2D(x=5, y=0, color='black')
由於 Nametuple 是一個類別,相對於回傳 Tuple ,我們可以直觀地用 NT.attribute1 取得屬性的值,而非使用切片 T[0],這大大改善了程式的可讀性
from random import randint
from collections import namedtuple
def random_color():
red = randint(0, 255)
green = randint(0,255)
blue = randint(0, 255)
return red, green, blue
red = random_color()[0] # 僅能用索引取值,後續維護數字索引對應的意義會很煩躁
Color = namedtuple('Color', 'red green blue')
def random_color():
red = randint(0, 255)
green = randint(0,255)
blue = randint(0, 255)
return Color(red, green, blue)
red = random_color().red # 可用屬性名稱取值,提升可讀性
Namedtuple 在功能上與字典類似,但最為關鍵的差異在於它為Immutable,可讀性為其次,而這兩種資料結構的相似用法及轉換方式如下:
from collections import namedtuple
data_dict = dict(key1=100, key3=300, key2=200)
Data = namedtuple('Data', sorted(data_dict.keys()))
data_NT = Data(**data_dict) # 利用 ** 運算子解開字典
print(data_NT)
Data(key1=100, key2=200, key3=300)
print(data_dict.get('key1', None), data_dict.get('key4', None))
100 None
# 使用 getattr 取得屬性數值,語法與 dict.get 類似
print(getattr(data_NT, 'key1', None), getattr(data_NT, 'key4', None))
100 None
9. Modules, Packages and Namespaces (待回頭補齊)
- What is a Module?
Module 只是型別為 types.ModuleType 的物件,且不一定從檔案載入,模組載入時
- How does Python Import Modules?
與其他傳統語言在編譯期間就將模組導入不同, Python 在執行期間才導入模組,以下是 Import 的整個過程 - 檢查快取中 (可透過 sys.modules 查看) 是否已載入此模組,若否,執行以下操作
- 建立一個空的 types.ModuleType 物件
- 從檔案將程式碼載入記憶體 (檔案尋找根據 sys.path 的順序進行)
- 使用模組檔案名稱作為變量指向到該物件,並將該物件加到 sys.modules 中
- 使用模組檔案名稱或自定義縮寫作為變量指向到該物件,並將該物件加到 global namespace (可透過 globals() 查看)
- 編譯並執行該模組的程式碼
import sys
from fractions import Fraction
# 查看快取中是否存在已導入的模組
print(sys.modules['fractions'])
<module 'fractions' from 'C:\\Users\\User\\Anaconda3\\lib\\fractions.py'>
# 查看全域命名空間是否存在導入的物件
print(globals()['Fraction'])
<class 'fractions.Fraction'>
# 模組內的內容會被紀錄在.__dict__中
import fractions
fractions.__dict__
{'__name__': 'fractions',
'__doc__': 'Fraction, infinite-precision, real numbers.',
'__package__': '',
'__loader__': <_frozen_importlib_external.SourceFileLoader at 0x2666f8fbc50>,
'__spec__': ModuleSpec(name='fractions', loader=<_frozen_importlib_external.SourceFileLoader object at 0x000002666F8FBC50>, origin='C:\\Users\\User\\Anaconda3\\lib\\fractions.py'),
'__file__': 'C:\\Users\\User\\Anaconda3\\lib\\fractions.py',
'__cached__': 'C:\\Users\\User\\Anaconda3\\lib\\__pycache__\\fractions.cpython-36.pyc',
'__builtins__': {'__name__': 'builtins',
'__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.",
'__package__': '',
'__loader__': _frozen_importlib.BuiltinImporter,
'__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>),
'__build_class__': <function __build_class__>,
'__import__': <function __import__>,
'abs': <function abs(x, /)>,
'all': <function all(iterable, /)>,
'any': <function any(iterable, /)>,
......}
- Imports and importlib
除了 import 之外,我們可透過 importlib 達到相同的功能,而 import 的當下(參考前一節第 3 點),Python 會詢問各個 Finder 是否知道要去哪裡找到程式碼,再交給相應的Loader
import sys
import importlib
# 等同於 import pandas as pd
pd = importlib.import_module('pandas')
# 依序使用 Finder 搜尋模組所在
sys.meta_path
[_frozen_importlib.BuiltinImporter,
_frozen_importlib.FrozenImporter,
_frozen_importlib_external.WindowsRegistryFinder,
_frozen_importlib_external.PathFinder,
<six._SixMetaPathImporter at 0x12ca3e24518>]
# PathFinder 根據 sys.path 搜尋的順序
print(sys.path[:3])
['', # 空字串代表當前目錄
'C:\\Users\\User\\Anaconda3\\python36.zip',
'C:\\Users\\User\\Anaconda3\\DLLs']
# 檢視套件要使用何種Loader
importlib.util.find_spec('pandas')
ModuleSpec(name='pandas', loader=<_frozen_importlib_external.SourceFileLoader object at 0x0000012CA76DC320>, origin='C:\\Users\\User\\Anaconda3\\lib\\site-packages\\pandas\\__init__.py', submodule_search_locations=['C:\\Users\\User\\Anaconda3\\lib\\site-packages\\pandas'])
- Import Variants and Misconceptions
- 不論是 import A 或是 from A import B ,系統都會將 A 完整載入系統快取,差別只差在將 A 或是 B 連結到 Global Namespace
- 不建議在函數裏面導入模組,會沒有統一的地方進行查看,影響可讀性,且每次呼叫函式,程式需要將快取中的模組再次連結到函式的 Local namespace,除了避免覆蓋變量等特殊考量,平時應盡量避免
- Reloading Modules
在程式運行的過程中,我們可以透過 importlib.reload(modulename) 重新載入模組,但並不能確保已經透過 from A import B 載入舊模組的程式皆重新引用到新模組,因此在正式環境中應避免這類行為
- Using __main__
- 透過 if __name__ == '__main__',我們可以賦予模組在命令列被直接調用時的行為,例如:python module.py -r abc
- 當我們從命令列呼叫 python foldername 時,實際上會去執行目錄內的__main__.py
- What are Packages?
- 套件是一個可以包含子套件及子模組的模組,以檔案系統為架構的套件會以目錄名稱作為套件名稱,且程式碼會記錄在目錄中的 __init__.py
- 若一模組為套件,其 __path__ 屬性內容為套件存在目錄的絕對路徑
- import pac_1.pac1_1.mod1
- Why Packages?
使用套件的架構可以讓開發者將完整的程式依功能切分成多個檔案,以便於後續維護
- Structuring Packages - Part 1
- Structuring Packages - Part 2
- Namespace Packages
- Importing from Zip Archives
Module 只是型別為 types.ModuleType 的物件,且不一定從檔案載入,模組載入時
與其他傳統語言在編譯期間就將模組導入不同, Python 在執行期間才導入模組,以下是 Import 的整個過程
- 檢查快取中 (可透過 sys.modules 查看) 是否已載入此模組,若否,執行以下操作
- 建立一個空的 types.ModuleType 物件
- 從檔案將程式碼載入記憶體 (檔案尋找根據 sys.path 的順序進行)
- 使用模組檔案名稱作為變量指向到該物件,並將該物件加到 sys.modules 中
- 使用模組檔案名稱或自定義縮寫作為變量指向到該物件,並將該物件加到 global namespace (可透過 globals() 查看)
- 編譯並執行該模組的程式碼
import sys from fractions import Fraction # 查看快取中是否存在已導入的模組 print(sys.modules['fractions']) <module 'fractions' from 'C:\\Users\\User\\Anaconda3\\lib\\fractions.py'> # 查看全域命名空間是否存在導入的物件 print(globals()['Fraction']) <class 'fractions.Fraction'> # 模組內的內容會被紀錄在.__dict__中 import fractions fractions.__dict__ {'__name__': 'fractions', '__doc__': 'Fraction, infinite-precision, real numbers.', '__package__': '', '__loader__': <_frozen_importlib_external.SourceFileLoader at 0x2666f8fbc50>, '__spec__': ModuleSpec(name='fractions', loader=<_frozen_importlib_external.SourceFileLoader object at 0x000002666F8FBC50>, origin='C:\\Users\\User\\Anaconda3\\lib\\fractions.py'), '__file__': 'C:\\Users\\User\\Anaconda3\\lib\\fractions.py', '__cached__': 'C:\\Users\\User\\Anaconda3\\lib\\__pycache__\\fractions.cpython-36.pyc', '__builtins__': {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': _frozen_importlib.BuiltinImporter, '__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>), '__build_class__': <function __build_class__>, '__import__': <function __import__>, 'abs': <function abs(x, /)>, 'all': <function all(iterable, /)>, 'any': <function any(iterable, /)>, ......}
除了 import 之外,我們可透過 importlib 達到相同的功能,而 import 的當下(參考前一節第 3 點),Python 會詢問各個 Finder 是否知道要去哪裡找到程式碼,再交給相應的Loader
import sys
import importlib
# 等同於 import pandas as pd
pd = importlib.import_module('pandas')
# 依序使用 Finder 搜尋模組所在
sys.meta_path
[_frozen_importlib.BuiltinImporter,
_frozen_importlib.FrozenImporter,
_frozen_importlib_external.WindowsRegistryFinder,
_frozen_importlib_external.PathFinder,
<six._SixMetaPathImporter at 0x12ca3e24518>]
# PathFinder 根據 sys.path 搜尋的順序
print(sys.path[:3])
['', # 空字串代表當前目錄
'C:\\Users\\User\\Anaconda3\\python36.zip',
'C:\\Users\\User\\Anaconda3\\DLLs']
# 檢視套件要使用何種Loader
importlib.util.find_spec('pandas')
ModuleSpec(name='pandas', loader=<_frozen_importlib_external.SourceFileLoader object at 0x0000012CA76DC320>, origin='C:\\Users\\User\\Anaconda3\\lib\\site-packages\\pandas\\__init__.py', submodule_search_locations=['C:\\Users\\User\\Anaconda3\\lib\\site-packages\\pandas'])
- 不論是 import A 或是 from A import B ,系統都會將 A 完整載入系統快取,差別只差在將 A 或是 B 連結到 Global Namespace
- 不建議在函數裏面導入模組,會沒有統一的地方進行查看,影響可讀性,且每次呼叫函式,程式需要將快取中的模組再次連結到函式的 Local namespace,除了避免覆蓋變量等特殊考量,平時應盡量避免
在程式運行的過程中,我們可以透過 importlib.reload(modulename) 重新載入模組,但並不能確保已經透過 from A import B 載入舊模組的程式皆重新引用到新模組,因此在正式環境中應避免這類行為
- 透過 if __name__ == '__main__',我們可以賦予模組在命令列被直接調用時的行為,例如:python module.py -r abc
- 當我們從命令列呼叫 python foldername 時,實際上會去執行目錄內的__main__.py
- 套件是一個可以包含子套件及子模組的模組,以檔案系統為架構的套件會以目錄名稱作為套件名稱,且程式碼會記錄在目錄中的 __init__.py
- 若一模組為套件,其 __path__ 屬性內容為套件存在目錄的絕對路徑
使用套件的架構可以讓開發者將完整的程式依功能切分成多個檔案,以便於後續維護
10. Python Updates
- Python 3.10
- Match statement
symbols = {"F": "\u2192", "B": "\u2190", "L": "\u2191",
"R": "\u2193", "pick": "\u2923", "drop": "\u2925"}
def op(command):
match command:
# 若指令為 move 且後綴為 symbols 中的元素
case ['move', *directions] if set(directions) < symbols.keys():
return tuple(symbols[direction] for direction in directions)
case "pick":
return symbols["pick"]
case "drop":
return symbols["drop"]
# _代表任何字元都匹配,可當作預設值
case _:
raise ValueError(f"{command} does not compute!")
[op(["move", "F", "F", "L"]),
op("pick"),
op(["move", "R", "L", "F"]),
op("drop"),]
[('→', '→', '↑'), '⤣', ('↓', '↑', '→'), '⤥']
- zip() 新增 Strict 參數,可在迭代物長度不一時丟出錯誤
l1 = (i ** 2 for i in range(4))
l2 = (i ** 3 for i in range(3))
try:
list(zip(l1, l2, strict=True))
except ValueError as ex:
print(ex)
zip() argument 2 is shorter than argument 1
- Python 3.9
- 時區轉換在以前通常透過 pytz 模組進行處理,3.9 版本內建了 zoneinfo模組可進行相同的處理
import zoneinfo
import pytz
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
# 列出模組內已定義的時區(舊)
for tz in pytz.all_timezones:
print(tz)
# (新)
for tz in sorted(zoneinfo.available_timezones()):
print(tz)
Africa/Abidjan
Africa/Accra
Africa/Addis_Ababa
Africa/Algiers
Africa/Asmara
Africa/Asmera
...
Etc/GMT+8
...
now_utc_naive = datetime.utcnow()
now_utc_naive
datetime.datetime(2022, 3, 20, 6, 1, 3, 368403) # 單純時間物件
now_utc_aware = now_utc_naive.replace(tzinfo=timezone.utc)
datetime.datetime(2022, 3, 20, 6, 1, 3, 368403, tzinfo=datetime.timezone.utc) #加入時區
# 轉換時區(舊)
now_utc_aware.astimezone(pytz.timezone('Australia/Melbourne'))
datetime.datetime(2022, 3, 20, 17, 1, 3, 368403, tzinfo=<DstTzInfo 'Australia/Melbourne' AEDT+11:00:00 DST>)
# (新)
now_utc_aware.astimezone(ZoneInfo("Europe/Dublin"))
datetime.datetime(2022, 3, 20, 6, 1, 3, 368403, tzinfo=zoneinfo.ZoneInfo(key='Europe/Dublin'))
- Math module enhancement
新增了 GCD (最大公約數) 及 LCM (最小公倍數) 函式
- 聯集 (|) 運算子支援使用在字典上,與過去使用 ** operator 一樣會有覆蓋的問題
d1 = {'a': 1, 'b': 2, 'c': 3}
d2 = {'c': 30, 'd': 40}
{**d1, **d2}
{'a': 1, 'b': 2, 'c': 30, 'd': 40}
d1 | d2
{'a': 1, 'b': 2, 'c': 30, 'd': 40}
- 新增了str.removeprefix() 及 str.removesuffix() 方法,在處理字串上更加彈性
txt = "(log) log: [2022-03-01T13:30:01] Log record 1"
txt.lstrip("(log) ") # 移除掉(, l, o, g, ), " "直到出現其它字元
': [2022-03-01T13:30:01] Log record 1'
txt.replace("(log) ", '') # 替換掉所有 "(log) "
'log: [2022-03-01T13:30:01] Log record 1'
txt.removeprefix("(log) ") # 將文字前的 "(log) " 移除,若無則不做異動
'log: [2022-03-01T13:30:02] Log record 2'
- Python 3.8 / 3.7
- Math module enhancement
新增了距離函數
- Position-only arguments
開發者現在可以在傳入參數時使用 / 表示其前面的參數都必須以位置參數的方式傳遞
def my_func(a, b, /):
return a + b
my_func(1, 2)
3
try:
my_func(a=1, b=2)
except TypeError as ex:
print(ex)
my_func() got some positional-only arguments passed as keyword arguments: 'a, b'
- F-string print expression shortcut
F-string 現在可以透過 = 符號將括號內的表示式印出來
from datetime import datetime
from math import pi
d = datetime.utcnow()
e = pi
print(f"{d=}, {e=:.3f}")
d=datetime.datetime(2022, 3, 20, 6, 1, 13, 990493), e=3.142
print(f"{1+2=}")
1+2=3
- Namedtuple 現在支援在宣告時設定預設值
NT = namedtuple("NT", "a b c", defaults = (20, 30))
nt = NT(10)
nt
NT(a=10, b=20, c=30)
- Python 3.6
- Dictionary Ordering
字典內元素的順序為元素被插入的順序,而不再是透過雜湊表決定
- Underscores in Numeric Literals
數值可以用下劃線於內部區隔,但不影響功能,例如:1_000 與 1000 相等
- Preserved Order of kwargs and Named Tuple Application
將 **kwargs 傳入函式時,其元素會依照傳入的順序排列
- f-Strings
F-string 讓我們可以用較緊湊的程式碼取代 str.format,提升可讀性
name = 'Python'
'{aname} rocks'.format(aname=name)
'Python rocks'
f'{name} rocks'
'Python rocks'
- Match statement
symbols = {"F": "\u2192", "B": "\u2190", "L": "\u2191", "R": "\u2193", "pick": "\u2923", "drop": "\u2925"} def op(command): match command: # 若指令為 move 且後綴為 symbols 中的元素 case ['move', *directions] if set(directions) < symbols.keys(): return tuple(symbols[direction] for direction in directions) case "pick": return symbols["pick"] case "drop": return symbols["drop"] # _代表任何字元都匹配,可當作預設值 case _: raise ValueError(f"{command} does not compute!") [op(["move", "F", "F", "L"]), op("pick"), op(["move", "R", "L", "F"]), op("drop"),] [('→', '→', '↑'), '⤣', ('↓', '↑', '→'), '⤥']
- zip() 新增 Strict 參數,可在迭代物長度不一時丟出錯誤
l1 = (i ** 2 for i in range(4)) l2 = (i ** 3 for i in range(3)) try: list(zip(l1, l2, strict=True)) except ValueError as ex: print(ex) zip() argument 2 is shorter than argument 1
- 時區轉換在以前通常透過 pytz 模組進行處理,3.9 版本內建了 zoneinfo模組可進行相同的處理
import zoneinfo import pytz from datetime import datetime, timezone from zoneinfo import ZoneInfo # 列出模組內已定義的時區(舊) for tz in pytz.all_timezones: print(tz) # (新) for tz in sorted(zoneinfo.available_timezones()): print(tz) Africa/Abidjan Africa/Accra Africa/Addis_Ababa Africa/Algiers Africa/Asmara Africa/Asmera ... Etc/GMT+8 ... now_utc_naive = datetime.utcnow() now_utc_naive datetime.datetime(2022, 3, 20, 6, 1, 3, 368403) # 單純時間物件 now_utc_aware = now_utc_naive.replace(tzinfo=timezone.utc) datetime.datetime(2022, 3, 20, 6, 1, 3, 368403, tzinfo=datetime.timezone.utc) #加入時區 # 轉換時區(舊) now_utc_aware.astimezone(pytz.timezone('Australia/Melbourne')) datetime.datetime(2022, 3, 20, 17, 1, 3, 368403, tzinfo=<DstTzInfo 'Australia/Melbourne' AEDT+11:00:00 DST>) # (新) now_utc_aware.astimezone(ZoneInfo("Europe/Dublin")) datetime.datetime(2022, 3, 20, 6, 1, 3, 368403, tzinfo=zoneinfo.ZoneInfo(key='Europe/Dublin'))
- Math module enhancement
新增了 GCD (最大公約數) 及 LCM (最小公倍數) 函式 - 聯集 (|) 運算子支援使用在字典上,與過去使用 ** operator 一樣會有覆蓋的問題
d1 = {'a': 1, 'b': 2, 'c': 3} d2 = {'c': 30, 'd': 40} {**d1, **d2} {'a': 1, 'b': 2, 'c': 30, 'd': 40} d1 | d2 {'a': 1, 'b': 2, 'c': 30, 'd': 40}
- 新增了str.removeprefix() 及 str.removesuffix() 方法,在處理字串上更加彈性
txt = "(log) log: [2022-03-01T13:30:01] Log record 1" txt.lstrip("(log) ") # 移除掉(, l, o, g, ), " "直到出現其它字元 ': [2022-03-01T13:30:01] Log record 1' txt.replace("(log) ", '') # 替換掉所有 "(log) " 'log: [2022-03-01T13:30:01] Log record 1' txt.removeprefix("(log) ") # 將文字前的 "(log) " 移除,若無則不做異動 'log: [2022-03-01T13:30:02] Log record 2'
- Math module enhancement
新增了距離函數 - Position-only arguments
開發者現在可以在傳入參數時使用 / 表示其前面的參數都必須以位置參數的方式傳遞def my_func(a, b, /): return a + b my_func(1, 2) 3 try: my_func(a=1, b=2) except TypeError as ex: print(ex) my_func() got some positional-only arguments passed as keyword arguments: 'a, b'
- F-string print expression shortcut
F-string 現在可以透過 = 符號將括號內的表示式印出來from datetime import datetime from math import pi d = datetime.utcnow() e = pi print(f"{d=}, {e=:.3f}") d=datetime.datetime(2022, 3, 20, 6, 1, 13, 990493), e=3.142 print(f"{1+2=}") 1+2=3
- Namedtuple 現在支援在宣告時設定預設值
NT = namedtuple("NT", "a b c", defaults = (20, 30)) nt = NT(10) nt NT(a=10, b=20, c=30)
- Dictionary Ordering
字典內元素的順序為元素被插入的順序,而不再是透過雜湊表決定 - Underscores in Numeric Literals
數值可以用下劃線於內部區隔,但不影響功能,例如:1_000 與 1000 相等 - Preserved Order of kwargs and Named Tuple Application
將 **kwargs 傳入函式時,其元素會依照傳入的順序排列 - f-Strings
F-string 讓我們可以用較緊湊的程式碼取代 str.format,提升可讀性name = 'Python' '{aname} rocks'.format(aname=name) 'Python rocks' f'{name} rocks' 'Python rocks'
11. Extras
- Random: Seeds
透過 random.seed(a_number) 可以固定隨機數產生所使用的起始點,藉此讓某些程式或實驗具有可重複性
- Timing code using *timeit*
timeit 可在命令列或是程式碼中執行,但要注意的是調用函式 timeit(stmt, setup, globals) 時,import 的部份應放在 setup 而不是要測量的 stmt 參數中
- Don't Use *args and **kwargs Names Blindly
當不定量的參數名稱具有一定意義時,避免使用args, kwargs 這樣無意義的名稱
- Command Line Arguments
若需從命令列執行 Python 腳本,除了手動從 sys.argv 獲取參數外,可透過 argparse 模組進行設定
import argparse
parser = argparse.ArgumentParser(description='testing')
# 設定以 -f 或 --first 觸發字串參數輸入,並儲存到 first_name 變量中,非必要參數,使用時需帶數值
parser.add_argument('-f', '--first', help='specify first name', type=str, required=False, dest='first_name')
# 設定以 -y 或 --yob 觸發整數參數輸入,並儲存到 yob 變量中,為必要參數
parser.add_argument('-y','--yob', help='year of birth', type=int, required=True)
# 設定以 -m 觸發常量 'python',並儲存到 m 變量中,未觸發則為 None,使用時不可帶數值
parser.add_argument('-m', action='store_const', const='Python')
# 設定以 -n 或 --name 觸發字串參數輸入,並儲存到 name 變量中,未觸發則預設為 'John',使用時需帶數值
parser.add_argument('-n', '--name', default='John', type=str)
# 設定以 --sq 觸發零至多個以空格隔開的浮點數輸入,並儲存到 sq 變量中,非必要參數
parser.add_argument('--sq', help='list of numbers to square', nargs='*', type=float)
# 設定以 --cu 觸發一至多個以空格隔開的浮點數輸入,並儲存到 sq 變量中,為必要參數
parser.add_argument('--cu', help='list of numbers to cube', nargs='+', type=float, required=True)
# group 內的參數不可同時觸發
group = parser.add_mutually_exclusive_group()
# 設定以 -v 或 --verbose 觸發 True 的常量,並儲存到 verbose 變量中,未觸發則預設為 False,使用時不可帶數值
group.add_argument('-v', '--verbose', action='store_const', const=True, default=False)
# 設定以 -q 或 --quiet 觸發 False 的常量,並儲存到 quiet 變量中,未觸發則為 True,使用時不可帶數值
group.add_argument('-q', '--quiet', action='store_false')
#取得參數
args = parser.parse_args()
print(args)
C:\Users\User\Python_Tools\Python Deep Dive>python 123.py -h
usage: 123.py [-h] [-f FIRST_NAME] -y YOB [-m] [-n NAME] [--sq [SQ [SQ ...]]]
--cu CU [CU ...] [-v | -q]
testing
optional arguments:
-h, --help show this help message and exit
-f FIRST_NAME, --first FIRST_NAME
specify first name
-y YOB, --yob YOB year of birth
-m
-n NAME, --name NAME
--sq [SQ [SQ ...]] list of numbers to square
--cu CU [CU ...] list of numbers to cube
-v, --verbose
-q, --quiet
C:\Users\User\Python_Tools\Python Deep Dive>python 123.py -y 23 --cu 1 2 --sq -q
Namespace(cu=[1.0, 2.0], first_name=None, m=None, name='John', quiet=False, sq=[], verbose=False, yob=23)
C:\Users\User\Python_Tools\Python Deep Dive>python 123.py -y 23 --cu 1 2 --sq -q -v
123.py: error: argument -v/--verbose: not allowed with argument -q/--quiet
C:\Users\User\Python_Tools\Python Deep Dive>python 123.py -y 23 --cu 1 2 -f
123.py: error: argument -f/--first: expected one argument
C:\Users\User\Python_Tools\Python Deep Dive>python 123.py -y 23 --cu 1 2 -v a
123.py: error: unrecognized arguments: a
- Sentinel Values for Parameter Defaults
有時候我們會需要區分使用者是沒有傳入參數,或是傳入了 None,此時可利用函式的參數預設值在定義時創建這一點,建立不可被輕易取得的物件用以比對
def validate(a=object()):
default_a = validate.__defaults__[0]
if a is not default_a:
print('Argument was provided')
else:
print('Argument was not provided')
validate()
Argument was not provided
validate(None)
Argument was provided
- Simulating a simple switch in Python
在 Python 中還未有 match 語句時,可透過 IF 條件式、字典查找及裝飾器分配函式幾種作法實現相同功能
透過 random.seed(a_number) 可以固定隨機數產生所使用的起始點,藉此讓某些程式或實驗具有可重複性
timeit 可在命令列或是程式碼中執行,但要注意的是調用函式 timeit(stmt, setup, globals) 時,import 的部份應放在 setup 而不是要測量的 stmt 參數中
當不定量的參數名稱具有一定意義時,避免使用args, kwargs 這樣無意義的名稱
若需從命令列執行 Python 腳本,除了手動從 sys.argv 獲取參數外,可透過 argparse 模組進行設定
import argparse
parser = argparse.ArgumentParser(description='testing')
# 設定以 -f 或 --first 觸發字串參數輸入,並儲存到 first_name 變量中,非必要參數,使用時需帶數值
parser.add_argument('-f', '--first', help='specify first name', type=str, required=False, dest='first_name')
# 設定以 -y 或 --yob 觸發整數參數輸入,並儲存到 yob 變量中,為必要參數
parser.add_argument('-y','--yob', help='year of birth', type=int, required=True)
# 設定以 -m 觸發常量 'python',並儲存到 m 變量中,未觸發則為 None,使用時不可帶數值
parser.add_argument('-m', action='store_const', const='Python')
# 設定以 -n 或 --name 觸發字串參數輸入,並儲存到 name 變量中,未觸發則預設為 'John',使用時需帶數值
parser.add_argument('-n', '--name', default='John', type=str)
# 設定以 --sq 觸發零至多個以空格隔開的浮點數輸入,並儲存到 sq 變量中,非必要參數
parser.add_argument('--sq', help='list of numbers to square', nargs='*', type=float)
# 設定以 --cu 觸發一至多個以空格隔開的浮點數輸入,並儲存到 sq 變量中,為必要參數
parser.add_argument('--cu', help='list of numbers to cube', nargs='+', type=float, required=True)
# group 內的參數不可同時觸發
group = parser.add_mutually_exclusive_group()
# 設定以 -v 或 --verbose 觸發 True 的常量,並儲存到 verbose 變量中,未觸發則預設為 False,使用時不可帶數值
group.add_argument('-v', '--verbose', action='store_const', const=True, default=False)
# 設定以 -q 或 --quiet 觸發 False 的常量,並儲存到 quiet 變量中,未觸發則為 True,使用時不可帶數值
group.add_argument('-q', '--quiet', action='store_false')
#取得參數
args = parser.parse_args()
print(args)
C:\Users\User\Python_Tools\Python Deep Dive>python 123.py -h
usage: 123.py [-h] [-f FIRST_NAME] -y YOB [-m] [-n NAME] [--sq [SQ [SQ ...]]]
--cu CU [CU ...] [-v | -q]
testing
optional arguments:
-h, --help show this help message and exit
-f FIRST_NAME, --first FIRST_NAME
specify first name
-y YOB, --yob YOB year of birth
-m
-n NAME, --name NAME
--sq [SQ [SQ ...]] list of numbers to square
--cu CU [CU ...] list of numbers to cube
-v, --verbose
-q, --quiet
C:\Users\User\Python_Tools\Python Deep Dive>python 123.py -y 23 --cu 1 2 --sq -q
Namespace(cu=[1.0, 2.0], first_name=None, m=None, name='John', quiet=False, sq=[], verbose=False, yob=23)
C:\Users\User\Python_Tools\Python Deep Dive>python 123.py -y 23 --cu 1 2 --sq -q -v
123.py: error: argument -v/--verbose: not allowed with argument -q/--quiet
C:\Users\User\Python_Tools\Python Deep Dive>python 123.py -y 23 --cu 1 2 -f
123.py: error: argument -f/--first: expected one argument
C:\Users\User\Python_Tools\Python Deep Dive>python 123.py -y 23 --cu 1 2 -v a
123.py: error: unrecognized arguments: a
有時候我們會需要區分使用者是沒有傳入參數,或是傳入了 None,此時可利用函式的參數預設值在定義時創建這一點,建立不可被輕易取得的物件用以比對
def validate(a=object()):
default_a = validate.__defaults__[0]
if a is not default_a:
print('Argument was provided')
else:
print('Argument was not provided')
validate()
Argument was not provided
validate(None)
Argument was provided
在 Python 中還未有 match 語句時,可透過 IF 條件式、字典查找及裝飾器分配函式幾種作法實現相同功能
沒有留言:
張貼留言