2024年5月3日 星期五

Udemy - Python 3: Deep Dive (Part 1 - Functional) - 2

(第七、九節待補)  

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




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




9. Modules, Packages and Namespaces (待回頭補齊)

  • What is a Module?
    Module 只是型別為 types.ModuleType 的物件,且不一定從檔案載入,模組載入時


  • How does Python Import Modules?
    與其他傳統語言在編譯期間就將模組導入不同, Python 在執行期間才導入模組,以下是 Import 的整個過程
    1. 檢查快取中 (可透過 sys.modules 查看) 是否已載入此模組,若否,執行以下操作
    2. 建立一個空的 types.ModuleType 物件
    3. 從檔案將程式碼載入記憶體 (檔案尋找根據 sys.path 的順序進行)
    4. 使用模組檔案名稱作為變量指向到該物件,並將該物件加到 sys.modules 中
    5. 使用模組檔案名稱或自定義縮寫作為變量指向到該物件,並將該物件加到 global namespace (可透過 globals() 查看)
    6. 編譯並執行該模組的程式碼
      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




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'




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 條件式、字典查找及裝飾器分配函式幾種作法實現相同功能

沒有留言:

張貼留言