Python除錯終極指南 - martinheinz

banq發表於2020-06-02

即使您編寫清晰易讀的程式碼,即使使用測試覆蓋了程式碼,即使您是非常有經驗的開發人員,也不可避免地會出現奇怪的錯誤,並且您將需要以某種方式進行除錯。許多人只使用一堆print語句來檢視程式碼中正在發生的事情。這種方法遠非理想,還有很多更好的方法來找出您的程式碼出了什麼問題,我們將在本文中探討其中的一些方法。

記錄是必須的
如果您編寫的應用程式沒有某種日誌設定,您最終會後悔的。您的應用程式中沒有任何日誌可能會很難對所有錯誤進行故障排除。幸運的是-在Python中-設定基本記錄器非常簡單:

import logging
logging.basicConfig(
    filename='application.log',
    level=logging.WARNING,
    format= '[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s',
    datefmt='%H:%M:%S'
)

logging.error("Some serious error occurred.")
logging.warning('Function you are using is deprecated.')


這就是開始將日誌寫入檔案的所有操作,該日誌看起來像這樣(您可以使用來找到檔案的路徑logging.getLoggerClass().root.handlers[0].baseFilename):

[12:52:35] {<stdin>:1} ERROR - Some serious error occurred.
<p class="indent">[12:52:35] {<stdin>:1} WARNING - Function you are using is deprecated.


這種設定看起來似乎已經足夠好了(通常是這樣),但是配置合理,格式清晰,可讀性強的日誌可以使您的生活更加輕鬆。改善和擴大配置的一種方法是使用.ini或.yaml檔案被由記錄器讀取。作為您可以在配置中執行的操作的示例:

version: 1
disable_existing_loggers: true

formatters:
  standard:
    format: "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s"
    datefmt: '%H:%M:%S'

handlers:
  console:  # handler which will log into stdout
    class: logging.StreamHandler
    level: DEBUG
    formatter: standard  # Use formatter defined above
    stream: ext://sys.stdout
  file:  # handler which will log into file
    class: logging.handlers.RotatingFileHandler
    level: WARNING
    formatter: standard  # Use formatter defined above
    filename: /tmp/warnings.log
    maxBytes: 10485760 # 10MB
    backupCount: 10
    encoding: utf8

root:  # Loggers are organized in hierarchy - this is the root logger config
  level: ERROR
  handlers: [console, file]  # Attaches both handler defined above

loggers:  # Defines descendants of root logger
  mymodule:  # Logger for "mymodule"
    level: INFO
    handlers: [file]  # Will only use "file" handler defined above
    propagate: no  # Will not propagate logs to "root" logger


在您的python程式碼中擁有這種廣泛的配置將很難導航,編輯和維護。將內容儲存在YAML檔案中,可以使用上述非常特定的設定輕鬆設定和調整多個記錄器。
如果您想知道所有這些配置欄位的來源,請在此處進行記錄,它們中的大多數只是關鍵字引數,如第一個示例所示。
因此,現在在檔案中包含配置,意味著我們需要以某種方式載入。使用YAML檔案的最簡單方法是:

import yaml
from logging import config

with open("config.yaml", 'rt') as f:
    config_data = yaml.safe_load(f.read())
    config.dictConfig(config_data)


Python記錄器實際上並不直接支援YAML檔案,但它支援字典配置,可以使用yaml.safe_load輕鬆從YAML中建立。如果您傾向於使用舊.ini檔案,那麼我只想指出,作為一種docs,對於新應用程式,建議使用字典配置。有關更多示例,請檢視日誌記錄手冊

日誌記錄裝飾器
使用日誌記錄修飾器代替修改函式的主體,該修飾符將記錄具有特定日誌級別和可選訊息的每個函式呼叫。讓我們看一下裝飾器:

from functools import wraps, partial
import logging

def attach_wrapper(obj, func=None):  # Helper function that attaches function as attribute of an object
    if func is None:
        return partial(attach_wrapper, obj)
    setattr(obj, func.__name__, func)
    return func

