2024年4月6日 星期六

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

      

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/
程式碼:


2. Basic review

  • Python types
    • Numbers
      • Integral
        • Integer
        • Boolean
      • Non-integral
        • Float (底層為C Double)
        • Complex
        • Decimal
        • Fraction

    • Collection
      • Sequence
        • Mutable
          • List
        • Immutable
          • Tuple
          • String
      • Set
        • Mutable
          • Set
        • Immutable
          • Frozen set

      • Mapping
        • Dictionary

      • Callable (可被呼叫的物件)
        • User-Defined Function
        • Generator
        • Class
        • Instance Method
        • Class Instance:可被呼叫的類別實例,透過編輯類別的 __call__() 實現
        • Built-in Function
        • Built-in Method

      • Singleton (在Global範圍內唯一的存在)
        • None
        • Notimplemented
        • Ellipsis (...):省略符號運算子,可用來取子集或代替pass


  • Multi-Line Statements and Strings
    為了可讀性,我們有時會將複雜的程式碼以多行的方式呈現,為了做到這一點,編譯器有以下幾種方式進行轉換

    • Implicit
      List, Tuple, Dictionary, Set and Function argument/parameter 都可以做到隱式的多行轉換,例如:
      a = [1, 
          2, 
          3, 
          ]

    • Explicit
      顯示轉換透過反斜線("\")觸發,通常用在冗長的陳述式,例如:
      a = 10
      b = 20
      c = 30
      if a > 5 \
          and b > 10 \
          and c > 20:
          print('yes!!')

    • Multi-Line String Literal
      多行的字串透過使用三個單雙引號建立,通常用在宣告大量文本及註釋時使用,只是要注意引號間的內容會受到程式碼整體的縮排引響,例如:
      def my_func():
          a = '''a multi-line string
          that is actually indented in the second line'''
          return a
      print(my_func())
      
      a multi-line string
          that is actually indented in the second line

  • Variable Names
    變數名稱必須以_, a-z, A-Z開頭,且不得使用Python的保留字,例如:None, if, lambda, ...etc ,除此之外,遵守社群共同的編碼習慣可讓往後的維護及閱讀更容易

    • 物件用途
      • _variable
        Python沒有私有變數,但可用底線開頭命名代表不希望此變數遭到修改,此外這類變數不會透過 from module import * 的語法被直接匯入

      • __variable
        用來避免變數被子類別意外覆蓋,以雙底線前綴命名的變數會被解釋器重寫,例如
        class A():
            def __test(self):
                print("I'm a test method in class A")
        
            def test(self):
                self.__test()
        class B(A):
            def __test(self):
                print("I'm a test method in class B")
        
        b=B()
        b.test()
        b._A__test()
        b._B__test()
        
        I'm a test method in class A
        I'm a test method in class A
        I'm a test method in class B

      • __variable__
        Python內建的名稱,通常具有特定用途,極度不建議使用

    • 物件命名
      • 套件:盡量短,不要有底線
      • 模組:盡量短,可有底線
      • 類別:名稱內字詞開頭都用大寫,不要用底線,例如:BankAccount
      • 函式:全小寫,可用底線
      • 變數:全小寫,可用底線
      • 常數:全大寫,可用底線


  • Conditional
    • Ternary Operator
      X if (condition is true) else Y


  • Function
    • def
      Python 主要透過 def 來建立函式,在建立的當下解釋器會將函式的內容儲存起來,並不會真正調用,例如 fn2 尚未被定義,fn1仍可正常建立:
      def fn1():
          return(fn2())
      
      fn1()
          
      def fn2():
          print("running_fn2")
      
      NameError: name 'fn2' is not defined
      

    • lambda
      透過 Lambda 定義的函式與透過 def 定義的函示只差在有無名稱,通常用在只需回傳簡單表達式及一次性使用的場合
      (lambda x: print(x))('Hi')
      
      Hi


  • While Loop
    • do while
      Python中沒有do while 的語法,只能透過while true + break達成類似效果
       i = 5
      
      while True:
          print(i)
          if i >= 5:
              break
      5

    • while else
      Python 的 while loop 可以搭配 else 使用,而包在else內的程式碼只會在 while loop 正常結束時執行 (無觸發break)
      l = [1, 2, 3]
      val = 10
      
      idx = 0
      while idx < len(l):
          if l[idx] == val:
              break
          idx += 1
      else:
          l.append(val)
      
      print(l)
      
      [1, 2, 3, 10]


  • Try, except and finally in loop
    在 Loop 中 使用 try...except...finally... 語句時,即使使用 break 及continue跳出迴圈,finally中的程式碼依然會先執行,可用來確保關閉資料庫連線…等必要動作
    a = 0
    b = 2
    
    while a < 3:
        print('-------------')
        a += 1
        b -= 1
        try:
            res = a / b
        except ZeroDivisionError:
            print('{0}, {1} - division by 0'.format(a, b))
            res = 0
            break
        finally:
            print('{0}, {1} - always executes'.format(a, b))
            
        print('{0}, {1} - main loop'.format(a, b))
    
    -------------
    1, 1 - always executes
    1, 1 - main loop
    -------------
    2, 0 - division by 0
    2, 0 - always executes


  • For loop
    Python 中的 for loop 讓我們可以遍歷一個可迭代的對象,一次獲取一個值,跟 C 稍有不同,C比較像是定義一個變數的初始值及變化規律,不斷地做重複的事情。


  • Class
    • What is Self?
      在Class 中經常會呼叫 self,這裡的 self 指的是 class 本身,把它替換成其他單詞也能運作,但會造成其他人維護困難= =
      class Rectangle:
          def __init__(what, width, height):
              what.width = width
              what.height = height
              
      a = Rectangle(10, 20)
      a.width        
      
      10

    • Dunder method
      class Rectangle:
          def __init__(self, width, height):
              self.width = width
              self.height = height
          
          def __lt__(self, other):
              if isinstance(other, Rectangle):
                  return self.area() < other.area()
              else:
                  return NotImplemented
      
      r1 < r2
      False
      r1 < r2
      '<' not supported between instances of 'Rectangle' and 'int'
              
      • __init__
        定義實例在建立後初始化的動作
      • __repr__
        定義呼叫class時顯示的字串(用來呈現 class 組成)
      • __str__
        定義列印class時顯示的字串(用來描述 class)
      • __eq__
        定義class 與 == 運算子的行為
      • ......

    • Getter and setter
      考慮到後續維護及更動的成本,JAVA這類語言在封裝類別時通常會將類別屬性設為私有,並實現 getter 及 setter method

      在Python中,並沒有真正私有變數的存在, getter 及 setter 的使用與否應取決於 (1) 是否需對使用者的輸入進行驗證、(2) 是否需建立唯讀屬性、(3)希望在修改其它相關屬性時,連帶修改該屬性
      
      class Rectangle:
          def __init__(self, width, height):
              self.width = width #call setter
              self.height = height #call setter
      
              #self._width = width #NO call setter
              #self._height = height #NO call setter
      def __repr__(self): return 'Rectangle({0}, {1})'.format(self.width, self.height) @property #getter of width def width(self): return self._width @width.setter #setter of width def width(self, width): if width <= 0: raise ValueError('Width must be positive.') self._width = width @property #getter of height def height(self): return self._height @height.setter #setter of height def height(self, height): if height <= 0: raise ValueError('Height must be positive.') self._height = height r1 = Rectangle(0, 10) ValueError: Width must be positive.



3. Variables and memory

  • Reference Counting
    當我們在程式碼中宣告變數與值時,實際上是在記憶體位址中儲存了值,並將變數指向到該記憶體位址,Python 會計算記憶體位址被參照使用的次數,並釋放未被參照到的記憶體空間
    import sys
    import ctypes
    
    my_var = 'zzz'
    print(ctypes.c_long.from_address(id(my_var)).value) #確認my_var記憶體位址的參照次數
    print(sys.getrefcount(my_var)) #確認my_var的參照次數
    
    1 #地址被參照的次數,my_var傳遞給id函式使用後就被釋放了,故只有1次
    2 #地址被參照 + my_var被傳遞給sys.getrefcount = 2次
    
    
    other_var = my_var #實際上other_var 也指向到與my_var相同的記憶體位址
    print(ctypes.c_long.from_address(id(my_var)).value)
    print(sys.getrefcount(my_var))
    
    2
    3


  • Garbage Collection
    儘管 Python 使用 Reference counting 來釋放未使用的記憶體位址,但在某些情況下並不管用,例如物件之間出現循環參照:
    import ctypes
    import gc
    
    def ref_count(address):
        return ctypes.c_long.from_address(address).value
    
    def object_by_id(object_id):
        for obj in gc.get_objects():
            if id(obj) == object_id:
                return "Object exists"
        return "Not found"
    
    class A:
        def __init__(self):
            self.b = B(self)
            
    class B:
        def __init__(self, a):
            self.a = a
            
    gc.disable() #手動關閉GC
    
    my_var = A()
    print('a: \t{0}'.format(hex(id(my_var))))
    print('a.b: \t{0}'.format(hex(id(my_var.b))))
    print('b.a: \t{0}'.format(hex(id(my_var.b.a)))) 
    
    a: 	0x2043ec93ba8
    a.b: 	0x2043ec93f28
    b.a: 	0x2043ec93ba8
    
    a_id = id(my_var)
    b_id = id(my_var.b)
    
    print('refcount(a) = {0}'.format(ref_count(a_id)))
    print('refcount(b) = {0}'.format(ref_count(b_id)))
    print('a: {0}'.format(object_by_id(a_id)))
    print('b: {0}'.format(object_by_id(b_id)))
    
    refcount(a) = 2
    refcount(b) = 1
    a: Object exists
    b: Object exists
    
    my_var= None
    
    print('refcount(a) = {0}'.format(ref_count(a_id)))
    print('refcount(b) = {0}'.format(ref_count(b_id)))
    print('a: {0}'.format(object_by_id(a_id)))
    print('b: {0}'.format(object_by_id(b_id)))
    
    refcount(a) = 1
    refcount(b) = 1
    a: Object exists
    b: Object exists
    
    gc.collect() #手動執行GC
    
    print('refcount(a) = {0}'.format(ref_count(a_id)))
    print('refcount(b) = {0}'.format(ref_count(b_id)))
    print('a: {0}'.format(object_by_id(a_id)))
    print('b: {0}'.format(object_by_id(b_id)))
    
    refcount(a) = 0
    refcount(b) = 0
    a: Not found
    b: Not found
    


  • Dynamic vs Static Typing
    Python 是一種動態型別語言,在宣告變數時不需像 JAVA 這類靜態型別語言明確地指出該變數的型別及變換型別
    #JAVA
    int x;
    x = 5;
    x = "Hello";
    HelloWorld.java:5: error: incompatible types: String cannot be converted to int
    
    #Python
    x = 5
    x = 'hello' #Works fine


  • Variable Re-assignment
  • Object Mutability
    若一個物件的內部狀態時可被改變,則此物件被稱作 mutable,在Python中的型別可以下列清單劃分
    • type
      • Immutable
        my_int = 100
        print(hex(id(my_int)))
        my_int = 50 #對整數重新賦值,實際上是將變數指向到另一個具有該值的記憶體位址
        print(hex(id(my_int)))
        0x7668c630
        0x7668bff0
        • Number
        • String
        • Tuple
        • Frozen set
        • User-defined class
      • Mutable
        my_list = [1, 2, 3]
        print(hex(id(my_list)))
        my_list.append(4) #更改了物件的內部狀態
        print(hex(id(my_list)))
        
        0x2043eadeb88
        0x2043eadeb88
        
        my_list = [1, 2, 3]
        print(hex(id(my_list)))
        my_list = my_list + [4] #實際上是取兩者做運算並指向到新的記憶體位址
        print(hex(id(my_list)))
        
        0x2043eadce88
        0x2043eadedc8
        • List
        • Set
        • Dictionary
        • User-defined class
    • Notice
      所謂的Immutable並不是真的完全不變,當物件元素為mutable,實際上元素還是可做變化,不變的是其元素個數,以下舉例
      a = [1, 2]
      b = [3, 4]
      t = (a, b)
      print(hex(id(t)))
      
      0x2043eadc508
      
      a.append(3)
      b.append(5)
      print(hex(id(t)))
      print(t)
      
      0x2043eadc508
      ([1, 2, 3], [3, 4, 5])


  • Function Argument and Mutability
    當可變物件被當作參數傳遞至參數時,有可能會導致意外的副作用,以下舉例
    def modify_list(items):
        print('initial items # = {0}'.format(hex(id(items))))
        if len(items) > 0:
            items[0] = items[0] ** 2
        items.pop()
        items.append(5)
        print('final items # = {0}'.format(hex(id(items))))
    
    my_list = [2, 3, 4]
    print(my_list)
    print('my_list # = {0}'.format(hex(id(my_list))))
    [2, 3, 4]
    my_list # = 0x12ca75062c8
    
    modify_list(my_list)
    initial items # = 0x12ca75062c8
    final items # = 0x12ca75062c8
    
    print(my_list)
    print('my_list # = {0}'.format(hex(id(my_list))))
    [4, 3, 5]
    my_list # = 0x12ca75062c8
    


  • Shared Reference and Mutability
    在 Python 中,共享引用指的是多個變數引用了相同的記憶體位址,要注意的是,對於 mutable 物件,Python 並不處理這件事情,原因在於唯一能修改Immutable 物件的方法是修改它們引用的記憶體位址,反之則否
    my_list_1 = [1, 2, 3]
    my_list_2 = [1, 2 ,3]
    print(hex(id(my_list_1)))
    print(hex(id(my_list_2)))
    0x12ca74af208
    0x12ca7506b08

  • Variable Equality
    在 Python 中,若我們想比較兩個變數是否擁有一樣的記憶體位址,我們可以使用 is 運算子,若是想比較變數的內容,則是使用==運算子
    a = [1, 2, 3]
    b = [1, 2, 3]
    
    print(hex(id(a)))
    print(hex(id(b)))
    0x27006f17288
    0x27006e968c8
    
    print("a is b: ", a is b)
    print("a == b:", a == b)
    a is b:  False
    a == b: True

  • Everything is an object
    包含函式,所有東西在Python中都可被視為一個物件,因此函式也可以被賦予給變數、傳遞給函式或是被函式回傳

  • Python Optimizations: Interning
    Python底層的 CPython 在啟動時會預先加載 [-5, 256] 之間的整數留用,以避免這些常用的整數頻繁地被創建
    a = 10
    b = 10
    print(id(a))
    print(id(b))
    
    1968827120
    1968827120
    
    a = 500
    b = 500
    print(id(a))
    print(id(b))
    1935322088624 1935322089008

  • Python Optimizations: String Interning
    Python 對於符合命名規則的字串(以字母及下劃線開頭的字串,並只包含字母、數字及下劃線)進行留用,以避免過多的逐字比對耗用大量資源,開發者也可以手動使用 sys.intern method 留用字串,但建議只在需要巨量字串比對時使用
    a = 'hello'
    b = 'hello'
    print(id(a))
    print(id(b))
    
    1342722069536
    1342722069536
    
    a = 'hello world'
    b = 'hello world'
    print('a==b:', a==b)
    print('a is b:', a is b)
    a==b: True a is b: False
    import sys
    a = sys.intern('hello world')
    b = sys.intern('hello world')
    c = 'hello world'
    print(id(a))
    print(id(b))
    print(id(c))
    1342722172080 1342722172080 1342722174896
    print('a==b:', a==b)
    print('a is b:', a is b)
    a==b: True a is b: True
    字串是否留用在巨量字串比對時顯出效能差異
    def compare_using_equals(n):
        a = 'a long string that is not interned' * 200
        b = 'a long string that is not interned' * 200
        for i in range(n):
            if a == b:
                pass
    def compare_using_interning(n):
        a = sys.intern('a long string that is not interned' * 200)
        b = sys.intern('a long string that is not interned' * 200)
        for i in range(n):
            if a is b:
                pass
            
    import time
    
    start = time.perf_counter()
    compare_using_equals(10000000)
    end = time.perf_counter()
    
    print('cost: ', end-start)
    cost: 2.96
    
    start = time.perf_counter()
    compare_using_interning(10000000)
    end = time.perf_counter()
    
    print('cost: ', end-start)
    cost: 0.28
    
    

  • Python Optimizations: Peephole
    在編譯之前的階段,解釋器會對程式碼中的部分先行編譯,以避免在接下來的階段重複進行相同的動作
    • 常量表達式
      def my_func():
          a = 24 * 60
          b = (1, 2) * 5
          c = 'abc' * 3
          d = 'ab' * 11
          e = 'the quick brown fox' * 10
          f = [1, 2] * 5
      my_func.__code__.co_consts
      
      (None,
       24,
       60,
       1,
       2,
       5,
       'abc',
       3,
       'ab',
       11,
       'the quick brown fox', #因太長沒有預先編譯10倍長度的字串
       10,
       1440, #預先編譯的數字
       (1, 2),
       (1, 2, 1, 2, 1, 2, 1, 2, 1, 2), #預先編譯的短序列
       'abcabcabc') #預先編譯的短序列
      • 數字運算
      • 長度小於20的序列

    • 集合成員比對
      在程式碼中若出現 if a in [1,2,3] 這樣的比對成員條件句,解釋器會將後方的常量序列轉為 immutable,這會使得成員比對更加快速
      def my_func():
          if e in [1, 2, 3]:
              pass
      my_func.__code__.co_consts
      (None, 1, 2, 3, (1, 2, 3))
      
      def my_func():
          if e in {1, 2, 3}:
              pass
      my_func.__code__.co_consts
      (None, 1, 2, 3, frozenset({1, 2, 3}))
      當需要進行巨量的序列成員比對時,盡量使用 Set 取代 List 及 Tuple,可得到驚人的效能改善
      import string
      import time 
      
      char_list = list(string.ascii_letters)
      char_tuple = tuple(string.ascii_letters)
      char_set = set(string.ascii_letters)
      
      def membership_test(n, container):
          for i in range(n):
              if 'p' in container:
                  pass
              
      start = time.perf_counter()
      membership_test(10000000, char_list)
      end = time.perf_counter()
      print('cost: ', end-start)       
      cost: 2.6
      
      start = time.perf_counter()
      membership_test(10000000, char_tuple)
      end = time.perf_counter()
      print('cost: ', end-start)
      cost: 2.6
      
      start = time.perf_counter()
      membership_test(10000000, char_set)
      end = time.perf_counter()
      print('cost: ', end-start)
      cost: 0.4




4. Numeric types

  • Integers: 
    • Operations
      在 Python 中,沒有像 JAVA 提供固定位數的整數型態,而是隨著數字大小增加使用的位元數,要注意的是隨著整數大小增加,其運算所需時間也會增加
      import sys
      
      sys.getsizeof(0)
      24
      
      sys.getsizeof(1)
      28
      
      sys.getsizeof(2**1000)
      160

    • Constructors and Bases
      整數在 Python 中可透過單純的表達式及 int 函式宣告,除此之外還有像是bin(), oct(), hex() 等其它函示可宣告不同位元進制的整數,如要轉換不同進制的整數,可透過 n = b * (n // b) + (n % b) 的等式進行迭代
      print(int(10))
      10
      print(int(10.9))
      10 # 小數點後被truncate 
      print(int(-10.9))
      -10 # 小數點後被truncate 
      from fractions import Fraction a = Fraction(22, 7) print(int(a)) 3 print(int("10")) 10 print(int("101", 2)) 5 print(int("101", base=2)) 5 print(int("F1A", base=16)) 3866 print(bin(10), oct(10), hex(10)) 0b1010 0o12 0xa # 0b, 0o 及 0x 代表這些數字的進制 print(int('1010', 2), int('0b1010', 2), 0b1010) 10 10 10


  • Rational Numbers
    有理數可以表達為兩個整數構成的分數,在Python 中可以使用Fraction 模組進行這類數字的宣告。對於無理數,分數只能給到近似值,使用者可透過限制分母大小來簡化輸出
    from fractions import Fraction
    import math
    
    Fraction(1, 3)
    Fraction(1, 3)
    
    Fraction(0.125)
    Fraction(1, 8)
    
    Fraction('10.5')
    Fraction(21, 2)
    Fraction('22/7')
    Fraction(22, 7)
    
    Fraction(8, 16)
    Fraction(1, 2) # 自動約分
    
    Fraction(1, -4)
    Fraction(-1, 4) # 負數會被歸在分子
    
    f1 = Fraction(2, 3)
    f2 = Fraction(3, 4)
    f3 = f1 + f2
    print(f3, f3.numerator, f3.denominator)
    17/12    17    12
    
    x = Fraction(math.pi)
    print(x, float(x))
    884279719003555/281474976710656    3.141592653589793
    
    y = x.limit_denominator(1000)
    print(y, float(y))
    355/113    3.1415929203539825


  • Floats
    • Internal Representations
      跟整數不同,浮點數使用了固定長度為64的位元,由於長度固定,並非所有小數都可以用二進位制表示,因此計算上會出現不可避免的誤差
      format(0.125, '.25f')
      '0.1250000000000000000000000'
      format(0.1, '.25f')
      '0.1000000000000000055511151' # 無法用二進制表示

    • Equality Testing - Lecture
      根據前一節的內容,程式在比對兩個浮點數是否相等時必須進行特殊的處理,直觀來講有兩種方式,當兩個數字間差異小於 "絕對誤差" 或 "相對誤差" 時即認定兩個數字為相等,但兩種方法都有其缺點,因此我們參照 PEP485 的建議,同時使用兩種誤差進行判定,具體實作可透過 math.isclose() 函式:
      from math import isclose
      
      x, y = 0.0000001, 0.0000002
      print('x = y (abs):', isclose(x, y, abs_tol=0.0001, rel_tol=0))
      print('x = y (rel):', isclose(x, y, abs_tol=0            , rel_tol=0.01))
      print('x = y (mix):', isclose(x, y, abs_tol=0.0001, rel_tol=0.01))
      x = y (abs): True
      x = y (rel): False # 相對誤差在兩個數字接近零時有問題
      x = y (mix): True
          
      a, b = 123456789.01, 123456789.02
      print('a = b (abs):', isclose(a, b, abs_tol=0.0001, rel_tol=0))
      print('a = b (rel):', isclose(a, b, abs_tol=0            , rel_tol=0.01))
      print('a = b (mix):', isclose(a, b, abs_tol=0.0001, rel_tol=0.01))
      a = b (abs): False # 絕對誤差在比較兩個大數時有問題
      a = b (rel): True
      a = b (mix): True
      • 絕對誤差:ε_abs
      • 相對誤差:ε_rel * max(|a|, |b|) 
      • 混合誤差:max(ε_abs, ε_rel * max(|a|, |b|) )

    • Coercing to Integers and rounding
      在 Python 中有數種方式對浮點數進行轉換--
      • Truncate:捨棄該數字的小數點後面位數
      • Floor:找到比該數字小的最大整數
      • Ceiling:找到比該數字大的最小整數
      • Round:找到最接近該數字有效位數的數字,若最接近的數字不只一個,取有效位數為偶數的數字(非四捨五入)
        from math import trunc, floor, ceil
        
        print(trunc(10.6), trunc(-10.6))
        print(floor(10.6), floor(-10.6))
        print(ceil(10.6), ceil(-10.6))
        10 -10
        10 -11
        11 -10
        
        def round45(x): # 四捨五入
            from math import copysign
            return int(x + 0.5 * copysign(1, x))
        
        print(round(1.5), round45(1.5))
        print(round(2.5), round45(2.5))
        2 2
        2 3 # 可以看到內建的函式非四捨五入


  • Decimals
    為避免浮點數的計算誤差,Python內建了Decimal類別來處理這個問題
    • Constructors
      小數在宣告時可使用數字表達式、字串、Tuple,但要注意不要直接傳入非整數的數字表達式進行宣告,模組會將以二進制儲存的資料轉換成小數類別

      import decimal
      from decimal import Decimal
      
      Decimal(10)
      Decimal('10')
      Decimal(-10)
      Decimal('-10')
      
      Decimal('0.1')
      Decimal('0.1')
      Decimal('-3.1415')
      Decimal('-3.1415')
      
      Decimal ((0, (3,1,4,1,5), -4)) # 正負號、數值、指數
      Decimal('3.1415')
      Decimal((1, (1,2,3,4), -3))
      Decimal('-1.234')
      Decimal((0, (1,2,3), 3))
      Decimal('1.23E+5')
      
      Decimal(0.1) # 直接傳入非整數會有問題
      Decimal('0.1000000000000000055511151231257827021181583404541015625')

    • Contexts
      可以透過decimal.getcontext() 及 decimal.localcontext() 修改小數的 Global 及 Local 精度及求整方式

      a = Decimal('0.12345')
      b = Decimal('0.12345')
      decimal.getcontext().prec=3 # 調整計算精度為小數點下三為
      
      print('a+b within default global context: {0}'.format(a+b))
      a+b within global context: 0.247
      
      with decimal.localcontext() as ctx:
          ctx.prec = 2
          c = a + b
          print('c within local context: {0}'.format(c))
      c within local context: 0.25  
      
      print(decimal.getcontext().rounding)  
      'ROUND_HALF_EVEN' # 預設一樣為banker's rounding,調整為ROUND_HALF_UP即為四捨五入

    • Math operations
      對小數使用 div 及 mod 運算子的結果與整數不同,但 n = b * (n // b) + (n % b) 的等式依然成立,另外對於小數,需使用小數類別的method 進行運算,直接使用 math 模組會將小數轉換為浮點數,從而失去使用小數的意義
      # a//b = floor(a/b)
      x = -10
      y = 3
      print(x//y, x%y)
      print(divmod(x, y))
      print( x == y * (x//y) + x % y)
      -4 2
      (-4, 2)
      True
      
      # a//b = trunc(a/b)
      a = Decimal('-10')
      b = Decimal('3')
      print(a//b, a%b)
      print(divmod(a, b))
      print( a == b * (a//b) + a % b)
      -3 -1
      (Decimal('-3'), Decimal('-1'))
      True
      
      x = 0.01
      x_dec = Decimal('0.01')
      
      root_float = math.sqrt(x)
      root_mixed = math.sqrt(x_dec)
      root_dec = x_dec.sqrt()
      
      print(format(root_float, '1.27f'))
      print(format(root_mixed, '1.27f'))
      print(root_dec)
      0.010000000000000001942890293
      0.010000000000000001942890293
      0.01
      

    • Performance considerations
      小數本身的資源使用率及運算效能都不及浮點數,因此僅在必要時使用。
      import sys
      from decimal import Decimal
      a = 3.1415
      b = Decimal('3.1415')
      sys.getsizeof(a)
      24
      
      sys.getsizeof(b)
      104  # 小數使用的記憶體比浮點數多出了80bit!!
      
      import time
      from decimal import Decimal
      
      def run_float(n=1):
          for i in range(n):
              a = 3.1415
              
      def run_decimal(n=1):
          for i in range(n):
              a = Decimal('3.1415')
      
      n = 10000000
      start = time.perf_counter()
      run_float(n)
      end = time.perf_counter()
      print('float: ', end-start)
      float:  0.21406484986433047
      
      start = time.perf_counter()
      run_decimal(n)
      end = time.perf_counter()
      print('decimal: ', end-start)
      decimal:  2.1353148079910156 # 僅僅是創建時間就與浮點數相差了約十倍


  • Complex Numbers
    複數較少用到,其數學運算有專門的 cmath 模組可以呼叫
    • Constructor
      a = complex(1, 2)
      b = 1 + 2j
      print(a == b)
      True

    • Operation
      print(a.real, type(a.real))
      print(a.imag, type(a.imag))
      print(a.conjugate())
      1.0 <class 'float'> # 複數中的實部跟虛部都是以浮點數的形式儲存
      2.0 <class 'float'>
      (1-2j)
      
      import cmath
      
      a = 1 + 5j
      print(cmath.sqrt(a))
      (1.7462845577958914+1.4316108957382214j)
      
      a = 1 + 1j
      r = abs(a)
      phi = cmath.phase(a)
      print('{0} = ({1},{2})'.format(a, r, phi))
      (1+1j) = (1.4142135623730951,0.7853981633974483)
      
      r = math.sqrt(2)
      phi = cmath.pi/4
      print(cmath.rect(r, phi))
      (1.0000000000000002+1.0000000000000002j)
      
      print(cmath.isclose(cmath.rect(r, phi), 1+1J, abs_tol=0.00001))
      True


  • Booleans
    布林是整數的子類別,True 及 False 也是單例物件,基本上運作等同於整數中的 1 跟 0,但存放的記憶體位址不同,且另外多了AND 及 OR 等運算子
    print(issubclass(bool, int))
    True
    
    print(type(True), id(True), int(True))
    (bool, 1658060976, 1)
    print(type(False), id(False), int(False))
    (bool, 1658061008, 0)
    
    print(isinstance(True, bool))
    True
    print(isinstance(True, int))
    True
    
    
    print(id(True), id(1 < 2))
    (1658060976, 1658060976)
    
    print(1 == True, 1 is True)
    (True, False) # 1與True值相等,但存放的記憶體位址不同
    
    print(bool(1), bool(0), bool(-1))
    (True, False, True) # 非零整數的布林值皆為True
    • Truth Values
      除了下列情況,每個物件都有 True 的真值,因此在檢查序列時可已利用這一點簡化 IF 判斷式
      • None
      • False
      • 任何數值為 0 的數值型態
      • 空序列,例如[], (), ''
      • 空集合,例如{} 
      • 自定義的類別,且實作的 __bool__ 或 __len__ 方法回傳了 0 或False
        from fractions import Fraction
        from decimal import Decimal
        
        print(bool(100))
        True
        
        print((100).__bool__())
        True
        
        print(bool(10), bool(1.5), bool(Fraction(3, 4)), bool(Decimal('10.5')))
        (True, True, True, True)
        print(bool(0), bool(0.0), bool(Fraction(0,1)), bool(Decimal('0')), bool(0j))
        (False, False, False, False, False)
        
        print(bool([1, 2, 3]), bool((1, 2, 3)), bool('abc'), bool(1j))
        (True, True, True, True)
        print(bool([]), bool(()), bool(''))
        (False, False, False)
        
        print(bool({'a': 1}), bool({1, 2, 3}))
        (True, True)
        print(bool({}), bool(set()))
        (False, False)
        
        print(bool(None))
        False
        
        a = []
        if a: # 等價於 if a is not None and len(a) > 0:
            print(a[0])
        else:
            print('a is None, or a is empty')
        a is None, or a is empty

    • Precedence
      布林運算的優先層級如下,但為了可讀性,能加上括號的話還是盡量加
      1. ()
      2. <, >, <=, >=, ==, !=, in, is
      3. not
      4. and
      5. or
        True or True and False
        True
        (True or True) and False
        False
    • Short-Circuiting
      在邏輯運算中,當第一個運算數的值無法確定邏輯運算結果時,才對第二個運算數進行求值,我們可以利用這一點對IF判斷式進行簡化
      • X AND Y:若 X 為False,則略過對Y的運算,回傳False
      • X OR Y:若 X 為True,則略過對Y的運算,回傳True
        name = ""
        if name[0] in string.digits:
            print('Name cannot start with a digit!')
        IndexError: string index out of range
        
        if name and name[0] in string.digits: # 因為 name 空字串為False,and後面的判斷被直接略過
            print('Name cannot start with a digit!')
            
        name = '1Bob'
        if name and name[0] in string.digits:
            print('Name cannot start with a digit!')
        Name cannot start with a digit!

    • Boolean Operators
      在 Python 中,AND 及 OR 的實際作用有點不同,我們可以利用這一點達到設定預設值,或是做預先檢查的功能
      • X AND Y:若 X 為True,回傳Y,否則回傳X
      • X OR Y:若 X 為False,回傳Y,否則回傳X
        s1 = None
        s2 = ''
        s3 = 'abc'
        
        print((s1 and s1[0]) or 'NA') # 若字串不為空或None,就取字串第1個字,反之回傳字串被'NA'取代
        print((s2 and s2[0]) or 'NA')
        print((s3 and s3[0]) or 'NA')
        NA
        NA
        a
        
        if s1: # 等價於上面寫法
            print(s1[0])
        else:
            print('NA')
        NA

    • Chained Comparisons
      在 Python 中,連續出現的運算子會被以AND進行連接,例如 A<B>C 會被解讀成 A<B and B>C



5. Function parameters

  • Argument vs Parameter
    定義函式時,所傳入的變量叫做Parameter,但在實際調用函式時,傳入的變量此時稱作Argument


  • Positional and Keyword Arguments
    調用函式時會依據我們定義的順序將變量傳入,若有預設值需要設定,也可在定義時先行給予預設值,但要注意的是不論是在定義或是調用函數,預設值及指定變量給予都必須排在未指定的變量後面,否則解釋器會無法判斷要將記憶體位址引用給哪一個變量
    def my_func(a, b, c):
        print("a={0}, b={1}, c={2}".format(a, b, c))
    my_func(3, 50, 1)   
    a=3, b=50, c=1
    
    def my_func(a, b, c=20): # 定義 C 的預設值為20
        print("a={0}, b={1}, c={2}".format(a, b, c))    
    my_func(50, 10) # 此時呼叫函式不傳入C也是可以的
    a=50, b=10, c=20
    
    def my_func(a=6, b, c): # 預設值的定義要排在最後面,否則會出錯
        print("a={0}, b={1}, c={2}".format(a, b, c))    
    SyntaxError: non-default argument follows default argument
    
    my_func(3, b=50, 1) # 呼叫函式時帶變量名稱也須排在最後面
    SyntaxError: positional argument follows keyword argument


  • Unpacking Iterables
    Unpacking 可迭代對象時,Python 會先在記憶體中創建一組新的tuple,其元素為等號右方變量所引用的記憶體位址,接著再將等號左方的變量指向新的記憶體位址,unpacking 時應注意等號左右兩邊的元素數量須相等
    l = [1, 2, 3, 4]
    a, b, c, d = l
    print(a, b, c, d)
    1 2 3 4
    
    # 利用unpacking 交換變量參照位址,實現交換變數內容
    a = 10
    b = 20
    print("a={0}, b={1}".format(a, b))
    a=10, b=20
    a, b = b, a
    print("a={0}, b={1}".format(a, b))
    a=20, b=10
    
    # 集合及字典是無序的,不一定會按照你想要的順序進行解包
    s = {'p', 'y', 't', 'h', 'o', 'n'}
    a, b, c, d, e, f = s
    print(a,b,c,d,e,f)
    n o p t y h
    
    a = (1, 2, 3)
    print(type(a))
    tuple
    a = 1, 2, 3
    print(type(a))
    tuple
    a = (1)
    print(type(a))
    int
    a = (1,)
    print(type(a))
    tuple
    a = 1, # 僅需逗號即可宣告為tuple
    print(type(a))
    tuple
    
    a = () # 同等於 a = tuple(),唯一一個不須逗號的特例
    print(type(a))
    tuple


  • Extended Unpacking - Lecture
    在 Python 中,我們可以使用下列兩個運算子協助 unpacking
    • * operator
      * 運算子可以將剩餘的元素 unpacking 到 list 中,其出現時需包含在 list 或 tuple 內,且在等號左方只能出現一次
      l = [1, 2, 3, 4, 5, 6]
      a, b, c = l[0], l[1:-1], l[-1]
      print(a,b,c)
      1 [2, 3, 4, 5] 6
      
      a, *b, c = l # 等價於上方寫法,但更加彈性
      print(a,b,c)
      1 [2, 3, 4, 5] 6
      
      
      l1 = [1, 2, 3]
      s = 'ABC'
      l = [*l1, *s] # 利用 * operator 將兩個物件的元素 unpacking 到同一個 list 中,實現合併兩個可迭代物件
      print(l)
      [1, 2, 3, 'A', 'B', 'C']

    • ** operator
      ** operator 可用來unpacking dictionary,同時保留鍵值對的內容,但要注意的是 key 具有唯一性,後面合併的相同 key 會覆蓋之前的內容
      d1 = {'key1': 1, 'key2': 2}
      d2 = {'key2': 3, 'key3': 3}
      print([*d1, *d2])
      ['key1', 'key2', 'key2', 'key3']
      
      print({**d1, **d2})
      {'key1': 1, 'key2': 3, 'key3': 3} 
      # 常見的應用是當程式需要將用戶自定義設定覆蓋預設值時,透過這類方式來合併兩個設定檔的字典
      
      


  • function parameters
    在定義函式參數時,常看到以下型式-def func1(a, b, c=1, *args/*, kw1, kw2=1, **kwargs),以下對這些定義方式做介紹
    • a, b, c=10:positional parameters,可給定參數預設值,但該參數須排在無預設值的參數後方,無預設值的參數在調用時必須給予該參數數值

    • *args:將剩餘的 positional arguments 打包到名稱為 args 的 tuple 內,*args 後面不得有 positional parameters

    • *:讓解釋器知道後面沒有其它 positional parameters

    • kw1, kw2=1:Keyword-only parameters,可給定參數預設值,無預設值的參數在調用時必須給予該參數數值,且調用時必須列出參數名稱

    • **kwargs:將剩餘的 Keyword-only arguments 打包到名稱為 args 的 dictionary 內
      def func(a, b, *args):
          print(a, b, args)
      func(1, 2, 'x', 'y', 'z')
      1 2 ('x', 'y', 'z')
      
      def func(*, a=1, b):
          print(a, b)
      func(a=10, b=20)
      10 20
      
      def func(a, b, *, c, d):
          print(a, b, c, d)
      func(1, 2, c=3, d=4)
      1 2 3 4
      
      def func(a, b=2, *, c, d=4):
          print(a, b, c, d)
      func(1, c=3)
      1 2 3 4    
          
      def func(a, b=2, *args, c=3, d): # 同時使用*args 及 default positional parameter 時,解釋器會分不清楚參數要給b還是給args
          print(a, b, args, c, d)
      func(1, 'x', 'y', 'z', c=3, d=4)
      1 'x' ('y', 'z') 3 4
      
      def func(a, b, *, c, d=4, **kwargs):
          print(a, b, c, d, kwargs)
      func(1, 2, c=3, x=100, y=200, z=300)
      1 2 3 4 {'x': 100, 'y': 200, 'z': 300}
      
      def func(a, b, *args, c, d=4, **kwargs):
          print(a, b, args, c, d, kwargs)
      func(1, 2, 'x', 'y', 'z', c=3, d=5, x=100, y=200, z=300)
      1 2 ('x', 'y', 'z') 3 5 {'x': 100, 'y': 200, 'z': 300}    
          
      def func(*args, **kwargs):
          print(args, kwargs)   
      func(1, 2, 3, x=100, y=200, z=300)
      (1, 2, 3) {'x': 100, 'y': 200, 'z': 300}    



  • Application: A Simple Function Timer
    def time_it(fn, *args, rep=5, **kwargs): # 傳入args 及 kwargs 給 fn 使用
        start = time.perf_counter()
        for i in range(rep):
            fn(*args, **kwargs)
        end = time.perf_counter()
        return (end - start) / rep
    
    def compute_powers_2(n, *, start=1, end):
        # using a list comprehension
        return [n**i for i in range(start, end)]
    
    time_it(compute_powers_2, 2, end=20000, rep=4)
    0.340482875
    


  • Parameter Defaults - Beware!!
    在定義函式參數的初始值時,有兩點需要注意,第一點是初始值的宣告在函式定義時完成,而非調用函式當下,第二點則是使用可變物件做初始值可能造成的副作用
    # Example 1
    
    from datetime import datetime
    import time
    def log(msg, *, dt=datetime.utcnow()): # 定義函式時所給的預設值在宣告後就會被建立,在調用函式時是不會變的
        print('{0}: {1}'.format(dt, msg))
    log('message 1')
    2024-04-17 06:54:22.158788: message 1
    
    time.sleep(1)
    log('message 2')
    2024-04-17 06:54:22.158788: message 2
                
    def log(msg, *, dt=None): # 建議做法是給定 None 為預設值,在函式內做判斷
        dt = dt or datetime.utcnow()
        print('{0}: {1}'.format(dt, msg))    
    log('message 1')
    2024-04-17 06:54:23.159672: message 1
                
    time.sleep(1)
    log('message 2')
    2024-04-17 06:54:24.166576: message 2
    
    
    # Example 2
    
    def add_item(name, quantity, unit, grocery_list=[]): # 由於初始值在定義時就被建立,後面使用到的 grocery_list都是同一個
        item_fmt = "{0} ({1} {2})".format(name, quantity, unit)
        grocery_list.append(item_fmt)
        return grocery_list
    
    store_1 = add_item('bananas', 2, 'units')
    add_item('grapes', 1, 'bunch', store_1)
    print(store_1)
    ['bananas (2 units)', 'grapes (1 bunch)']
    
    store_2 = add_item('milk', 1, 'gallon')
    print(store_2)
    ['bananas (2 units)', 'grapes (1 bunch)', 'milk (1 gallon)']
    
    
    def add_item(name, quantity, unit, grocery_list=None): # 建議作法一樣是定義為None 再由內部判斷
        if not grocery_list:
            grocery_list = []
        item_fmt = "{0} ({1} {2})".format(name, quantity, unit)
        grocery_list.append(item_fmt)
        return grocery_list
    
    store_1 = add_item('bananas', 2, 'units')
    add_item('grapes', 1, 'bunch', store_1)
    print(store_1)
    ['bananas (2 units)', 'grapes (1 bunch)']
    
    store_2 = add_item('milk', 1, 'gallon')
    print(store_2)
    ['milk (1 gallon)']
    



6. First-Class Functions

  • Higher Order Function
  • 在Python中,包括函式在內的所有物件都是 First-Class 物件,其定義為
    • 物件可以被當作參數傳遞
    • 物件可以被函式回傳
    • 物件可以被賦予給變量
    • 物件可以被資料結構(List, Tuple, ...)儲存
        其中,可接收函式做為參數或是可回傳函式的函式,又被稱做Higher Order
        Function,例如後面要介紹的map, filter, sorted, ...等


  • Docstrings
  • 根據 PEP257,函式內第一行的字串會被視為 docstring,通常使用三重引號進行定義,用意是替撰寫的函式加上一段文字說明,跟comment 會被解釋器忽略不同,docstring 會被儲存在函式的.__doc__屬性中,可使用help(function) 來觀看內容
    def my_func(a, b):
        'Returns the product of a and b'
        return a*b
    
    help(my_func)
    Help on function my_func in module __main__:
    
    my_func(a, b)
        Returns the product of a and b
        
    my_func.__doc__
    'Returns the product of a and b'


  • Annotations
    在 PEP3107 中引入了另一種針對函式傳入參數及回傳值的註解方式,稱做annotation,其定義方式為 def func(p1: expression) -> expression:,註釋內容會被儲存在函式的.__annotations__屬性內,且同樣可被 help 函式叫出,除了註釋的功能,解釋器可以利用 annotation 做輸出輸入的型別檢查,但要注意的是該檢查並不具有強制力。

    def my_func(a: "an positive integer", b: int= 1) -> int: # 註釋a是正整數、b型態為整數且預設值1、回傳值型態為整數
        return a*b
    
    x = 3
    y = 5
    def my_func(a: str) -> 'a repeated ' + str(max(x, y)) + ' times':
        return a*max(x, y)
    
    help(my_func)
    Help on function my_func in module __main__:
    
    my_func(a:str) -> 'a repeated 5 times'
    
    x=10
    help(my_func)
    Help on function my_func in module __main__:
    
    my_func(a:str) -> 'a repeated 5 times' # annotation 在宣告時就已被定義,要注意可變/不可變物件的影響
    
    my_func.__annotations__
    {'a': str, 'return': 'a repeated 5 times'}
    


  • Lambda Expressions
    Lambda 表達式是另一種宣告函式的方法,因為不需要提供函式名稱,又被稱作匿名函式,與一般用 def 宣告的函式幾乎相同,差異在於不能撰寫docstring、annotation 且無法對變量進行賦值,僅作為表達式求值
    func = lambda x, *args, y1, y2=7, **kwargs: (x, args, y1, y2, kwargs)
    type(func)
    function
    
    func(1, 'a', 'b', y1=100, a=10, b=20)
    (1, ('a', 'b'), 100, 7, {'a': 10, 'b': 20})
    
    def apply_func(fn, *args, **kwargs):
        return fn(*args, **kwargs)
    apply_func(func, 1, 2, 3, y1=50 ,other=99, another =100)
    (1, (2, 3), 50, 7, {'other': 99, 'another': 100})
    
    範例:使用 sorted 建立亂數排序
    import random
    sorted(range(5), key=lambda x: random.random()) # 傳入key 參數,以 random 值作為排序依據
    [3, 1, 2, 4, 0]
    [1, 0, 3, 2, 4]
    [2, 0, 3, 4, 1]
    


  • Function Introspection
    除了使用 dir() 手動查看 function 中的屬性,如.__defaults__, .__kwdefaults__ & .__code__, ...等,Python 的 
    inspect模組提供了許多方法讓開發者更方便地查看這些資訊
    # TODO: Provide implementation
    def my_func(a: 'a string', 
                b: int = 1, 
                *args: 'additional positional args', 
                kw1: 'first keyword-only arg', 
                kw2: 'second keyword-only arg' = 10,
                **kwargs: 'additional keyword-only args') -> str:
        """does something
           or other"""
        pass
    
    dir(my_func)
    ['__annotations__',
     '__call__',
     '__class__',
     '__closure__',
     '__code__',
     '__defaults__',
     '__delattr__',
     '__dict__',
     '__dir__',
     '__doc__',
     ...]
    
    import inspect
    
    def print_info(f: "callable") -> None:
        print(f"{f.__name__} from module {inspect.getmodule(f)}")
        print('=' * len(f.__name__), end='\n\n')
        
        print('{0}\n{1}\n'.format(inspect.getcomments(f), 
                                  inspect.cleandoc(f.__doc__)))
        
        print('{0}\n{1}'.format('Inputs', '-'*len('Inputs')))
        
        sig = inspect.signature(f)
        for param in sig.parameters.values():
            print('Name:', param.name)
            print('Default:', param.default)
            print('Annotation:', param.annotation)
            print('Kind:', param.kind)
            print('--------------------------\n')
            
        print('{0}\n{1}'.format('\n\nOutput', '-'*len('Output')))
        print(sig.return_annotation)
        print('--------------------------\n')
        print('function body:\n\n')
        print(inspect.getsource(f))
    
        
    print_info(my_func)
    my_func from module <module '__main__'>
    =======
    
    # TODO: Provide implementation
    
    does something
    or other
    
    Inputs
    ------
    Name: a
    Default: <class 'inspect._empty'>
    Annotation: a string
    Kind: POSITIONAL_OR_KEYWORD
    --------------------------
    Name: b
    Default: 1
    Annotation: <class 'int'>
    Kind: POSITIONAL_OR_KEYWORD
    --------------------------
    Name: args
    Default: <class 'inspect._empty'>
    Annotation: additional positional args
    Kind: VAR_POSITIONAL
    --------------------------
    Name: kw1
    Default: <class 'inspect._empty'>
    Annotation: first keyword-only arg
    Kind: KEYWORD_ONLY
    --------------------------
    Name: kw2
    Default: 10
    Annotation: second keyword-only arg
    Kind: KEYWORD_ONLY
    --------------------------
    Name: kwargs
    Default: <class 'inspect._empty'>
    Annotation: additional keyword-only args
    Kind: VAR_KEYWORD
    --------------------------
    
    Output
    ------
    <class 'str'>
    --------------------------
    function body:
    
    def my_func(a: 'a string', 
                b: int = 1, 
                *args: 'additional positional args', 
                kw1: 'first keyword-only arg', 
                kw2: 'second keyword-only arg' = 10,
                **kwargs: 'additional keyword-only args') -> str:
        """does something
           or other"""
        pass
    
    在 inspect.signature(f).parameters.values.kind 回傳的種類中有一種Position_only ,開發者不能在定義函式時撰寫,只有Python 內建的韓式可以使用,舉例像是 str.replace(old, new)
    help(str.replace)
    Help on method_descriptor:
    
    replace(...)
        S.replace(old, new[, count]) -> str
        
        Return a copy of S with all occurrences of substring
        old replaced by new.  If the optional argument count is
        given, only the first count occurrences are replaced.
        
    'abcdefg'.replace(old='abc', new='xyz')
    TypeError: replace() takes no keyword arguments


  • Callables
    任何物件可以透過 () 運算子進行調用的都可被稱作"可呼叫的",可分為以下幾類:
    • function
    • method
    • class:在宣告類別時會先透過.__new__建構物件,再透過.__init__初始
    • 有定義.__call__ 的 class instance
      class MyClass:
          def __init__(self):
              print('initializing...')
              self.counter = 0
          
          def __call__(self, x=1):
              self.counter += x
              print(self.counter)
      
      my_obj = MyClass()
      print(callable(my_obj))
      True
      my_obj() # 定義了.__call__ 所以可透過 () 呼叫實例
      1
      my_obj()
      2
    • generator, coroutines, asynchronous


  • Map, Filter, Zip and List Comprehensions
    • Map
      Map(function, *iterables) 會盡可能地將可迭代物件套用函式做處理,並回傳一個 iterator
      l1 = [1, 2, 3, 4, 5]
      l2 = [10, 20, 30]
      f = lambda x, y: x+y
      
      m = map(f, l1, l2)
      list(m)
      [11, 22, 33] # 因 l2 長度較短,因此到第三個元素就停止映射

    • Filter
      Filter(function, iterable) 依函式篩選可迭代物件內的值回傳一個 iterator ,若函式為None,則篩選物件內真值為真的元素
      l = range(6)
      result = filter(lambda x: x % 2 == 0, l)
      print(list(result))
      [0, 2, 4]
      
      result = filter(None, l)
      print(list(result))
      [1, 2, 3, 4, 5]

    • Zip
      Zip(*iterables) 接收多個可迭代物件並依序盡可能地將元素打包成 tuple
      l1 = [1, 2, 3]
      l2 = (10, 20, 30, 40)
      l3 = 'abcde'
      print(list(zip(l1, l2, l3)))
      [(1, 10, 'a'), (2, 20, 'b'), (3, 30, 'c')] # 因 l1 長度較短,因此到第三個元素就停止打包 
      
      print(list(zip(*zip(l1, l2, l3))))
      [(1, 2, 3), (10, 20, 30), ('a', 'b', 'c')] # 透過 * 運算子與 zip 逆向還原
      [1, 2, 3, 4, 5]

    • List Comprehensions
      透過List Comprehensions 可以達到跟 map 及 filter 相同的功能,有時也能讓程式的可讀性上升不少
      l1 = [1, 2, 3, 4, 5]
      l2 = [10, 20, 30, 40, 50]
      
      print(list(map(lambda x, y: x+y, l1, l2)))
      [11, 22, 33, 44, 55]
      
      print([i + j for i,j in zip(l1,l2)])
      [11, 22, 33, 44, 55]
      
      
      print(list(filter(lambda y: y < 25, map(lambda x: x**2, range(10)))))
      [0, 1, 4, 9, 16]
      print([x**2 for x in range(10) if x**2 < 25])
      [0, 1, 4, 9, 16]

  • Reducing Functions
    Reducing 指的是...e 是開發者可透過內建在 functools 模組內的 reduce(function, iterable) 簡化實作過程,常見的 Reducing function 如 min, max, sum, any, all, ...等
    from functools import reduce
    l = [5, 8, 6, 10, 9]
    
    print(reduce(lambda a, b: a if a > b else b, l)) # 等同於 max(l)
    10
    
    n = 5
    print(reduce(lambda a, b: a * b, range(1, n+1))) # 透過 reduce 實作階層函式
    120


  • Partial Functions
    Partial(function, default-value, kw=default-value, ...) 可用來將部份參數固定,簡化每次調用函式須重複輸入冗長的參數
    def my_func(a, b, *args, k1, k2, **kwargs):
        print(a, b, args, k1, k2, kwargs)
        
    def f(b, *args, k2, **kwargs):
        return my_func(10, b, *args, k1='a', k2=k2, **kwargs)
    f(20, 30, 40, k2='b', k3='c')
    10 20 (30, 40) a b {'k3': 'c'}
    
    f = partial(my_func, 10, k1='a') # 等同於上方寫法,但可省略其餘無須變動的部份
    f(20, 30, 40, k2='b', k3='c')
    10 20 (30, 40) a b {'k3': 'c'}
    
    
    def my_func(a, b, c):
        print(a, b, c)
        
    a = 10
    f = partial(my_func, a)
    f(20, 30)
    10 20 30
    
    a = 100
    f(20, 30)
    10 20 30 # 與原有定義函式要注意的事項相同,若定義函式時傳入可變/不可變物件,須注意
    


  • The operator Module
    Python 內建的 Operator 模組有許多可取代常規運算子的函式,例如+, -, *, /,   取值及切片…等行為,搭配高階函式使用可省去自己造輪子的時間,並提高程式可讀性,要注意的是在大部分情況下運算速度的排序為:原生運算子>Operator module> Lambda function,能不用函式傳入就不用

    import operator
    
    dir(operator)
    ['__abs__',
     '__add__',
     '__all__',
     '__and__',
     ...]
     
    from functools import reduce
    print(reduce(lambda x, y: x*y, [1, 2, 3, 4]))
    24
    
    print(reduce(operator.mul, [1, 2, 3, 4]))
    24
    
    
    l = [10+1j, 8+2j, 5+3j]
    print(sorted(l, key=operator.attrgetter('real')))
    [(5+3j), (8+2j), (10+1j)]
    
    print(sorted(l, key=lambda x: x.real))
    [(5+3j), (8+2j), (10+1j)]
    
    
    
    from operator import add
    import operator
    
    def timeit_apply(fn,n=10000000):
        for _ in range(n):
            fn(111,222)
                  
    start = time.perf_counter()
    timeit_apply(add)
    print('cost: ', time.perf_counter()-start)
    cost:  1.0275831999606453
    
    f_lambda = lambda x,y: x+y
    start = time.perf_counter()
    timeit_apply(f_lambda)   
    print('cost: ', time.perf_counter()-start)
    cost:  1.1910063999821432


沒有留言:

張貼留言