MUR009 思辨註釋與文檔

目錄

思辨註釋與文檔

紫式晦澀每日一篇文章第9天

前言

  1. 今天是2022年第7天, 第1週的第1個週五. 今天來思考紀錄, 在寫程式時很重要的, 關於「註解與文檔」相關的知識. 長期而言, 這是「編程思維」系列下的第一篇.

  2. 今天的素材主要來自Documenting Python Code: A Complete Guide 中, 節選相關段落與紀錄個人理解與心得.

註解與文檔: 為讀者服務

  1. 讀者思維於程式: 寫程式本身為兩種主要讀者服務. 第一種是開發者(包含未來的自己), 另一種是使用者. 作為開發者, 把註解(Comment)寫好, 也會讓未來的自己省時間. 作為使用者, 把文檔(Documentation)寫好, 會讓未來可能的使用者學習成本下降.

“Code is more often read than written.”

— Guido van Rossum

  1. 定義註解: 「程式註解」是向開發者描述你的程式. 預期的主要受眾是 Python 程式的維護者和開發者。 編寫良好的程式+註釋有助於引導讀者更好地理解你的程式的「目的」和「設計」.

“Code tells you how; Comments tell you why.”

— Jeff Atwood (aka Coding Horror)

  1. 定義文檔: 「程式文檔」是向使用者描述你的程式的用途與功能. 其主要的受眾為用戶, 也會在開發過程中有幫助.

註解: 面向開發者

註解的兩種技法: 井字號, 類型提示

  1. 利用#給註解: 註解應該是「簡潔的幾個句子」. 例如:
1def hello_world():
2    # A simple comment preceding a simple print statement
3    print("Hello World") 

其中的「# A simple comment preceding a simple print statement」就是簡潔的註解. 根據PEP 8 , 一個註解不超過72個字元.

  1. 利用類型提示: 類型提示(Type Hint)於 Python 3.5 開始支援.類型提示是一種幫助讀者閱讀程式的附加形式。它允許開發人員設計和解釋他們的部分代碼而不用註釋。 例如
1def hello_name(name: str) -> str:
2    return(f"Hello {name}")

就提示了函數hellp_name是一個輸入為字串str資料類型, 輸出也是字串str資料類型的函數.

  1. 通過檢查類型提示,可以立即看出該函數期望輸入名稱為 str 或字符串類型。 此外, 還可以判斷該函數的預期輸出也將是 str 或 string 類型。 雖然類型提示有助於減少註釋,但在創建或更新項目文檔時,這樣做也可能會增加額外的工作量。

省現在的時間, 或省未來的時間.

註解的四種用法: 計畫與回顧, 程式描述, 演算法描述, 標記待改良點

  1. 註解用法一:計畫與回顧: 當開發程式的新部分,可以首先使用註釋作為「規劃(Planning)」或概述該部分程式。 一旦實際程式已經實踐和審查/測試,就可以刪除這些註解.
1# First step
2# Second step
3# Third step
  1. 這個用法, 在寫文章也很常用. 尤其是寫科學研究論文的草稿時, 用註解的方式先安排文章的結構, 然後分個完成. 完成後的部分, 之後重新組合, 可能可以出現更好的組織, 昇華文章帶給讀者的意象.

  2. 註解用法二:程式描述: 註解可解釋「特定部分程式」的意圖.

1# Attempt a connection based on previous settings. If unsuccessful,
2# prompt user for new settings.
  1. 這個用法, 在寫理論文章時, 因為要證明很多lemma, 也會利用註解來描述特定lemma的意圖. 因此, 我們可以很合理的類比「lemma」就是「script」 , 而很多的lemma與很多的script構造出智慧的結晶.

  2. 註解用法三:演算法描述: 複雜的演算法, 可以利用註解向開發者解釋如何實踐你的程式. 也可描述為何選擇特定的算法. 這個可以寫在function裡面詳細每步的動機.

1# Using quick sort for performance gains
  1. 註解用法四: 標記: 對特定部分的程式, 標記已知問題, 來改良該部分的問題. 實際例子有: BUG, FIXME 與 TODO.
