Python進階學習之程式碼閱讀

北北北樂發表於2018-10-08

起因

最近在公司的任務是寫一些簡單的運營工具,因為是很小的工具,所以就用了github上面的一個開源專案Flask-admin,可以省去很多的事情。

但是,這個開源專案是個人維護的專案,所以文件相對簡單,網上的資料相對較少,遇到一些產品經理要求具體功能並不能直接通過文件和例子中的程式碼找到答案。所以,我只能通過閱讀原始碼,重寫相關類以及方法實現了具體的需求。在這個過程中,學習到了一些東西,同時整理了自己以前的一些收穫,然後分享給大家,有不對的地方還望海涵、指正。

閱讀程式碼有助於處理bug

閱讀程式碼是一項更重要的技能,在大學程式語言的考試中也有相關的考察——程式碼填空、程式碼查錯。在工作中用到的地方更多:

1. 查詢bug

2. 參與到已有的專案

3. 接手別人的工作

4. 開源專案的二次開發

python是解釋性語言,不需要反編譯就可以看到原始碼,利於查詢bug。在找bug的時候,最重要的是定位bug的位置,比較直觀的bug是通過閱讀異常可以定位到bug的位置。而有的異常資訊,例如:AttributeError: 'NoneType' object has no attribute 'get' 會讓你覺得很費解,因為你本以為這個肯定有值,怎麼就是None了呢?這個時候,你可以在異常定位的位置前面,把這個object的值列印出來,重新除錯。察看這個值到底是什麼,然後一步步的向上找到,是什麼地方操作了這個物件使得它的值為None,造成了這個異常(當然也可以使用通過ide除錯模式進行排查,本文重點是閱讀程式碼,所以就不介紹 打斷點決解bug的方法 了)。

綜合上面的講的:閱讀程式碼,定位到問題的位置,然後列印出來!這樣有利於分析問題,解決問題。

