發(fā)布于:2021-01-21 09:09:10
0
273
0
有很多方法可以編寫糟糕的代碼。但在python中,有一個(gè)特別的國(guó)王。
我們筋疲力盡,但仍興高采烈。另外兩個(gè)工程師分別花了三天時(shí)間試圖修復(fù)一個(gè)神秘的Unicode錯(cuò)誤,但都沒(méi)有成功,我只花了一天時(shí)間就找到了原因。十分鐘后,我們找到了一個(gè)候選人。
悲劇在于,我們本可以跳過(guò)七天,直接跳到十分鐘。但我已經(jīng)超越了我自己。
這是關(guān)鍵。以下代碼是Python開發(fā)人員可以編寫的最具自我破壞性的代碼之一:
try:
do_something()
except:
pass
例如,有些變體與except Exception:
或except Exception as e:
相當(dāng)。它們都會(huì)造成同樣的巨大危害:悄悄地、不可見地隱藏錯(cuò)誤條件,否則可以快速檢測(cè)和調(diào)度。
為什么我要宣稱這是當(dāng)今Python世界中最邪惡的反模式?
人們這樣做是因?yàn)樗麄兤谕抢飼?huì)發(fā)生特定的錯(cuò)誤。然而,捕捉Exception
隱藏了所有的錯(cuò)誤…甚至是那些完全出乎意料的錯(cuò)誤。
當(dāng)錯(cuò)誤最終被發(fā)現(xiàn)時(shí),因?yàn)樗?jīng)常出現(xiàn)在生產(chǎn)環(huán)境中,所以您可能幾乎不知道或根本不知道它在代碼庫(kù)中的什么地方出錯(cuò)了。即使在try塊中發(fā)生錯(cuò)誤,也要花上相當(dāng)令人沮喪的時(shí)間才能弄清楚。
一旦您意識(shí)到錯(cuò)誤正在那里發(fā)生,由于缺少關(guān)鍵信息,您的故障排除會(huì)受到極大的阻礙。什么是錯(cuò)誤/異常類?涉及到什么調(diào)用或數(shù)據(jù)結(jié)構(gòu)?錯(cuò)誤源于哪一行代碼和哪一個(gè)文件?
您將丟掉堆棧跟蹤信息-這實(shí)際上是無(wú)價(jià)的信息,可能會(huì)在數(shù)天或數(shù)分鐘內(nèi)對(duì)錯(cuò)誤進(jìn)行故障排除。是的,幾分鐘。稍后對(duì)此進(jìn)行更多討論。
最糟糕的是,這很容易最終損害在代碼庫(kù)上工作的工程師的士氣,幸福甚至自尊。當(dāng)錯(cuò)誤浮出水面時(shí),疑難解答人員可能會(huì)花費(fèi)數(shù)小時(shí)來(lái)單獨(dú)了解根本原因。他們認(rèn)為自己是一個(gè)糟糕的程序員,因?yàn)橐ê荛L(zhǎng)時(shí)間才能弄清楚。他們不是; 靜默捕獲Exception所產(chǎn)生的錯(cuò)誤本質(zhì)上難以識(shí)別,解決和修復(fù)。
在我用Python編寫應(yīng)用程序的近十年經(jīng)驗(yàn)中,無(wú)論是單獨(dú)編寫還是作為團(tuán)隊(duì)的一部分編寫,這種模式都是開發(fā)人員生產(chǎn)力和應(yīng)用程序可靠性的最大消耗,尤其是長(zhǎng)期以來(lái)。如果你認(rèn)為你有更糟的候選人,我很樂(lè)意聽你說(shuō)。
為什么我們要這樣對(duì)自己?
當(dāng)然,沒(méi)有人會(huì)故意編寫代碼,讓您的開發(fā)伙伴感到壓力,破壞應(yīng)用程序的可靠性。我們這樣做是因?yàn)閠ry塊中的代碼在正常操作中有時(shí)會(huì)以某種特定的方式失敗。樂(lè)觀地嘗試然后抓住一個(gè)異常是處理這種情況的一種極好的、完全是惡作劇的方法。
不知不覺地,抓住一個(gè)異常然后默默地繼續(xù),目前看來(lái)并不是那么可怕的想法。但是一旦你保存了文件,你就設(shè)置了你的代碼來(lái)創(chuàng)建最壞的bug:
在開發(fā)過(guò)程中可以逃脫檢測(cè)的bug,并被推送到實(shí)時(shí)生產(chǎn)系統(tǒng)中。
可以在生產(chǎn)代碼中生存數(shù)分鐘、數(shù)小時(shí)的bug,在您意識(shí)到錯(cuò)誤一直在發(fā)生之前的幾天或幾周。
很難排除的錯(cuò)誤。
即使您知道被抑制的異常是在哪里引發(fā)的,也很難修復(fù)的錯(cuò)誤。
請(qǐng)注意,我并不是說(shuō)永遠(yuǎn)不要捕獲異常。有很好的理由捕捉異常,然后繼續(xù)——只是不要沉默。一個(gè)很好的例子是一個(gè)任務(wù)關(guān)鍵型的過(guò)程,你根本不想失敗。在這里,一個(gè)聰明的模式是注入try子句來(lái)捕獲異常,記錄嚴(yán)重性logging.ERROR
或更高級(jí)別的完整堆棧跟蹤,然后繼續(xù)。
如果我們不想捕獲異常,我們?cè)撛趺醋觯坑袃煞N選擇。
在大多數(shù)情況下,最好的選擇是捕獲更具體的異常。類似于這樣:
try:
do_something()
# Catch some very specific exception - KeyError, ValueError, etc.
except ValueError:
pass
這是您應(yīng)該嘗試的第一件事。這需要對(duì)調(diào)用的代碼有一點(diǎn)了解,這樣您就知道它可能會(huì)引發(fā)什么類型的錯(cuò)誤。當(dāng)您第一次編寫代碼時(shí),這比清理其他人的代碼更容易做好。
如果某些代碼路徑必須廣泛地捕獲所有異常(例如,某個(gè)長(zhǎng)期運(yùn)行的持久化進(jìn)程的頂層循環(huán)),那么每個(gè)捕獲的異常都必須將完整堆棧跟蹤寫入日志或文件,還有時(shí)間戳。如果您使用的是Python的logging
模塊,那么這非常簡(jiǎn)單-每個(gè)logger對(duì)象都有一個(gè)名為exception的方法,它接受一個(gè)消息字符串。如果在except塊中調(diào)用它,捕獲的異常將自動(dòng)被完全記錄,包括跟蹤。
import logging
def get_number():
return int('foo')
try:
x = get_number()
except Exception as ex:
logging.exception('Caught an error')
日志將包含錯(cuò)誤消息,然后是一個(gè)跨多行的格式化堆棧跟蹤:
ERROR:root:Caught an error
Traceback (most recent call last):
File "example-logging-exception.py", line 8, inx = get_number()
File "example-logging-exception.py", line 5, in get_number
return int('foo')
ValueError: invalid literal for int() with base 10: 'foo'
非常簡(jiǎn)單。
如果應(yīng)用程序以其他方式進(jìn)行日志記錄-不使用logging
模塊,該怎么辦?假設(shè)您不想重構(gòu)應(yīng)用程序來(lái)實(shí)現(xiàn)這一點(diǎn),那么只需獲取并格式化與異常相關(guān)聯(lián)的回溯。這在Python3中是最簡(jiǎn)單的:
# The Python 3 version. It's a little less work.
import traceback
def log_traceback(ex):
tb_lines = traceback.format_exception(ex.__class__, ex, ex.__traceback__)
tb_text = ''.join(tb_lines)
# I'll let you implement the ExceptionLogger class,
# and the timestamping.
exception_logger.log(tb_text)
try:
x = get_number()
except Exception as ex:
log_traceback(ex)
在Python2中,您需要做更多的工作,因?yàn)楫惓?duì)象沒(méi)有附加它們的回溯。您可以通過(guò)調(diào)用except塊中的sys.exc_info()
來(lái)實(shí)現(xiàn)這一點(diǎn):
import sys
import traceback
def log_traceback(ex, ex_traceback):
tb_lines = traceback.format_exception(ex.__class__, ex, ex_traceback)
tb_text = ''.join(tb_lines)
exception_logger.log(tb_text)
try:
x = get_number()
except Exception as ex:
# Here, I don't really care about the first two values.
# I just want the traceback.
_, _, ex_traceback = sys.exc_info()
log_traceback(ex, ex_traceback)
事實(shí)證明,您可以定義一個(gè)可用于Python 2和Python 3的回溯日志函數(shù):
import traceback
def log_traceback(ex, ex_traceback=None):
if ex_traceback is None:
ex_traceback = ex.__traceback__
tb_lines = [ line.rstrip('n') for line in
traceback.format_exception(ex.__class__, ex, ex_traceback)]
exception_logger.log(tb_lines)
您現(xiàn)在可以做什么
“好的,亞倫,你說(shuō)服了我。在過(guò)去的所有時(shí)間里,我都為之哭泣和悲傷。我該如何贖罪?” 我很高興你問(wèn)。您可以從這里開始一些實(shí)踐。
在您的編碼準(zhǔn)則中明確禁止使用
如果您的團(tuán)隊(duì)進(jìn)行代碼審查,則可能會(huì)有一個(gè)編碼準(zhǔn)則文檔。如果沒(méi)有,那么就很容易開始-這就像創(chuàng)建一個(gè)新的Wiki頁(yè)面一樣簡(jiǎn)單,您的第一個(gè)條目可以是這個(gè)頁(yè)面。只需添加以下兩個(gè)準(zhǔn)則:
如果某個(gè)代碼路徑只是必須大致捕獲所有異常(例如,某些長(zhǎng)時(shí)間運(yùn)行的持久性進(jìn)程的頂級(jí)循環(huán)),則每個(gè)此類捕獲的異常必須寫入完整的堆棧跟蹤以及時(shí)間戳記日志或文件。不僅是異常類型和消息,而且還有完整的堆棧跟蹤。
對(duì)于所有其他除外條款(實(shí)際上應(yīng)該占絕大多數(shù)),捕獲的異常類型必須盡可能具體。諸如KeyError或ConnectionTimeout等之類的東西。
為現(xiàn)有的過(guò)境條款創(chuàng)建工單
上面的內(nèi)容將有助于防止將新問(wèn)題納入您的代碼庫(kù)?,F(xiàn)有的過(guò)度捕撈量如何?簡(jiǎn)單:在您的錯(cuò)誤跟蹤系統(tǒng)中制作票證或問(wèn)題以進(jìn)行修復(fù)。這是一個(gè)簡(jiǎn)單的操作步驟,大大增加了解決該問(wèn)題且不會(huì)被遺忘的機(jī)會(huì)。認(rèn)真地說(shuō),您現(xiàn)在就可以這樣做。
我建議您為每個(gè)存儲(chǔ)庫(kù)或應(yīng)用程序制作一張票證,以檢查代碼以查找捕獲異常的每個(gè)位置。(您也許可以通過(guò)在“除外:”和“例外除外”的代碼庫(kù)中重復(fù)查找全部?jī)?nèi)容。)對(duì)于每種情況,都可以將其轉(zhuǎn)換為捕獲非常具體的異常類型;或者,如果還不能立即清除應(yīng)有的內(nèi)容,請(qǐng)修改except塊以記錄完整的堆棧跟蹤。
(可選)審核開發(fā)人員可以為任何特定的try / except塊創(chuàng)建其他票證。如果您覺得可以使異常類更加具體,但是又不十分了解代碼部分以至于無(wú)法自信,那么這是一件好事。在這種情況下,您需要輸入代碼來(lái)記錄完整的堆棧跟蹤。創(chuàng)建單獨(dú)的票證以進(jìn)一步調(diào)查;并將其分配給可能更清晰的人。如果您發(fā)現(xiàn)自己花了超過(guò)五分鐘的時(shí)間來(lái)思考一個(gè)特定的try / except塊,建議您這樣做并繼續(xù)進(jìn)行下一個(gè)。
教育同伴團(tuán)隊(duì)成員
你們定期舉行工程會(huì)議嗎?每周,每?jī)芍苓€是每月?在下一個(gè)請(qǐng)求五分鐘的時(shí)間來(lái)解釋這種反模式,它對(duì)團(tuán)隊(duì)的生產(chǎn)力產(chǎn)生的成本以及簡(jiǎn)單的解決方案。
更好的是,事先聯(lián)系您的技術(shù)主管或工程經(jīng)理,并告訴他們有關(guān)情況。這將更容易出售,因?yàn)樗麄冎辽傧衲粯雨P(guān)注團(tuán)隊(duì)的生產(chǎn)力。發(fā)送給他們這篇文章的鏈接。哎呀,如果需要的話,我會(huì)幫助您-與他們通電話,我會(huì)說(shuō)服他們。
您可以在社區(qū)中接觸到更多人。您會(huì)去當(dāng)?shù)氐腜ython聚會(huì)嗎?他們有閃電談話,還是可以在下一次會(huì)議上談判五到十五分鐘的演講時(shí)間?宣傳這個(gè)崇高的事業(yè),為您的工程師服務(wù)。
為什么記錄完整堆棧跟蹤?
在上面的幾次中,我曾嘗試記錄整個(gè)堆棧跟蹤,而不僅僅是記錄異常對(duì)象的消息。如果這似乎需要更多工作,那是因?yàn)樗梢允牵焊櫚瑩Q行符,這些新行可能會(huì)破壞日志記錄系統(tǒng)的格式,您可能不得不考慮使用traceback模塊,依此類推。僅記錄消息本身還不夠嗎?
不,這不對(duì)。精心設(shè)計(jì)的異常消息僅告訴您except子句在哪里-什么文件和什么代碼行。通常,它甚至不會(huì)縮小這么多,但讓我們?cè)谶@里假設(shè)最好的情況。只記錄消息總比不記錄任何消息要好,但是不幸的是,它不能告訴您有關(guān)錯(cuò)誤起源的任何信息。通常,它可以位于完全不同的文件或模塊中,并且通常很難猜到。
除此之外,團(tuán)隊(duì)開發(fā)的實(shí)際應(yīng)用程序傾向于具有多個(gè)可以調(diào)用異常引發(fā)塊的代碼路徑。可能僅在調(diào)用Foo類的方法bar時(shí)發(fā)生錯(cuò)誤,而在調(diào)用函數(shù)bar()時(shí)則不會(huì)發(fā)生。僅記錄消息不會(huì)幫助您區(qū)分這兩者。
我最好的戰(zhàn)爭(zhēng)故事是來(lái)自一支大約五十人的中型工程團(tuán)隊(duì)。我相對(duì)較新,并且遇到了一個(gè)unicode錯(cuò)誤,該錯(cuò)誤會(huì)定期喚醒正在呼叫四個(gè)月以上的人員。捕獲到異常,并記錄了消息,但未記錄其他信息。另外兩名高級(jí)工程師各自工作了幾天,然后放棄了,說(shuō)他們無(wú)法解決。
這些都是強(qiáng)大而可怕的聰明工程師。最后,出于絕望,他們嘗試將其傳遞給我。使用他們的廣泛注釋,我立即著手很好地重現(xiàn)該問(wèn)題以獲取堆棧跟蹤。六個(gè)小時(shí)后,我終于明白了。一旦獲得了令人垂涎的堆棧跟蹤,您能猜出我花了多長(zhǎng)時(shí)間進(jìn)行修復(fù)?
10分鐘。那就對(duì)了。一旦有了堆棧跟蹤,修復(fù)就很明顯了。一個(gè)文字一周的時(shí)間工程師可能已被保存,如果我們已經(jīng)從一開始的日志記錄的堆棧跟蹤。還記得上面的內(nèi)容,當(dāng)我說(shuō)堆棧跟蹤可以在幾天內(nèi)解決錯(cuò)誤和在幾分鐘內(nèi)解決問(wèn)題之間有所區(qū)別嗎?我不是在開玩笑。
作者介紹
熱門博客推薦