1# TODO: Add condition for when val is None

Jeff Atwood的四條註解規則: 就近註釋, 避免複雜格式, 開門見山, 設計好的程式結構

  1. 就近註釋原則: 使註釋盡可能靠近所描述的代碼。 不在描述代碼附近的註釋會讓讀者感到沮喪,並且在進行更新時很容易錯過。

  2. 避免複雜格式: 不要使用複雜的格式(例如表格或 ASCII 數字)。 複雜的格式會導致內容分散注意力,並且隨著時間的推移可能難以維護。

  3. 開門見山原則: 不要包含多餘的信息。 假設代碼的讀者對編程原理和語言語法有基本的了解。

  4. 設計好的程式結構: 設計你的代碼來評論自己。 理解代碼最簡單的方法是閱讀它。 當您使用清晰、易於理解的概念設計代碼時,讀者將能夠快速概念化您的意圖。

文檔: 面向使用者

  1. 用字符串做文檔: 前面了解了註釋(Comment), 現在開始討論文檔(Documentation). 我們要學習使用「字符串 (Docstrings)」來做文檔. 若內置的字符串配置正確, 可顯著幫助用戶與自己的項目發展.

召喚字符串, 物件目錄, 文檔: help(), dir(), __doc__

  1. help()召喚字符串: 利用Python的內置函數help()可召喚出「物件字符串」. 例如:
 1>>> help(str)
 2Help on class str in module builtins:
 3
 4class str(object)
 5 |  str(object='') -> str
 6 |  str(bytes_or_buffer[, encoding[, errors]]) -> str
 7 |
 8 |  Create a new string object from the given object. If encoding or
 9 |  errors are specified, then the object must expose a data buffer
10 |  that will be decoded using the given encoding and error handler.
11 |  Otherwise, returns the result of object.__str__() (if defined)
12 |  or repr(object).
13 |  encoding defaults to sys.getdefaultencoding().
14 |  errors defaults to 'strict'.
15 # Truncated for readability
  1. dir()召喚物件目錄: 在Python中, 什麼都是物件(Object). 利用函數dir()可召喚出物件的目錄(directory).
1>>> dir(str)
2['__add__', ..., '__doc__', ..., 'zfill'] # Truncated for readability
  1. 目錄的屬性:__doc__: 在召喚出來的物件目錄中, 有個特殊的屬性__doc__, 其內容如下:
 1>>> print(str.__doc__)
 2str(object='') -> str
 3str(bytes_or_buffer[, encoding[, errors]]) -> str
 4
 5Create a new string object from the given object. If encoding or
 6errors are specified, then the object must expose a data buffer
 7that will be decoded using the given encoding and error handler.
 8Otherwise, returns the result of object.__str__() (if defined)
 9or repr(object).
10encoding defaults to sys.getdefaultencoding().
11errors defaults to 'strict'.

文檔客製物件:編輯__doc__, 三引號(triple-double quote)註解, 多行字符串

  1. 客製物件的文檔1: 編輯__doc__: 自定義函數, 可以透過編輯其__doc__屬性來提供文檔.
1def say_hello(name):
2    print(f"Hello {name}, is it me you're looking for?")
3
4say_hello.__doc__ = "A simple function that says hello... Richie style"
1>>> help(say_hello)
2Help on function say_hello in module __main__:
3
4say_hello(name)
5    A simple function that says hello... Richie style
  1. 客製物件的文檔2:三引號(triple-double quote)註解:另一種方法來寫函數的文檔, 是直接在def的下面加註解.
1def say_hello(name):
2    """A simple function that says hello... Richie style"""
3    print(f"Hello {name}, is it me you're looking for?")
1>>> help(say_hello)
2Help on function say_hello in module __main__:
3
4say_hello(name)
5    A simple function that says hello... Richie style
  1. 多行字符串的格式: 當文檔較為精緻, 成為多行字符串, 則建議包含以下四個部分:
  • A one-line summary line 一行總結
  • A blank line proceeding the summary 一行空白
  • Any further elaboration for the docstring 更細緻的描述
  • Another blank line 另一行空白

