1. 程式人生 > 其它 >python實踐中雜項

python實踐中雜項

技術標籤:pythonpython

python實踐中雜項

python的模組化

sys.path可以檢視專案的尋找模組的路徑

pycharm預設會將當前專案的根路徑加入到sys.path中,並且是加入到很前的位置,即程式跑起來位置的,下一個位置。

在這裡插入圖片描述

Python 是指令碼語言,和 C++、Java 最大的不同在於,不需要顯式提供 main() 函式入口。那麼下面的程式碼作用是什麼呢?

 if __name__ == '__main__':
    print('Hello World')

import 在匯入檔案的時候,會自動把所有暴露在外面的程式碼全都執行一遍,為了importst時不讓外面的程式碼執行一遍,可以放到main裡面去。為什麼呢?其實,__name__

作為 Python 的魔術內建引數,本質上是模組物件的一個屬性。我們使用 import 語句時,__name__ 就會被賦值為該模組的名字,自然就不等於 __main__了。只有在跑起來的那個指令碼__name__才是main

導包原則:在大型工程中模組化非常重要,模組的索引要通過絕對路徑來做,而絕對路徑從程式的根目錄開始。即設定根路徑到sys.path中,設定方式可以查一下。pycharm自動做到了這點。

python中的類

__開頭的屬性是私有屬性

self.__context = context # 私有屬性

類函式、成員函式和靜態函式。靜態函式與類沒有什麼關聯,最明顯的特徵便是,靜態函式的第一個引數沒有任何特殊性,靜態函式可以用來做一些簡單獨立的任務,既方便測試,也能優化程式碼結構。靜態函式還可以通過在函式前一行加上 @staticmethod 來表示,程式碼中也有相應的示例。類函式的第一個引數一般為 cls,表示必須傳一個類進來。類函式最常用的功能是實現不同的 init

建構函式,比如上文程式碼中,我們使用 create_empty_book 類函式,來創造新的書籍物件,其 context 一定為 '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

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-xqa86bez-1612074310430)(C:\Users\Jazon\AppData\Roaming\Typora\typora-user-images\1612073008527.png)]