python學習要點(二)
我的部落格:https://www.luozhiyun.com/archives/269
'==' VS 'is'
'=='操作符比較物件之間的值是否相等。
'is'操作符比較的是物件的身份標識是否相等,即它們是否是同一個物件,是否指向同一個記憶體地址。
如:
a = 10
b = 10
a == b
True
id(a)
4427562448
id(b)
4427562448
a is b
True
Python 會為 10 這個值開闢一塊記憶體,然後變數 a 和 b 同時指向這塊記憶體區域,即 a 和 b 都是指向 10 這個變數,因此 a 和 b 的值相等,id 也相等。
不過,對於整型數字來說,以上a is b為 True 的結論,只適用於 -5 到 256 範圍內的數字。這裡和java的Integer的快取有點像,java快取-127到128。
當我們比較一個變數與一個單例(singleton)時,通常會使用'is'。一個典型的例子,就是檢查一個變數是否為 None:
if a is None:
...
if a is not None:
...
比較操作符'is'的速度效率,通常要優於'=='。因為'is'操作符不能被過載,而執行a == b相當於是去執行a.eq(b),而 Python 大部分的資料型別都會去過載__eq__這個函式。
淺拷貝和深度拷貝
淺拷貝
淺拷貝,是指重新分配一塊記憶體,建立一個新的物件,裡面的元素是原物件中子物件的引用。因此,如果原物件中的元素不可變,那倒無所謂;但如果元素可變,淺拷貝通常會帶來一些副作用,如下:
l1 = [[1, 2], (30, 40)]
l2 = list(l1)
l1.append(100)
l1[0].append(3)
l1
[[1, 2, 3], (30, 40), 100]
l2
[[1, 2, 3], (30, 40)]
l1[1] += (50, 60)
l1
[[1, 2, 3], (30, 40, 50, 60), 100]
l2
[[1, 2, 3], (30, 40)]
在這個例子中,因為淺拷貝里的元素是對原物件元素的引用,因此 l2 中的元素和 l1 指向同一個列表和元組物件。
l1[0].append(3),這裡表示對 l1 中的第一個列表新增元素 3。因為 l2 是 l1 的淺拷貝,l2 中的第一個元素和 l1 中的第一個元素,共同指向同一個列表,因此 l2 中的第一個列表也會相對應的新增元素 3。
l1[1] += (50, 60),因為元組是不可變的,這裡表示對 l1 中的第二個元組拼接,然後重新建立了一個新元組作為 l1 中的第二個元素,而 l2 中沒有引用新元組,因此 l2 並不受影響。
深度拷貝
所謂深度拷貝,是指重新分配一塊記憶體,建立一個新的物件,並且將原物件中的元素,以遞迴的方式,通過建立新的子物件拷貝到新物件中。因此,新物件和原物件沒有任何關聯。
Python 中以 copy.deepcopy() 來實現物件的深度拷貝。
import copy
l1 = [[1, 2], (30, 40)]
l2 = copy.deepcopy(l1)
l1.append(100)
l1[0].append(3)
l1
[[1, 2, 3], (30, 40), 100]
l2
[[1, 2], (30, 40)]
不過,深度拷貝也不是完美的,往往也會帶來一系列問題。如果被拷貝物件中存在指向自身的引用,那麼程式很容易陷入無限迴圈:
import copy
x = [1]
x.append(x)
x
[1, [...]]
y = copy.deepcopy(x)
y
[1, [...]]
這裡沒有出現 stack overflow 的現象,是因為深度拷貝函式 deepcopy 中會維護一個字典,記錄已經拷貝的物件與其 ID。拷貝過程中,如果字典裡已經儲存了將要拷貝的物件,則會從字典直接返回。
def deepcopy(x, memo=None, _nil=[]):
"""Deep copy operation on arbitrary Python objects.
See the module's __doc__ string for more info.
"""
if memo is None:
memo = {}
d = id(x) # 查詢被拷貝物件 x 的 id
y = memo.get(d, _nil) # 查詢字典裡是否已經儲存了該物件
if y is not _nil:
return y # 如果字典裡已經儲存了將要拷貝的物件,則直接返回
...
Python引數傳遞
Python 中引數的傳遞是賦值傳遞,或者是叫物件的引用傳遞。這裡的賦值或物件的引用傳遞,不是指向一個具體的記憶體地址,而是指向一個具體的物件。
- 如果物件是可變的,當其改變時,所有指向這個物件的變數都會改變。
- 如果物件不可變,簡單的賦值只能改變其中一個變數的值,其餘變數則不受影響。
例如:
def my_func1(b):
b = 2
a = 1
my_func1(a)
a
1
這裡的引數傳遞,使變數 a 和 b 同時指向了 1 這個物件。但當我們執行到 b = 2 時,系統會重新建立一個值為 2 的新物件,並讓 b 指向它;而 a 仍然指向 1 這個物件。所以,a 的值不變,仍然為 1。
def my_func3(l2):
l2.append(4)
l1 = [1, 2, 3]
my_func3(l1)
l1
[1, 2, 3, 4]
這裡 l1 和 l2 先是同時指向值為 [1, 2, 3] 的列表。不過,由於列表可變,執行 append() 函式,對其末尾加入新元素 4 時,變數 l1 和 l2 的值也都隨之改變了。
def my_func4(l2):
l2 = l2 + [4]
l1 = [1, 2, 3]
my_func4(l1)
l1
[1, 2, 3]
這裡 l2 = l2 + [4],表示建立了一個“末尾加入元素 4“的新列表,並讓 l2 指向這個新的物件。這個過程與 l1 無關,因此 l1 的值不變。
裝飾器
首先我們看一個裝飾器的簡單例子:
def my_decorator(func):
def wrapper():
print('wrapper of decorator')
func()
return wrapper
def greet():
print('hello world')
greet = my_decorator(greet)
greet()
# 輸出
wrapper of decorator
hello world
這段程式碼中,變數 greet 指向了內部函式 wrapper(),而內部函式 wrapper() 中又會呼叫原函式 greet(),因此,最後呼叫 greet() 時,就會先列印'wrapper of decorator',然後輸出'hello world'。
my_decorator() 就是一個裝飾器,它把真正需要執行的函式 greet() 包裹在其中,並且改變了它的行為。
在python中,可以使用更優雅的方式:
def my_decorator(func):
def wrapper():
print('wrapper of decorator')
func()
return wrapper
@my_decorator
def greet():
print('hello world')
greet()
@my_decorator就相當於前面的greet=my_decorator(greet)語句
通常情況下,我們會把args和**kwargs,作為裝飾器內部函式 wrapper() 的引數。args和**kwargs,表示接受任意數量和型別的引數,因此裝飾器就可以寫成下面的形式:
def my_decorator(func):
def wrapper(*args, **kwargs):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
這樣可以讓裝飾器接受任意的引數。
自定義引數的裝飾器
比如我想要定義一個引數,來表示裝飾器內部函式被執行的次數
def repeat(num):
def my_decorator(func):
def wrapper(*args, **kwargs):
for i in range(num):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
return my_decorator
@repeat(4)
def greet(message):
print(message)
greet('hello world')
# 輸出:
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world
保留原函式的元資訊
如下:
greet.__name__
## 輸出
'wrapper'
help(greet)
# 輸出
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
greet() 函式被裝飾以後,它的元資訊變了。元資訊告訴我們“它不再是以前的那個 greet() 函式,而是被 wrapper() 函式取代了”。
因此,可以加上內建的裝飾器@functools.wrap,它會幫助保留原函式的元資訊。
如下:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
@my_decorator
def greet(message):
print(message)
greet.__name__
# 輸出
'greet'
類裝飾器
類裝飾器主要依賴於函式__call_(),每當你呼叫一個類的示例時,函式__call__()就會被執行一次。
class Count:
def __init__(self, func):
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs):
self.num_calls += 1
print('num of calls is: {}'.format(self.num_calls))
return self.func(*args, **kwargs)
@Count
def example():
print("hello world")
example()
# 輸出
num of calls is: 1
hello world
example()
# 輸出
num of calls is: 2
hello world
裝飾器的巢狀
如:
@decorator1
@decorator2
@decorator3
def func():
...
等效於:
decorator1(decorator2(decorator3(func)))
例子:
import functools
def my_decorator1(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('execute decorator1')
func(*args, **kwargs)
return wrapper
def my_decorator2(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('execute decorator2')
func(*args, **kwargs)
return wrapper
@my_decorator1
@my_decorator2
def greet(message):
print(message)
greet('hello world')
# 輸出
execute decorator1
execute decorator2
hello world
協程
協程和多執行緒的區別,主要在於兩點,一是協程為單執行緒;二是協程由使用者決定,在哪些地方交出控制權,切換到下一個任務。
我們先來看一個例子:
import asyncio
async def crawl_page(url):
print('crawling {}'.format(url))
sleep_time = int(url.split('_')[-1])
await asyncio.sleep(sleep_time)
print('OK {}'.format(url))
async def main(urls):
tasks = [asyncio.create_task(crawl_page(url)) for url in urls]
for task in tasks:
await task
%time asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))
########## 輸出 ##########
crawling url_1
crawling url_2
crawling url_3
crawling url_4
OK url_1
OK url_2
OK url_3
OK url_4
Wall time: 3.99 s
執行協程有多種方法,這裡我介紹一下常用的三種:
首先,我們可以通過 await 來呼叫。await 執行的效果,和 Python 正常執行是一樣的,也就是說程式會阻塞在這裡,進入被呼叫的協程函式,執行完畢返回後再繼續,而這也是 await 的字面意思。
其次,我們可以通過 asyncio.create_task() 來建立任務。要等所有任務都結束才行,用for task in tasks: await task 即可。
最後,我們需要 asyncio.run 來觸發執行。asyncio.run 這個函式是 Python 3.7 之後才有的特性。一個非常好的程式設計規範是,asyncio.run(main()) 作為主程式的入口函式,在程式執行週期內,只調用一次 asyncio.run。
在上面的例子中,也可以使用await asyncio.gather(*tasks),表示等待所有任務。
import asyncio
async def crawl_page(url):
print('crawling {}'.format(url))
sleep_time = int(url.split('_')[-1])
await asyncio.sleep(sleep_time)
print('OK {}'.format(url))
async def main(urls):
tasks = [asyncio.create_task(crawl_page(url)) for url in urls]
await asyncio.gather(*tasks)
%time asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))
########## 輸出 ##########
crawling url_1
crawling url_2
crawling url_3
crawling url_4
OK url_1
OK url_2
OK url_3
OK url_4
Wall time: 4.01 s
協程中斷和異常處理
import asyncio
async def worker_1():
await asyncio.sleep(1)
return 1
async def worker_2():
await asyncio.sleep(2)
return 2 / 0
async def worker_3():
await asyncio.sleep(3)
return 3
async def main():
task_1 = asyncio.create_task(worker_1())
task_2 = asyncio.create_task(worker_2())
task_3 = asyncio.create_task(worker_3())
await asyncio.sleep(2)
task_3.cancel()
res = await asyncio.gather(task_1, task_2, task_3, return_exceptions=True)
print(res)
%time asyncio.run(main())
########## 輸出 ##########
[1, ZeroDivisionError('division by zero'), CancelledError()]
Wall time: 2 s
這個例子中,使用了task_3.cancel()來中斷程式碼,使用了return_exceptions=True來控制輸出異常,如果不設定的話,錯誤就會完整地 throw 到我們這個執行層,從而需要 try except 來捕捉,這也就意味著其他還沒被執行的任務會被全部取消掉。
Python 中的垃圾回收機制
python採用的是引用計數機制為主,標記-清除和分代收集(隔代回收)兩種機制為輔的策略。
引用計數法
引用計數法機制的原理是:每個物件維護一個ob_ref欄位,用來記錄該物件當前被引用的次數,每當新的引用指向該物件時,它的引用計數ob_ref加1,每當該物件的引用失效時計數ob_ref減1,一旦物件的引用計數為0,該物件立即被回收,物件佔用的記憶體空間將被釋放。
它的缺點是它不能解決物件的“迴圈引用”。
標記清除演算法
對於一個有向圖,如果從一個節點出發進行遍歷,並標記其經過的所有節點;那麼,在遍歷結束後,所有沒有被標記的節點,我們就稱之為不可達節點。顯而易見,這些節點的存在是沒有任何意義的,自然的,我們就需要對它們進行垃圾回收。
在 Python 的垃圾回收實現中,mark-sweep 使用雙向連結串列維護了一個數據結構,並且只考慮容器類的物件(只有容器類物件才有可能產生迴圈引用)。
分代收集演算法
Python 將所有物件分為三代。剛剛創立的物件是第 0 代;經過一次垃圾回收後,依然存在的物件,便會依次從上一代挪到下一代。而每一代啟動自動垃圾回收的閾值,則是可以單獨指定的。當垃圾回收器中新增物件減去刪除物件達到相應的閾值時,就會對這一代物件啟動垃圾回收