天天看點

python學習要點(二)輸出輸出:輸出輸出輸出輸出輸出

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