起因
最近在公司的任務是寫一些簡單的運營工具,因為是很小的工具,所以就用了github上面的一個開源項目flask-admin,可以省去很多的事情。
但是,這個開源項目是個人維護的項目,所以文檔相對簡單,網上的資料相對較少,遇到一些產品經理要求具體功能並不能直接通過文檔和例子中的代碼找到答案。所以,我只能通過閱讀源代碼,重寫相關類以及方法實現了具體的需求。在這個過程中,學習到了一些東西,同時整理了自己以前的一些收獲,然后分享給大家,有不對的地方還望海涵、指正。
閱讀代碼有助於處理bug
閱讀代碼是一項更重要的技能,在大學編程語言的考試中也有相關的考察——代碼填空、代碼查錯。在工作中用到的地方更多:
- 查找bug
- 參與到已有的項目
- 接手別人的工作
- 開源項目的二次開發
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,好的工具可以讓你事半功倍。
下面介紹幾個快捷鍵和設置,有助於幫助閱讀提高閱讀代碼的效率:
-
設置:在項目文件目錄中展示打開文件的位置
-
cmd b :跳轉到變量、方法、類等定義的位置(最好完成了步驟1設置)
便於查看相關定義 -
cmd +/- :展開/折疊代碼塊(當前位置的:函數,注釋等)--加shift是全部
更加清晰的展示 -
alt F7 :查找該函數在何處被調用——便於察看相關調用
-
cmd f :在當前文件中查找 --加shift是在本項目中查找——查找某字段的位置
以上快捷鍵適用於mac,其它系統可以參考
最后
本文介紹了閱讀代碼的好處,以及基本的方法。我希望看完這篇文章后,如果讀者覺得有對的地方,可以在自己的平常工作和編程中實踐這些技能。在閱讀源代碼后把學到的技巧,總結、吸收、應用,相信長此以往,編程能力一定會得到提高!
進階,是一段很長的路,每遇到一個問題都是一個提高的機會,再難的問題、不好理解的代碼只要努力去探索、堅持去研究、尋找解決的方法。最終一定會搞懂的。
最后:
不積跬步,無以至千里
共勉!