三大文檔類型: 類字符串, 包與模組字符串, 腳本字符串

  1. 定義三大文檔類型:
  • Class Docstrings: 類(Class) and 類方法(class methods)
  • Package and Module Docstrings: 包(Package), 模組(modules), and 函數(functions)
  • Script Docstrings: 腳本(Script) and 函數(functions)

類字符串: __init__類方法上

  1. 類與類方法字符串: 定義類與類方法以後, 建議就加上文檔.
1class SimpleClass:
2    """Class docstrings go here."""
3
4    def say_hello(self, name: str):
5        """Class method docstrings go here."""
6
7        print(f'Hello {name}')

其中有SimpleClass這個類的文檔, 也有say_hello這個類方法的文檔.

  1. 類文檔應有資訊:類文檔應有以下四種資訊
    1. A brief summary of its purpose and behavior 簡述類的「目的」與「行為」
    1. Any public methods, along with a brief description 任何「公用方法」與其簡單描述
    1. Any class properties (attributes) 任何「類屬性」
    1. Anything related to the interface for subclassers, if the class is intended to be subclassed 是否是作為「介面(interface)」而建立的類
  1. __init__類方法文檔: 在__init__類方法的字符串, 應該要聞黨類創造子的參數(class constructor parameters). 類創造子是一個物件導向中的基礎概念, 其限定了類創造物件時的細節.

  2. 類方法字符串應有資訊: 類方法字符串應有資訊包含以下六點:

    1. A brief description of what the method is and what it’s used for 方法的簡單描述與其目的
    1. Any arguments (both required and optional) that are passed including keyword arguments 任何會在「關鍵字參數 (Keyword arguments)」中「必要(required)」或「可選(optional)」的「實際參數(argument)」
    1. Label any arguments that are considered optional or have a default value 標記可選參數或其預設值
    1. Any side effects that occur when executing the method 方法的副作用
    1. Any exceptions that are raised 會出現的例外
    1. Any restrictions on when the method can be called 呼叫方法後會引發的限制
  1. 實例: Animal: 這個類有簡單的「類屬性(class properties)」, 「實例屬性(instance properties)」, 「__init__類方法」, 「實例方法(instance method )」
 1class Animal:
 2    """
 3    A class used to represent an Animal
 4
 5    ...
 6
 7    Attributes
 8    ----------
 9    says_str : str
10        a formatted string to print out what the animal says
11    name : str
12        the name of the animal
13    sound : str
14        the sound that the animal makes
15    num_legs : int
16        the number of legs the animal has (default 4)
17
18    Methods
19    -------
20    says(sound=None)
21        Prints the animals name and what sound it makes
22    """
23
24    says_str = "A {name} says {sound}"
25
26    def __init__(self, name, sound, num_legs=4):
27        """
28        Parameters
29        ----------
30        name : str
31            The name of the animal
32        sound : str
33            The sound the animal makes
34        num_legs : int, optional
35            The number of legs the animal (default is 4)
36        """
37
38        self.name = name
39        self.sound = sound
40        self.num_legs = num_legs
41
42    def says(self, sound=None):
43        """Prints what the animals name is and what sound it makes.
44
45        If the argument `sound` isn't passed in, the default Animal
46        sound is used.
47
48        Parameters
49        ----------
50        sound : str, optional
51            The sound the animal makes (default is None)
52
53        Raises
54        ------
55        NotImplementedError
56            If no sound is set for the animal or passed in as a
57            parameter.
58        """
59
60        if self.sound is None and sound is None:
61            raise NotImplementedError("Silent Animals are not supported!")
62
63        out_sound = self.sound if sound is None else sound
64        print(self.says_str.format(name=self.name, sound=out_sound))

