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
- 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
- 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
當我們在程式碼中宣告變數與值時,實際上是在記憶體位址中儲存了值,並將變數指向到該記憶體位址,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
儘管 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
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
若一個物件的內部狀態時可被改變,則此物件被稱作 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])
當可變物件被當作參數傳遞至參數時,有可能會導致意外的副作用,以下舉例
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
在 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
在 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
包含函式,所有東西在Python中都可被視為一個物件,因此函式也可以被賦予給變數、傳遞給函式或是被函式回傳
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 對於符合命名規則的字串(以字母及下劃線開頭的字串,並只包含字母、數字及下劃線)進行留用,以避免過多的逐字比對耗用大量資源,開發者也可以手動使用 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
在編譯之前的階段,解釋器會對程式碼中的部分先行編譯,以避免在接下來的階段重複進行相同的動作
- 常量表達式
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))
當需要進行巨量的序列成員比對時,盡量使用 Set 取代 List 及 Tuple,可得到驚人的效能改善def my_func(): if e in {1, 2, 3}: pass my_func.__code__.co_consts (None, 1, 2, 3, frozenset({1, 2, 3}))
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
布林運算的優先層級如下,但為了可讀性,能加上括號的話還是盡量加
- ()
- <, >, <=, >=, ==, !=, in, is
- not
- and
- 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
- 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
有理數可以表達為兩個整數構成的分數,在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
- 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 # 可以看到內建的函式非四捨五入
為避免浮點數的計算誤差,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 # 僅僅是創建時間就與浮點數相差了約十倍
複數較少用到,其數學運算有專門的 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
布林是整數的子類別,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
布林運算的優先層級如下,但為了可讀性,能加上括號的話還是盡量加
- ()
- <, >, <=, >=, ==, !=, in, is
- not
- and
- 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)']
定義函式時,所傳入的變量叫做Parameter,但在實際調用函式時,傳入的變量此時稱作Argument
調用函式時會依據我們定義的順序將變量傳入,若有預設值需要設定,也可在定義時先行給予預設值,但要注意的是不論是在定義或是調用函數,預設值及指定變量給予都必須排在未指定的變量後面,否則解釋器會無法判斷要將記憶體位址引用給哪一個變量
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 可迭代對象時,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
在 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} # 常見的應用是當程式需要將用戶自定義設定覆蓋預設值時,透過這類方式來合併兩個設定檔的字典
在定義函式參數時,常看到以下型式-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}
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
在定義函式參數的初始值時,有兩點需要注意,第一點是初始值的宣告在函式定義時完成,而非調用函式當下,第二點則是使用可變物件做初始值可能造成的副作用
# 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
- 物件可以被當作參數傳遞
- 物件可以被函式回傳
- 物件可以被賦予給變量
- 物件可以被資料結構(List, Tuple, ...)儲存
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'
在 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 表達式是另一種宣告函式的方法,因為不需要提供函式名稱,又被稱作匿名函式,與一般用 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]
除了使用 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
任何物件可以透過 () 運算子進行調用的都可被稱作"可呼叫的",可分為以下幾類:
- 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
Map(function, *iterables) 會盡可能地將可迭代物件套用函式做處理,並回傳一個 iteratorl1 = [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) 接收多個可迭代物件並依序盡可能地將元素打包成 tuplel1 = [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 指的是...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(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 # 與原有定義函式要注意的事項相同,若定義函式時傳入可變/不可變物件,須注意
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
沒有留言:
張貼留言