為什要先說這個技能,因為當我們用一個我們不熟悉、文件不完全的庫、類、方法或者函式的時候,通常會遇到問題,通過上面的方法,定位到問題,通過輸出值,閱讀程式碼。退後推敲出問題的原因,就可以很快的找到解決辦法。當然,這個方法也不是什麼bug能夠解決的,但是通過上面的方法嘗試解決不成功後,再拿著這個bug去問別人的時候,就可以具體到某個方法,精確的提問。大家的時間都很寶貴,如果你提出一個泛泛的問題、沒有自己嘗試解決過的問題,那麼誰也無法給你一個好的解答(提問的智慧。同時可以減少被批評的次數。。。。?(注意看異常資訊很重要,我曾經就拿很多低階問題去問我師父,我師傅走過來一看:你把這個異常提示給我翻一下。

def open_url(url:複製程式碼
                ^複製程式碼
SyntaxError: invalid syntax複製程式碼

原來是忘了寫')'。。。。?)

閱讀程式碼有助於提高自己的程式設計水平

閱讀原始碼也是提高編碼能力的一種途徑,就像臨摹大師的畫一樣。可以通過觀摩理解,吸收別人的智慧與技巧提高自己的能力。因為,工作上需要用flask,因為最開始自己學習flask的時候就對flask中的全域性變數:g、request、session等,全域性變數覺得很奇怪。request是全域性變數,但是每個請求的request都是不一樣,在我呼叫request物件的時候並沒有指定是那個請求的request,flask怎就能給我當前請求的request?通過查閱資料,再加上自己閱讀flask的程式碼:

class Local(object):    ## request物件是Local的例項    __slots__ = ('__storage__', '__ident_func__')     def __init__(self):複製程式碼
複製程式碼
複製程式碼
複製程式碼
複製程式碼

## object.__setattr__如此賦值,並不呼叫例項的__setattr__方法

object.__setattr__(self, '__storage__', {})

## 這裡的__ident_func__存的的是當前程式/協程的唯一id

object.__setattr__(self, '__ident_func__', get_ident)

def __iter__(self):

return iter(self.__storage__.items())

def __call__(self, proxy):

"""Create a proxy for a name."""

return LocalProxy(self, proxy)

def __release_local__(self):

self.__storage__.pop(self.__ident_func__(), None)

def __getattr__(self, name):

try:

## 每次呼叫request就是呼叫當前程式/協程下的request

return self.__storage__[self.__ident_func__()][name]

except KeyError:

raise AttributeError(name)

def __setattr__(self, name, value):

ident = self.__ident_func__()

storage = self.__storage__

try:

storage[ident][name] = value

except KeyError:

storage[ident] = {name: value}

def __delattr__(self, name):

try:

del self.__storage__[self.__ident_func__()][name]

except KeyError:

raise AttributeError(name)

通過閱讀flask的內部實現就明白了到底是如何優雅的實現:使用這些全域性變數的時候,你啥都不用管只要呼叫就行了。這就是python的優雅的一 面。而優秀的程式碼就是類似於這種的優雅的實現,多多‘臨摹’高手的程式碼,可以學到更多優雅技巧:裝飾器、協程、生成器、魔法方法等。而不是光學會概念、寫一些例子。閱讀程式碼中看到實際的應用程式碼片段,更加有助於自己以後用到自己的程式碼中。

閱讀程式碼有助於養成優秀的程式碼風格

“優秀的程式碼不需要文件”,這句話雖然說的有些誇張的成份,但是也並無一定道理。優秀的專案中的程式碼,註釋佔的比重是相當大的。比方tornado框架中的程式碼:

class HTTPServer(TCPServer, Configurable,

httputil.HTTPServerConnectionDelegate):

r"""A non-blocking, single-threaded HTTP server.

A server is defined by a subclass of `.HTTPServerConnectionDelegate`,

or, for backwards compatibility, a callback that takes an

`.HTTPServerRequest` as an argument. The delegate is usually a

`tornado.web.Application`.

`HTTPServer` supports keep-alive connections by default

(automatically for HTTP/1.1, or for HTTP/1.0 when the client

requests ``Connection: keep-alive``).

If ``xheaders`` is ``True``, we support the

``X-Real-Ip``/``X-Forwarded-For`` and

``X-Scheme``/``X-Forwarded-Proto`` headers, which override the

remote IP and URI scheme/protocol for all requests. These headers

are useful when running Tornado behind a reverse proxy or load

balancer. The ``protocol`` argument can also be set to ``https``

if Tornado is run behind an SSL-decoding proxy that does not set one of

the supported ``xheaders``.

To make this server serve SSL traffic, send the ``ssl_options`` keyword

argument with an `ssl.SSLContext` object. For compatibility with older

versions of Python ``ssl_options`` may also be a dictionary of keyword

arguments for the `ssl.wrap_socket` method.::

ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)

ssl_ctx.load_cert_chain(os.path.join(data_dir, "mydomain.crt"),

os.path.join(data_dir, "mydomain.key"))

HTTPServer(applicaton, ssl_options=ssl_ctx)

`HTTPServer` initialization follows one of three patterns (the

initialization methods are defined on `tornado.tcpserver.TCPServer`):

1. `~tornado.tcpserver.TCPServer.listen`: simple single-process::

server = HTTPServer(app)

server.listen(8888)

IOLoop.current().start()

In many cases, `tornado.web.Application.listen` can be used to avoid

the need to explicitly create the `HTTPServer`.

2. `~tornado.tcpserver.TCPServer.bind`/`~tornado.tcpserver.TCPServer.start`:

simple multi-process::

server = HTTPServer(app)

server.bind(8888)

server.start(0) # Forks multiple sub-processes

IOLoop.current().start()

When using this interface, an `.IOLoop` must *not* be passed

to the `HTTPServer` constructor. `~.TCPServer.start` will always start

the server on the default singleton `.IOLoop`.

3. `~tornado.tcpserver.TCPServer.add_sockets`: advanced multi-process::

sockets = tornado.netutil.bind_sockets(8888)

tornado.process.fork_processes(0)

server = HTTPServer(app)

server.add_sockets(sockets)

IOLoop.current().start()

The `~.TCPServer.add_sockets` interface is more complicated,

but it can be used with `tornado.process.fork_processes` to

give you more flexibility in when the fork happens.

`~.TCPServer.add_sockets` can also be used in single-process

servers if you want to create your listening sockets in some

way other than `tornado.netutil.bind_sockets`.

.. versionchanged:: 4.0

Added ``decompress_request``, ``chunk_size``, ``max_header_size``,

``idle_connection_timeout``, ``body_timeout``, ``max_body_size``

arguments. Added support for `.HTTPServerConnectionDelegate`

instances as ``request_callback``.

.. versionchanged:: 4.1

`.HTTPServerConnectionDelegate.start_request` is now called with

two arguments ``(server_conn, request_conn)`` (in accordance with the

documentation) instead of one ``(request_conn)``.

.. versionchanged:: 4.2

`HTTPServer` is now a subclass of `tornado.util.Configurable`.

"""

def __init__(self, *args, **kwargs):

# Ignore args to __init__; real initialization belongs in

# initialize since we're Configurable. (there's something

# weird in initialization order between this class,

# Configurable, and TCPServer so we can't leave __init__ out

# completely)

pass

上面的註釋包含了:類的說明、例子、版本主要改動。

優秀的程式碼風格:看到名字就能知道它是用來幹什麼的(顧名思義)、結構清晰、程式碼風格統一(命名規則、格式)

這些優秀的特質都是為了:可讀性、容易理解。正如:程式碼主要是給人看的,讓計算機執行是次要的

如果是在閱讀了不好的程式碼,如果你心裡在罵:“這程式碼簡直是一坨?”,一定要注意:自己寫的程式碼,不能讓人在背後罵啊。所以寫程式碼的時候不要圖一時爽,為了快沒有了原則。沒準一個月後你自己看的時候,心裡還在想這是誰寫的,這麼屎,最後發現是自己的‘傑作’。。。。

所以,自己的優秀的編碼風格也是成為一個合格的程式設計師必備的一項技能(面試要求會有這一項),通過閱讀程式碼學習,模仿優秀的程式碼風格,有助於自己寫出‘漂亮’、整潔的程式碼。

利用工具閱讀

因為我是個pythoner,常用語言是python(其實是別的語言都不會。。?),我推薦一款IDE——PyCharm,好的工具可以讓你事半功倍。

下面介紹幾個快捷鍵和設定,有助於幫助閱讀提高閱讀程式碼的效率:

1. 設定:在專案檔案目錄中展示開啟檔案的位置

Python進階學習之程式碼閱讀

Python進階學習之程式碼閱讀

2. cmd b :跳轉到變數、方法、類等定義的位置(最好完成了步驟1設定)

便於檢視相關定義

Python進階學習之程式碼閱讀

3. cmd +/- :展開/摺疊程式碼塊(當前位置的:函式,註釋等)--加shift是全部

更加清晰的展示

Python進階學習之程式碼閱讀

4. alt F7 :查詢該函式在何處被呼叫——便於察看相關呼叫

5. cmd f :在當前檔案中查詢 --加shift是在本專案中查詢——查詢某欄位的位置

以上快捷鍵適用於mac,其它系統可以參考

最後

本文介紹了閱讀程式碼的好處,以及基本的方法。我希望看完這篇文章後,如果讀者覺得有對的地方,可以在自己的平常工作和程式設計中實踐這些技能。在閱讀原始碼後把學到的技巧,總結、吸收、應用,相信長此以往,程式設計能力一定會得到提高!

想學習交流的同學可以加Python群705673780,群內有許多免費資料可供學習哦~

進階,是一段很長的路,每遇到一個問題都是一個提高的機會,再難的問題、不好理解的程式碼只要努力去探索、堅持去研究、尋找解決的方法。最終一定會搞懂的。

最後:

不積跬步,無以至千里

共勉!


相關文章