iT邦幫忙

2022 iThome 鐵人賽

DAY 4
0
自我挑戰組

基於自然語言處理的新聞意見提取應用開發筆記系列 第 4

[Day-04] 專案的 Python 風格(採用 Google Style Guides 搭配 yapf)

  • 分享至 

  • xImage
  •  

Day-04 內容

  • 專案的 Python 風格
    • Style guide 簡述
    • Google Style Guides(Python)
      • Python 語言使用規範 (Language Rules)
      • Python 風格規範 (Style Rules)
    • yapf 使用
      • 安裝 yapf
      • 如何執行 yapf
      • yapf 的效果範例

已經堅持三天,來到第四天了!雖然有寫作速度漸漸加快的趨勢,但前幾天花費的時間有些超出預期,壓縮到其他行程安排,所以今日就安排了一個相對複雜度低的主題來喘息一下,留點時間規劃接下來的內容。

專案的 Python 風格

這個「基於自然語言處理的新聞意見提取應用」預計九成佔比的程式碼會採用 Python,剩下的部分則為網頁架設時會用到的 javascript 等等。有別於以往趕時間完成的作業程式碼,我期許自己能在這次的專案程式碼中做到以下幾點:

  • 程式碼乾淨易讀
  • 檔案結構有條理
  • 註解清楚
  • (其他效能等重要議題在這暫時沒有列出)

我希望透過堅持以上幾點,降低後續自己在維護程式碼時的負擔,也使得後需其他人更容閱讀與參與合作,所以這次決定遵守部分 Google 開源專案會使用的 Google Style Guides 的指引,至於為什麼不是也很常見的 PEP 8 – Style Guide for Python Code,也許是做了以後會去這間大公司寫 code 的夢。

Style guide 簡述

什麼是 「Style(風格)」 呢?

「Style(風格)」”涵蓋了很多方面,從「使用駝峰式命名變數(use camelCase for variable names)」到「不使用全局變量(never use global variables)」都包含在其範疇。

「Style guide(風格指南)」又是什麼?

每個主要的開源項目多數都有各己的 Style guide(風格指南):是一套關於如何為該項目撰寫程式馬的規範。當其中的所有程式碼都採用一致的樣式時,在理解大型代碼庫會容易得多。

以上參考 Google Style Guides 的敘述


Google Style Guides(Python)

下面是 Google Python Style Guide 的整理,想看中文的也有翻譯可以參考 Google Python 風格指南 - 中文版

本段內容大量參考: Google Python Style GuideGoogle Python 風格指南 - 中文版 的內容。

Google Python Style Guide 主要分為下面兩大部分:

  1. Python 語言使用規範 (Language Rules)

    • 包含一些語法、技巧的使用注意事項等
  2. Python 風格規範 (Style Rules)

    • 有關程式碼的排列注意事項

Python 語言使用規範 (Language Rules)