包與模組字符串: __init__.py檔案中

  1. 包字符串: 包字符串要在包頂層的__init__.py檔案中. 這個字符串要包含「模組(modules)」與「子包(subpackages)」的列表.

  2. 模組字符串: 模組字符串要列表出任何其包含的函數. 模組字符串要在整的文件的最頂端. 須包含以下兩點:

    1. A brief description of the module and its purpose 簡述模組與其目的
    1. A list of any classes, exception, functions, and any other objects exported by the module 列表類, 例外, 函數, 其他任何模組會輸出的物件.
  1. 模組函數文檔: 其規則與「類方法」類似, 要包含六點:
    1. A brief description of what the method is and what it’s used for 方法的簡單描述與其目的
    1. Any arguments (both required and optional) that are passed including keyword arguments 任何會在「關鍵字參數 (Keyword arguments)」中「必要(required)」或「可選(optional)」的「實際參數(argument)」
    1. Label any arguments that are considered optional or have a default value 標記可選參數或其預設值
    1. Any side effects that occur when executing the method 方法的副作用
    1. Any exceptions that are raised 會出現的例外
    1. Any restrictions on when the method can be called 呼叫方法後會引發的限制

腳本字符串: argparse活用

  1. 定義腳本: 腳本(Scripts)是主機(Console)可執行的單檔案. Scripts are considered to be single file executables run from the console.

  2. 腳本字符串: 要在檔案最上方, 要提供足夠資訊讓用戶能使用腳本.

  3. 使用argparse:使用argparser.parser.add_argument函數, 利用argpase__doc__文檔來對參數的description. (詳細請見Command-Line Parsing Libraries )

  4. 列出腳本前置包: 文檔要包含讓用戶知道要先安裝哪些包. 以下是列印表格欄的腳本. 有說明要安裝pandas還有要輸入模組get_spreadsheet_cols,並包含函數main.

 1"""Spreadsheet Column Printer
 2
 3This script allows the user to print to the console all columns in the
 4spreadsheet. It is assumed that the first row of the spreadsheet is the
 5location of the columns.
 6
 7This tool accepts comma separated value files (.csv) as well as excel
 8(.xls, .xlsx) files.
 9
10This script requires that `pandas` be installed within the Python
11environment you are running this script in.
12
13This file can also be imported as a module and contains the following
14functions:
15
16    * get_spreadsheet_cols - returns the column headers of the file
17    * main - the main function of the script
18"""
19
20import argparse
21
22import pandas as pd
23
24
25def get_spreadsheet_cols(file_loc, print_cols=False):
26    """Gets and prints the spreadsheet's header columns
27
28    Parameters
29    ----------
30    file_loc : str
31        The file location of the spreadsheet
32    print_cols : bool, optional
33        A flag used to print the columns to the console (default is
34        False)
35
36    Returns
37    -------
38    list
39        a list of strings used that are the header columns
40    """
41
42    file_data = pd.read_excel(file_loc)
43    col_headers = list(file_data.columns.values)
44
45    if print_cols:
46        print("\n".join(col_headers))
47
48    return col_headers
49
50
51def main():
52    parser = argparse.ArgumentParser(description=__doc__)
53    parser.add_argument(
54        'input_file',
55        type=str,
56        help="The spreadsheet file to pring the columns of"
57    )
58    args = parser.parse_args()
59    get_spreadsheet_cols(args.input_file, print_cols=True)
60
61
62if __name__ == "__main__":
63    main()

後記

  1. 發展文檔: 原文後面有個文檔進展的五個階段:
    1. No Documentation
    1. Some Documentation
    1. Complete Documentation
    1. Good Documentation
    1. Great Documentation 感覺這也很適合講正在執筆寫的論文. 從No, some, complete, good, great. 先搞出完整的, 再到好, 再到很棒. 用這樣子來分類項目的成熟度, 十分不錯.
  1. 到此紀錄了由Documenting Python Code: A Complete Guide 中, 節選的相關段落. 首先理解註解是為開發者而服務, 而文檔是為用戶而服務. 接著看了類與類方法, 包與模組, 腳本三種Python文件的文檔字符串寫法.

  2. 非常好!今天是每日一篇文章的第九天. 今天這個技術文章很長, 一段一段讀, 改寫, 也刺激自己對這些技術概念的融會貫通, 感覺心中更踏實. 持續輸出! 共勉之!

2022.01.07. 紫蕊 於 西拉法葉, 印第安納, 美國.

版權

CC BY-NC-ND 4.0

評論