def log(level, message):  # Actual decorator
    def decorate(func):
        logger = logging.getLogger(func.__module__)  # Setup logger
        formatter = logging.Formatter(
            '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
        handler = logging.StreamHandler()
        handler.setFormatter(formatter)
        logger.addHandler(handler)
        log_message = f"{func.__name__} - {message}"

        @wraps(func)
        def wrapper(*args, **kwargs):  # Logs the message and before executing the decorated function
            logger.log(level, log_message)
            return func(*args, **kwargs)

        @attach_wrapper(wrapper)  # Attaches "set_level" to "wrapper" as attribute
        def set_level(new_level):  # Function that allows us to set log level
            nonlocal level
            level = new_level

        @attach_wrapper(wrapper)  # Attaches "set_message" to "wrapper" as attribute
        def set_message(new_message):  # Function that allows us to set message
            nonlocal log_message
            log_message = f"{func.__name__} - {new_message}"

        return wrapper
    return decorate

# Example Usage
@log(logging.WARN, "example-param")
def somefunc(args):
    return args

somefunc("some args")

somefunc.set_level(logging.CRITICAL)  # Change log level by accessing internal decorator function
somefunc.set_message("new-message")  # Change log message by accessing internal decorator function
somefunc("some args")


有點複雜,這裡的想法是log函式接受引數並將其提供給內部wrapper函式使用。然後,透過新增附加到裝飾器的訪問器函式使這些引數可調整。
至於functools.wraps裝飾器:如果我們在這裡不使用它,函式(func.__name__)的名稱將被裝飾器的名稱覆蓋。但這是一個問題,因為我們要列印名稱。透過functools.wraps將函式名稱,文件字串和引數列表複製到裝飾器函式上,可以解決此問題。
無論如何,這是上面程式碼的輸出。很整潔吧?

2020-05-01 14:42:10,289 - __main__ - WARNING - somefunc - example-param
2020-05-01 14:42:10,289 - __main__ - CRITICAL - somefunc - new-message


__repr__ 有關更多可讀日誌
對您的程式碼的輕鬆改進使其更易於除錯,這是__repr__在類中新增方法。如果您不熟悉此方法,它所做的就是返回類例項的字串表示形式。__repr__方法的最佳實踐是輸出可用於重新建立例項的文字。例如:

class Circle:
    def __init__(self, x, y, radius):
        self.x = x
        self.y = y
        self.radius = radius

    def __repr__(self):
        return f"Rectangle({self.x}, {self.y}, {self.radius})"

...
c = Circle(100, 80, 30)
repr(c)
# Circle(100, 80, 30)


如果不希望或不可能像上面那樣顯示物件,那麼很好的選擇是使用<...>,例如使用<_io.TextIOWrapper name='somefile.txt' mode='w' encoding='UTF-8'>。
除了__repr__之外,最好還是實現__str__,這是在print(instance)呼叫時預設使用的方法。使用這兩種方法,您只需列印變數即可獲得很多資訊。

__missing__ 字典的Dunder方法
如果出於某種原因需要實現自定義詞典類,那麼當您嘗試訪問實際上不存在的鍵時,可能會出現來自KeyErrors 一些錯誤。為了避免在程式碼中四處查詢並檢視缺少哪個,可以實現特殊的__missing__方法,該方法在每次KeyError引發時都會呼叫。

class MyDict(dict):
    def __missing__(self, key):
        message = f'{key} not present in the dictionary!'
        logging.warning(message)
        return message  # Or raise some error instead


上面的實現非常簡單,只返回並記錄缺少鍵的訊息,但是您也可以記錄其他有價值的資訊,以便為您提供更多有關程式碼錯誤的上下文。

除錯崩潰的應用程式
如果您的應用程式崩潰後才有機會檢視其中發生的情況,那麼您可能會發現此技巧非常有用。
使用-i引數(python3 -i app.py)執行該應用程式會使該程式在退出時立即啟動互動式Shell。此時,您可以檢查變數和函式。
如果這還不夠好,可以使用更大的錘子- pdb- Python除錯。pdb具有相當多的功能,這些功能可以保證自己撰寫一篇文章。但這是示例,也是最重要的部分的摘要。首先讓我們看一下崩潰的指令碼:

# crashing_app.py
SOME_VAR = 42

class SomeError(Exception):
    pass

def func():
    raise SomeError("Something went wrong...")

func()


現在,如果使用-i引數執行它,我們將有機會對其進行除錯:

# Run crashing application
~ $ python3 -i crashing_app.py
Traceback (most recent call last):
  File "crashing_app.py", line 9, in <module>
    func()
  File "crashing_app.py", line 7, in func
    raise SomeError("Something went wrong...")
__main__.SomeError: Something went wrong...
>>> # We are interactive shell
>>> import pdb
>>> pdb.pm()  # start Post-Mortem debugger
> .../crashing_app.py(7)func()
-> raise SomeError("Something went wrong...")
(Pdb) # Now we are in debugger and can poke around and run some commands:
(Pdb) p SOME_VAR  # Print value of variable
42
(Pdb) l  # List surrounding code we are working with
  2
  3   class SomeError(Exception):
  4       pass
  5
  6   def func():
  7  ->     raise SomeError("Something went wrong...")
  8
  9   func()
<p class="indent">[EOF]
(Pdb)  # Continue debugging... set breakpoints, step through the code, etc.


上面的除錯會話非常簡要地顯示了您可以使用的功能pdb。程式終止後,我們進入互動式除錯會話。首先,我們匯入pdb並啟動偵錯程式。在這一點上,我們可以使用所有pdb命令。在上面的示例中,我們使用p命令列印變數,並使用l命令列出程式碼。大多數情況下,您可能希望設定可以使用b LINE_NO的斷點並執行程式,直到命中斷點(c),然後繼續使用逐步執行該功能s,還可以選擇使用來列印stacktrace w。有關命令的完整列表,請轉到pdbdocs

檢查堆疊跟蹤
假設您的程式碼是例如在遠端伺服器上執行的Flask或Django應用程式,您無法獲得互動式除錯會話。在這種情況下,您可以使用traceback和sys軟體包來更深入地瞭解程式碼失敗的原因:

import traceback
import sys

def func():
    try:
        raise SomeError("Something went wrong...")
    except:
        traceback.print_exc(file=sys.stderr)


執行後,上面的程式碼將列印最後引發的異常。除了列印例外,您還可以使用traceback包來列印stacktrace(traceback.print_stack())或提取原始堆疊幀,對其進行格式化並進一步檢查(traceback.format_list(traceback.extract_stack()))。

在除錯過程中重新載入模組
有時您可能正在除錯或嘗試使用互動式Shell中的某些功能並對其進行頻繁更改。為了簡化執行/測試和修改的週期,可以執行importlib.reload(module)以避免每次更改後都必須重新啟動互動式會話:

>>> import func from module
>>> func()
"This is result..."

# Make some changes to "func"
>>> func()
"This is result..."  # Outdated result
>>> from importlib import reload; reload(module)  # Reload "module" after changes made to "func"
>>> func()
"New result..."


本技巧更多地是關於效率而不是除錯。能夠跳過一些不必要的步驟,並使您的工作流程更快,更高效,總是很高興的。通常,不時重新載入模組是個好主意,因為它可以幫助您避免嘗試除錯同時修改過很多次的程式碼。

相關文章