python學習要點(二)
'==' VS 'is'#
'=='操作符比較對象之間的值是否相等。
'is'操作符比較的是對象的身份辨別是否相等,即它們是否是同一個對象,是否指向同一個記憶體位址。
如:
Copy
a = 10
b = 10
a == b
True
id(a)
4427562448
id(b)
a is b
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)
[[1, 2, 3], (30, 40, 50, 60), 100]
在這個例子中,因為淺拷貝裡的元素是對原對象元素的引用,是以 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
l2 = copy.deepcopy(l1)
l2
[[1, 2], (30, 40)]
不過,深度拷貝也不是完美的,往往也會帶來一系列問題。如果被拷貝對象中存在指向自身的引用,那麼程式很容易陷入無限循環:
x = [1]
x.append(x)
x
[1, [...]]
y = copy.deepcopy(x)
y
這裡沒有出現 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)
[1, 2, 3, 4]
這裡 l1 和 l2 先是同時指向值為 [1, 2, 3] 的清單。不過,由于清單可變,執行 append() 函數,對其末尾加入新元素 4 時,變量 l1 和 l2 的值也都随之改變了。
def my_func4(l2):
l2 = l2 + [4]
my_func4(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 wrapper():
print('wrapper of decorator')
func()
return wrapper
@my_decorator
print('hello world')
@my_decorator就相當于前面的greet=my_decorator(greet)語句
通常情況下,我們會把args和kwargs,作為裝飾器内部函數 wrapper() 的參數。args和kwargs,表示接受任意數量和類型的參數,是以裝飾器就可以寫成下面的形式:
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')
輸出:
保留原函數的元資訊#
如下:
greet.__name__
'wrapper'
help(greet)
Help on function wrapper in module __main__:
wrapper(args, *kwargs)
greet() 函數被裝飾以後,它的元資訊變了。元資訊告訴我們“它不再是以前的那個 greet() 函數,而是被 wrapper() 函數取代了”。
是以,可以加上内置的裝飾器@functools.wrap,它會幫助保留原函數的元資訊。
import functools
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
print(message)
'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
num of calls is: 2
裝飾器的嵌套#
@decorator1
@decorator2
@decorator3
def func():
...
等效于:
decorator1(decorator2(decorator3(func)))
例子:
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
print(message)
execute decorator1
execute decorator2
協程#
協程和多線程的差別,主要在于兩點,一是協程為單線程;二是協程由使用者決定,在哪些地方交出控制權,切換到下一個任務。
我們先來看一個例子:
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),表示等待所有任務。
print('crawling {}'.format(url))
sleep_time = int(url.split('_')[-1])
await asyncio.sleep(sleep_time)
print('OK {}'.format(url))
tasks = [asyncio.create_task(crawl_page(url)) for url in urls]
await asyncio.gather(*tasks)
Wall time: 4.01 s
協程中斷和異常處理#
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 代;經過一次垃圾回收後,依然存在的對象,便會依次從上一代挪到下一代。而每一代啟動自動垃圾回收的門檻值,則是可以單獨指定的。當垃圾回收器中新增對象減去删除對象達到相應的門檻值時,就會對這一代對象啟動垃圾回收。
作者: luozhiyun
出處:
https://www.cnblogs.com/luozhiyun/p/12685722.html