1. 對你的 Python 程式碼執行 pylint(使用此 pylintrc(可點擊下載)

  • 優點:

    • 可以捕獲容易忽視的錯誤,例如輸入錯誤,使用未賦值的變數等。
  • pylint

    • pylint 是一個在 Python 程式碼中查找 bug 的工具。可以捕獲容易忽視的錯誤,例如輸入錯誤,使用未賦值的變數等。
    • 由於 Python 的動態特性,有些警告可能不正確(極少情況)。
  • 使用

    • 建議使用 Linting Python in Visual Studio Code 設定為 Pylint,並配合 pylintrc(可點擊下載)

    • 可以通過設置一個行註釋來抑制告警,例如:

      dict = 'something awful'  # Bad Idea... pylint: disable=redefined-builtin
      
    • 要抑制「變數未使用」告警,可以用 "" 作為變數標識符,或者在變數名稱前加 "unused"(e.g. unused_b, _ =d,e):

      def foo(a, unused_b, unused_c, d=None, e=None):
          _ = d, e
          return a
      

2. 只 import packages 和 module,而非單一 class 或 function

  • 優點:

    • 讓命名空間管理更加簡單。
    • x.Obj 表示 Obj 定義在 module x
  • 規則:

    • 使用 import x 來導入 package 和 module
    • 使用 from x import y , 其中 x 是 package 前綴, y 是不帶前綴的 module 名稱
    • 使用 from x import y as z,如果兩個要導入的 module 都叫做 z 或者 y 太長了

3. 在 packages 中使用 module 的完整徑名來 import 每個 module

  • 優點:避免 module 名衝突,找尋 package 更容易

  • 範例:

    # Reference in code with complete name.
    import sound.effects.echo
    
    # Reference in code with just module name (preferred).
    from sound.effects import echo
    

4. 小心使用 Exception

  • 要點
    • 不要使用 except: 語句來捕獲所有異常
    • 盡量減少 try 的程式碼量。 try區塊的體積越大,期望之外的異常就越容易觸發。這種情況下,容易隱藏真正的錯誤。
    • 使用 finally 子句來執行那些無論 try 區塊中有沒有異常都應該被執行的代碼。這對於清理資源常常很有用,例如關閉文件。

5. 盡量避免使用 global variables(全域變數)

  • 要點
    • 如果需要使用 global variables,應宣告在 module 級別,並通過在名稱前加上 _ 使其成為 module 的內部變量。
    • 對 global variables 的部訪問必須通過公共 module 級函數。

6. Nested local functions or classes are fine when used to close over a local variable. Inner classes are fine.

7. 在簡單的情況下使用 Comprehensions

  • 優點:
    List、Dict 和 Set Comprehensions 以及 generator 表達式提供了一種簡潔有效的方法來創建容器類型(container types)和迭代器(iterators),而無需使用傳統的循環、map()、filter() 或 lambda。

  • 舉例:

    • yes
      # Yes:
      result = [mapping_expr for value in iterable if filter_expr]
      
      result = [{'key': value} for value in iterable
              if a_long_filter_expression(value)]
      
      result = [complicated_transform(x)
              for x in iterable if predicate(x)]
      
      descriptive_name = [
        transform({'key': key, 'value': value}, color='black')
        for key, value in generate_iterable(some_input)
        if complicated_condition_is_met(key, value)
      ]
      
      result = []
      for x in range(10):
        for y in range(5):
            if x * y > 10:
                result.append((x, y))
      
      return {x: complicated_transform(x)
            for x in long_generator_function(parameter)
            if x is not None}
      
      squares_generator = (x**2 for x in range(10))
      
      unique_names = {user.name for user in users if user is not None}
      
      eat(jelly_bean for jelly_bean in jelly_beans
        if jelly_bean.color == 'black')
      
    • No
      # No:
      result = [complicated_transform(
                   x, some_argument=x+1)
               for x in iterable if predicate(x)]
      
      result = [(x, y) for x in range(10) for y in range(5) if x * y > 10]
      
      return ((x, y, z)
             for x in range(5)
             for y in range(5)
             if x != y
             for z in range(5)
             if y != z)
      

8. 在 type (類型)支持的情況下,就使用 default iterators (預設迭代器)和 membership test operators.

  • 像 dict 和 list,定義了默認的迭代器和關係測試操作符(in 和 not in)
  • 舉例:
    • Yes
      #Yes: 
      for key in adict: ...
      if key not in adict: ...
      if obj in alist: ...
      for line in afile: ...
      for k, v in adict.items(): ...
      
    • No
      #No:
      for key in adict.keys(): ...
      if not adict.has_key(key): ...
      for line in afile.readlines(): ...
      

9. 按需求使用 Generators(生成器)

  • 有關係到 yield 的使用,但我不常碰到。
  • 細節請參考 2.9 Generators

10. 只在單行情況下使用 Lambda function

  • 避免 Lambda function 過於複雜不易理解。

11. 只在簡單情況下使用 Conditional Expressions

  • 舉例:
    • Yes
      #Yes:
      one_line = 'yes' if predicate(value) else 'no'
      slightly_split = ('yes' if predicate(value)
                        else 'no, nein, nyet')
      the_longest_ternary_style_that_can_be_done = (
          'yes, true, affirmative, confirmed, correct'
          if predicate(value)
          else 'no, false, negative, nay')
      
    • No
      #No:
      bad_line_breaking = ('yes' if predicate(value) else
                           'no')
      portion_too_long = ('yes'
                          if some_long_module.some_long_predicate_function(
                              really_long_variable_name)
                          else 'no, false, negative, nay')
      

12. 可視情況使用 Default Argument Values (預設參數值)

  • 舉例:
    • Yes
      #Yes: 
      def foo(a, b=None):
       if b is None:
           b = []
      #Yes:
      def foo(a, b: Optional[Sequence] = None):
       if b is None:
           b = []
      #Yes:
      def foo(a, b: Sequence = ()):  # Empty tuple OK since tuples are immutable
           ...
      
    • No
      from absl import flags
      _FOO = flags.DEFINE_string(...)
      
      #No:
      def foo(a, b=[]):
               ...
      #No:  
      def foo(a, b=time.time()):  # The time the module was loaded???
               ...
      #No:  
      def foo(a, b=_FOO.value):  # sys.argv has not yet been parsed...
               ...
      #No:  
      def foo(a, b: Mapping = {}):  # Could still get passed to unchecked code
               ...
      

13. 視情況善用 properties

14. 盡可能使用 implicit (隱式) False

  • 優點
    • 使用Python 的 boolean 更易讀也更不易犯錯,大部分情況下也更快。
  • 要點
    • if foo: rather than if foo != []:
    • Always use if foo is None: (or is not None) to check for a None value
    • Never compare a boolean variable to False using ==. Use if not x: instead.
    • For sequences (strings, lists, tuples), use the fact that empty sequences are false, so if seq: and if not seq: are preferable to if len(seq): and if not len(seq): respectively.
    • When handling integers, implicit false may involve more risk than benefit (i.e., accidentally handling None as 0). You may compare a value which is known to be an integer (and is not the result of len()) against the integer 0.

15. 可使用 Lexical Scoping

  • 詳細說明:2.16 Lexical Scoping
  • 舉例
    def get_adder(summand1):
    """Returns a function that adds numbers to a given number."""
    def adder(summand2):
        return summand1 + summand2
    
    return adder
    

16. 善用 decorators , 避免 @staticmethod 且少用 @classmethod

17. 不要依賴內建類型(built-in types)的原子性(atomicity)

18. Avoid Power Feature

19. 鼓勵使用 from __future__ imports

20. Type Annotated Code

  • 優點
    你可以根據 PEP-484 使用類型提示(type hints according)註釋 Python 程式碼,並在構建時(build time)使用 pytype 之類的類型檢查工具對程式碼進行類型檢查。

  • 舉例:

    def func(a: int) -> list[int]:
    

Python 風格規範 (Style Rules)

1. 不要在行尾加分號(;),也不要用分號將兩條陳述(statements)放在同一行

2. 每行程式碼不超過 80 個字符

3. 謹慎使用括號

  • 舉例:
    • Yes
      #Yes: 
      if foo:
          bar()
      while x:
          x = bar()
      if x and y:
          bar()
      if not x:
          bar()
      # For a 1 item tuple the ()s are more visually obvious than the comma.
      onesie = (foo,)
      return foo
      return spam, beans
      return (spam, beans)
      for (x, y) in dict.items(): ...
      
    • No
      #No:  
      if (x):
          bar()
      if not(x):
          bar()
      return (foo)
      

4. 用4個空格(spaces)作為程式碼縮排

  • 建議使用 vscode 中的 Indent 設定

5. 頂級定義之間空兩行, 方法定義之間空一行

  • 頂級定義之間空兩行,比如 function 或者 class 定義。
  • method 定義,class 定義與第一個 method 之間,都應該空一行。
  • fuction 或 method 中某些地方要是你覺得合適,就空一行。

6. 按照標準的排版規範來使用標點兩邊的空格(space)

  • 舉例:
    • Yes
      spam(ham[1], {'eggs': 2}, [])
      
    • No
      spam( ham[ 1 ], { 'eggs': 2 }, [ ] )
      

7. 程式的的主要程式檔案(main) 的第一行為 Shebang(#!/usr/bin/python3)

Python風格規範 中的補充
在計算機科學中, Shebang (也稱為Hashbang)是一個由井號和歎號構成的字符串行(#!), 其出現在文本文件的第一行的前兩個字符. 在文件中存在Shebang的情況下, 類Unix操作系統的程序載入器會分析Shebang後的內容, 將這些內容作為解釋器指令, 並調用該指令, 並將載有Shebang的文件路徑作為該解釋器的參數. 例如, 以指令#!/bin/sh開頭的文件在執行時會實際調用/bin/sh程序
#!先用於幫助內核找到Python解釋器, 但是在導入模塊時, 將會被忽略. 因此只有被直接執行的文件中才有必要加入#!.

8. 對 module, function, method docstrings 和行內註釋(inline comments)使用正確的風格

9. String 使用及操作格式

  • 例如用 fstring% operator 或 format 取代對 str 使用 + 操作
  • 詳細規範請見:3.10 Strings

10. 在 files 和 sockets 結束時,使用顯式關閉

  • 推薦使用 with
    with open("hello.txt") as hello_file:
    for line in hello_file:
        print(line)
    

11. 短期解決方案臨時代碼使用TODO註釋

  • 格式:
    # TODO(kl@gmail.com): Use a "*" here for string repetition.
    # TODO(Zeke) Change this to use relations.
    

12. 每個 import 應該獨佔一行,除了 typingcollection.abc

  • 舉例:
    • Yes
      # Yes:
      from collections.abc import Mapping, Sequence
      import os
      import sys
      from typing import Any, NewType
      
    • No
      # No:
      import os, sys
      

13. 通常每個 statement 獨佔一行

  • 舉例:
    • Yes
      # Yes:
      if foo: bar(foo)
      
    • No
      # No:
      if foo: bar(foo)
      else:   baz(foo)
      try:               bar(foo)
      except ValueError: baz(foo)
      
      try:
        bar(foo)
      except ValueError: baz(foo)
      

14. 適時使用 getter 和 setter 函數

  • getter 和 setter 函數為物件導向程式設計中的概念。

15. 遵守變數命名規則

module_name, package_name, ClassName, method_name, ExceptionName, function_name, GLOBAL_CONSTANT_NAME, global_var_name, instance_var_name, function_parameter_name, local_var_name, query_proper_noun_for_thing, send_acronym_via_https.

16. 使用 Main

def main():
    ...

if __name__ == '__main__':
    main()

17. 盡量讓 function 保持精簡

18. 使用 type 註釋(Type Annotations)

  • 更多使用格式請見:[3.19 Type Annotations](3.19 Type Annotations)

yapf 使用

由於先前介紹的 Google Style 頗為複雜,我會建議先稍加熟記,使得程式碼撰寫時多半能按照規則,爾後在 git commit 前使用 YAPF 這一個工具來檢查程式碼風格是否正確。

下方內容參考 YAPF 的文檔。

安裝 yapf

  • 透過 PyPl 安裝
    pip install yapf
    
  • 在 Pipenv 建立的虛擬環境中安裝(Day-01中介紹的)
    pipenv install yapf --dev
    

如何執行 yapf

  • 完整說明

    usage: yapf [-h] [-v] [-d | -i | -q] [-r | -l START-END] [-e PATTERN]
                [--style STYLE] [--style-help] [--no-local-style] [-p]
                [-vv]
                [files ...]
    
    Formatter for Python code.
    
    positional arguments:
      files                 reads from stdin when no files are specified.
    
    optional arguments:
      -h, --help            show this help message and exit
      -v, --version         show program's version number and exit
      -d, --diff            print the diff for the fixed source
      -i, --in-place        make changes to files in place
      -q, --quiet           output nothing and set return value
      -r, --recursive       run recursively over directories
      -l START-END, --lines START-END
                            range of lines to reformat, one-based
      -e PATTERN, --exclude PATTERN
                            patterns for files to exclude from formatting
      --style STYLE         specify formatting style: either a style name (for
                            example "pep8" or "google"), or the name of a file
                            with style settings.  The default is pep8 unless a
                            .style.yapf or setup.cfg or pyproject.toml file
                            located in the same directory as the source or one
                            of its parent directories (for stdin, the current
                            directory is used).
      --style-help          show style settings and exit; this output can be
                            saved to .style.yapf to make your settings
                            permanent
      --no-local-style      don't search for local style definition
      -p, --parallel        run YAPF in parallel when formatting multiple
                            files. Requires concurrent.futures in Python 2.X
      -vv, --verbose        print out file names while processing
    
  • 使用 Google style 的範例

    yapf -i -r --style google
    

yapf 的效果範例

  • 執行前

    x = {  'a':37,'b':42,
    
    'c':927}
    
    y = 'hello ''world'
    z = 'hello '+'world'
    a = 'hello {}'.format('world')
    class foo  (     object  ):
      def f    (self   ):
        return       37*-+2
      def g(self, x,y=42):
          return y
    def f  (   a ) :
      return      37+-+a[42-x :  y**3]
    
  • 執行後

    x = {'a': 37, 'b': 42, 'c': 927}
    
    y = 'hello ' 'world'
    z = 'hello ' + 'world'
    a = 'hello {}'.format('world')
    
    
    class foo(object):
        def f(self):
            return 37 * -+2
    
        def g(self, x, y=42):
            return y
    
    
    def f(a):
        return 37 + -+a[42 - x:y**3]
    

寫的有些匆忙,如果文章有錯誤,歡迎指正~
/images/emoticon/emoticon41.gif


上一篇
[Day-03] 專案的 git branch 工作流程與 git commit message 格式(使用 Git Flow)
下一篇
[Day-05] 設計網路新聞的資料結構(使用 Python dataclass)
系列文
基於自然語言處理的新聞意見提取應用開發筆記17
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言