python實踐中雜項
python實踐中雜項
python的模組化
sys.path可以檢視專案的尋找模組的路徑
pycharm預設會將當前專案的根路徑加入到sys.path中,並且是加入到很前的位置,即程式跑起來位置的,下一個位置。
Python 是指令碼語言,和 C++、Java 最大的不同在於,不需要顯式提供 main() 函式入口。那麼下面的程式碼作用是什麼呢?
if __name__ == '__main__':
print('Hello World')
import 在匯入檔案的時候,會自動把所有暴露在外面的程式碼全都執行一遍,為了importst時不讓外面的程式碼執行一遍,可以放到main裡面去。為什麼呢?其實,__name__
__name__
就會被賦值為該模組的名字,自然就不等於 __main__
了。只有在跑起來的那個指令碼__name__
才是main
導包原則:在大型工程中模組化非常重要,模組的索引要通過絕對路徑來做,而絕對路徑從程式的根目錄開始。即設定根路徑到sys.path中,設定方式可以查一下。pycharm自動做到了這點。
python中的類
__開頭的屬性是私有屬性
self.__context = context # 私有屬性
類函式、成員函式和靜態函式。靜態函式與類沒有什麼關聯,最明顯的特徵便是,靜態函式的第一個引數沒有任何特殊性,靜態函式可以用來做一些簡單獨立的任務,既方便測試,也能優化程式碼結構。靜態函式還可以通過在函式前一行加上 @staticmethod 來表示,程式碼中也有相應的示例。類函式的第一個引數一般為 cls,表示必須傳一個類進來。類函式最常用的功能是實現不同的 init
'nothing'
。這樣的程式碼,就比你直接構造要清晰一些。類似的,類函式需要裝飾器 @classmethod 來宣告。
成員函式則是我們最正常的類的函式,它不需要任何裝飾器宣告,第一個引數 self 代表當前物件的引用,可以通過此函式,來實現想要的查詢 / 修改類的屬性等功能。
class Document(): WELCOME_STR = 'Welcome! The context for this book is {}.' def __init__(self, title, author, context): print('init function called') self.title = title self.author = author self.__context = context # 類函式 @classmethod def create_empty_book(cls, title, author): return cls(title=title, author=author, context='nothing') # 成員函式 def get_context_length(self): return len(self.__context) # 靜態函式 @staticmethod def get_welcome(context): return Document.WELCOME_STR.format(context)
類的繼承
class Sub(Parent):
def __init__(self, sub_name):
self.sub_name = sub_name
# 呼叫父類的構造函數了
Parent.__init__(self, 'Parent')
def print_sub_parent(self):
print('-'.join((self.sub_name, self.name)))
class Parent(object):
def __init__(self, name):
self.name = name
python物件的比較
==
是值比較,執行a == b
相當於是去執行a.__eq__(b)
,而 Python 大部分的資料型別都會去過載__eq__
這個函式,其內部的處理通常會複雜一些。比如,對於列表,__eq__
函式會去遍歷列表中的元素,比較它們的順序和值是否相等。is
是比較的是物件的身份標識是否相等,在python中,物件識別符號能通過id(object)獲取。
出於對效能優化的考慮,Python 內部會對 -5 到 256 的整型維持一個數組,起到一個快取的作用。這樣,每次你試圖建立一個 -5 到 256 範圍內的整型數字時,Python 都會從這個陣列中返回相對應的引用,而不是重新開闢一塊新的記憶體空間。但是,如果整型數字超過了這個範圍,比如上述例子中的 257,Python 則會為兩個 257 開闢兩塊記憶體區域,因此 a 和 b 的 ID 不一樣,a is b
就會返回 False 了。
對於不可變(immutable)的變數,如果我們之前用'=='
或者'is'
比較過,結果是不是就一直不變了呢?不是的,因為不可變物件可以巢狀可變物件,比如元組巢狀列表。
python的淺拷貝和深拷貝
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)]
python中的值傳遞和引用傳遞
準確地說,Python 的引數傳遞是賦值傳遞 (pass by assignment),或者叫作物件的引用傳遞(pass by object reference)。Python 裡所有的資料型別都是物件,所以引數傳遞時,只是讓新變數與原變數指向相同的物件而已,並不存在值傳遞或是引用傳遞一說。個人認為,這是殊途同歸。不可變物件對應值傳遞。可以和java傳遞值,傳遞String引用做對比。
python中的裝飾器
在python中,函式也是物件,我們可以把函式賦予變數,這樣就可以用變數呼叫函式。
我們可以把函式當作引數,傳入另一個函式中。
我們可以在函式裡定義函式。
函式的返回值也可以是函式(閉包)
一個簡單的裝飾器例子
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
裝飾器更優雅的表達方式
def my_decorator(func):
def wrapper():
print('wrapper of decorator')
func()
return wrapper
@my_decorator
def greet():
print('hello world')
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
裝飾器後原函式還是原函式嗎
不是的,他的__name
會變成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)
類裝飾器
類裝飾器主要依賴於函式__call_()
,每當你呼叫一個類的示例時,函式__call__()
就會被執行。
__call__
的用法:
stu = Stu()
# 呼叫了Stu類的__call__方法
stu()
```python
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")
裝飾器的巢狀
@decorator1
@decorator2
@decorator3
def func():
...
# 這個情況是允許的
# 等價於
decorator1(decorator2(decorator3(func)))
元類
所有的Python的使用者定義類,都是type這個類的例項,使用者自定義類,只不過是type類的__call__
運算子過載,metaclass是type的子類,通過替換type的__call__
運算子過載機制,"超越變形"正常的類。
myclass =MyClass()
# 這邊這行程式碼其實呼叫的是type('MyClass', (), {'data': 1})
# 而之前我們說過物件引號直接加括號是呼叫__call__
# 所以他就是呼叫的type的__call__
# type的__call__做的事情包括以下
type.__new__(typeclass, classname, superclasses, attributedict)
type.__init__(class, classname, superclasses, attributedict)
# 使用元類之後發生了一些變化
class = type(classname, superclasses, attributedict)
# 變為了
class = MyMeta(classname, superclasses, attributedict)
# 而MyMeta的init進行了超越變形,他會給這個類新增一些功能,修改了行為
# 下面就是利用元類,在每次例項化類的時候都會呼叫add_constructor
class YAMLObjectMetaclass(type):
def __init__(cls, name, bases, kwds):
super(YAMLObjectMetaclass, cls).__init__(name, bases, kwds)
if 'yaml_tag' in kwds and kwds['yaml_tag'] is not None:
cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml)
# 省略其餘定義
python的迭代器和生成器
判斷一個物件是否可迭代
def is_iterable(param):
try:
iter(param)
return True
except TypeError:
return False
生成器即只有在被使用的時候才會去生成物件,所以他不會像迭代器那樣佔用大量記憶體。
# ()是生成器, []是直接生成陣列
list_2 = (i for i in range(100000000))
使用迭代器返回與指定元素相等的下標
def index_generator(L, target):
for i, num in enumerate(L):
if num == target:
yield i
print(list(index_generator([1, 6, 2, 4, 5, 2, 8, 6, 3, 2], 2)))
這個是個生成器,你可以理解為,函式執行到yield這一行的時候,程式會從這裡暫停,然後跳出,不過跳到哪裡呢?答案是 next() 函式。那麼 i ** k
是幹什麼的呢?它其實成了 next() 函式的返回值。當下次再執行到這裡時,暫停的程式就會又復活了,從yield這裡向下繼續執行,同時注意變數i並沒有被清除掉,而是會繼續累加。一個Generator物件,需要使用list轉換為列表後,才能用print輸出。
生成器的技巧:
b = (i for i in range(5))
print(2 in b)
print(4 in b)
print(3 in b)
########## 輸出 ##########
True
True
False
上面的(2 in b)等價於
while True:
val = next(b)
if val == 2:
yield True
所以會過了4之後,想再回3是不行的。
python的協程
python的全域性直譯器鎖
python的直譯器並不是執行緒安全的,所以引入了全域性直譯器鎖,也就是同一個時刻,只允許一個執行緒執行。當然在執行I/O操作時,如果一個執行緒被block了,全域性直譯器鎖就會被釋放,從而讓另一個執行緒能夠繼續執行。
Asyncio工作原理
Asyncio和其他Python程式一樣,單執行緒的,它只有一個主執行緒,但是可以進行多個不同任務,這裡的任務,就是特殊的future物件,被一個叫做eventloop的物件所控制,這個任務只有兩個狀態: 一是預備狀態,二是等待狀態。eventloop會維護兩個任務列表,分別對應這兩種狀態,並選取預備狀態的一個任務,使其執行,一直到這個任務把控制權交還給eventloop為止。當任務把控制權交還給 event loop 時,event loop 會根據其是否完成,把任務放到預備或等待狀態的列表,然後遍歷等待狀態列表的任務,檢視他們是否完成。
Asyncio的用法
import asyncio
import aiohttp
import time
async def download_one(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
print('Read {} from {}'.format(resp.content_length, url))
async def download_all(sites):
tasks = [asyncio.create_task(download_one(site)) for site in sites]
await asyncio.gather(*tasks)
def main():
sites = [
'https://en.wikipedia.org/wiki/Portal:Arts',
'https://en.wikipedia.org/wiki/Portal:History',
'https://en.wikipedia.org/wiki/Portal:Society',
'https://en.wikipedia.org/wiki/Portal:Biography',
'https://en.wikipedia.org/wiki/Portal:Mathematics',
'https://en.wikipedia.org/wiki/Portal:Technology',
'https://en.wikipedia.org/wiki/Portal:Geography',
'https://en.wikipedia.org/wiki/Portal:Science',
'https://en.wikipedia.org/wiki/Computer_science',
'https://en.wikipedia.org/wiki/Python_(programming_language)',
'https://en.wikipedia.org/wiki/Java_(programming_language)',
'https://en.wikipedia.org/wiki/PHP',
'https://en.wikipedia.org/wiki/Node.js',
'https://en.wikipedia.org/wiki/The_C_Programming_Language',
'https://en.wikipedia.org/wiki/Go_(programming_language)'
]
start_time = time.perf_counter()
asyncio.run(download_all(sites))
end_time = time.perf_counter()
print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))
if __name__ == '__main__':
main()
Asyncio
要想用好Asyncio,很多情況下必須得有相應的Python庫支援,比如請求http時,必須要用支援協程的http庫,比如aiohttp庫,它相容Asyncio。
多執行緒還是Asyncio
if io_bound:
# io密集型
if io_slow:
# 每個io操作很慢
print('Use Asyncio')
else:
# io操作很快
print('Use multi-threading')
else if cpu_bound:
# cpu密集的
print('Use multi-processing')
python的GIL
CPython直譯器使用引用計數作記憶體管理,所有Python指令碼中建立的例項,都會有一個引用技術,來記錄有多少個指標,當引用計數只有0時,則會自動釋放。如果有兩個Python執行緒同時引用了a,就會造成引用計數的race condition,引用計數可能最終只增加1,這樣就會造成記憶體被汙染。
所以說,CPython 引進 GIL 其實主要就是這麼兩個原因:
- 一是設計者為了規避類似於記憶體管理這樣的複雜的競爭風險問題(race condition);
- 二是因為 CPython 大量使用 C 語言庫,但大部分 C 語言庫都不是原生執行緒安全的(執行緒安全會降低效能和增加複雜度)。
為什麼Python執行緒會主動釋放GIL呢?check_interval,CPython直譯器會去輪詢檢查執行緒GIL的鎖住情況,每隔一段時間,Python直譯器就會強制當前執行緒去釋放GIL,這樣別的執行緒才有機會執行。
python的垃圾記憶體回收機制
檢視python程序的記憶體
def show_memory_info(hint):
pid = os.getpid()
p = psutil.Process(pid)
info = p.memory_full_info()
memory = info.uss / 1024. / 1024
print('{} memory used: {} MB'.format(hint, memory))
檢視python物件的內部引用計數
import sys
a = []
# 兩次引用,一次來自 a,一次來自 getrefcount
print(sys.getrefcount(a))
def func(a):
# 四次引用,a,python 的函式呼叫棧,函式引數,和 getrefcount
print(sys.getrefcount(a))
func(a)
# 兩次引用,一次來自 a,一次來自 getrefcount,函式 func 呼叫已經不存在
print(sys.getrefcount(a))
########## 輸出 ##########
2
4
2
Python使用標記清除演算法和分代收集,來啟用針對迴圈引用的自動垃圾回收。標記清除可以類似於Java的GC root,分代收集也是類似於Java的分代。python的垃圾收集是以引用計數+不可達+分代實現的。
除錯記憶體洩漏
推薦使用objgraph庫,可以分析引用
import objgraph
a = [1, 2, 3]
b = [4, 5, 6]
a.append(b)
b.append(a)
objgraph.show_refs([a])
寫出對機器和閱讀者友好的python程式碼
-
對字典的遍歷不要使用keys,因為keys會生成一個臨時列表,導致多餘的記憶體浪費並且執行緩慢,使用iterator。
-
is和==的正確使用
-
不要使用import一次匯入多個模組
合理地運用assert
assert 1==2, 'This should fail'
這個語句等價於
if __debug__:
if not expression1: raise AssertionError(expression2)
這裡的__debug__
是一個常數。如果 Python 程式執行時附帶了-O
這個選項,比如Python test.py -O
,那麼程式中所有的 assert 語句都會失效,常數__debug__
便為 False;反之__debug__
則為 True。
巧用上下文管理器和With語句精簡程式碼
在python中,使用上下文管理器幫助程式設計師自動分配並且釋放資源,其中最典型的就是with語句。
with open('test.txt', 'w') as f:
f.write('hello')
some_lock = threading.Lock()
with somelock:
...
自定義上下文管理器
基於類的上下文管理器
class FileManager:
def __init__(self, name, mode):
print('calling __init__ method')
self.name = name
self.mode = mode
self.file = None
def __enter__(self):
print('calling __enter__ method')
self.file = open(self.name, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
print('calling __exit__ method')
if self.file:
self.file.close()
with FileManager('test.txt', 'w') as f:
print('ready to write to file')
f.write('hello world')
## 輸出
calling __init__ method
calling __enter__ method
ready to write to file
calling __exit__ method
如果在with內丟擲了異常,exit可以接收到,如果你在exit內處理了異常,記得返回True,否則異常仍會繼續丟擲。
基於生成器的上下文管理器
from contextlib import contextmanager
@contextmanager
def file_manager(name, mode):
try:
f = open(name, mode)
yield f
finally:
f.close()
with file_manager('test.txt', 'w') as f:
f.write('hello world')
python的除錯工具及效能分析工具
pdb使用例子
a = 1
b = 2
import pdb
pdb.set_trace()
c = 3
print(a + b + c)
cprofile使用例子
python3 -m cProfile xxx.py