更好的組織代碼
- 2020 年 4 月 23 日
- 筆記
- 【Python】常用cookbook
總覽

項目結構
README.rst
LICENSE
setup.py
requirements.txt # 或者pipfile
sample/__init__.py
sample/core.py
sample/helpers.py
docs/conf.py
docs/index.rst
tests/test_basic.py
tests/test_advanced.py
核心代碼在./sample/
,如果核心的文件只有一個,可以直接放在項目根目錄下./sample.py
。
License: 開源許可,也可以不要開源許可發佈。
docs:包參考文檔。
tests: 單元測試。
Makefile:管理任務。
混亂的代碼結構
多重混亂的循環依賴
furn.py
from workers import Carpenter
class Table():
pass
class Chair():
pass
workers.py
from furn import Table,Chair
class Carpenter():
pass
如果非要使用循環依賴,那隻能使用不太好的方式來導入:在method和function中導入使用。
隱藏耦合
因為有太多關聯,每次修改Table的實現,都需要小心翼翼,容易造成Carpenter的代碼邏輯問題。
大量使用全局變量或上下文
不顯示的傳遞,而使用大量的全局狀態,如height, width, type, wood,這些全局狀態容易被代理快速的修改了。一旦被莫名修改後,還需要仔細的檢查,能夠訪問這些全局變量的地方(或者是遠程的代碼修改了這些全局的狀態)。
麵條式代碼(Spaghetti code)
源代碼的控制流程複雜,條件句和循環語句中互相嵌套,混亂而難以理解, 大量重複的代碼,沒有適當的分割,被視為套管程序。python的縮進特性使得它很難維護這樣的代碼,最好的方式就是不要寫太多這樣的代碼。
Python中更可能出現混沌代碼(Ravioli code)
這類代碼包含上百段相似的邏輯碎片,通常是缺乏合適結構的類或對象,如果寫代碼時弄不清具體的邏輯,就可能出現混沌代碼。
模塊
Python模塊是最主要的抽象層之一,抽象層允許將代碼分為不同部分,每個部分包含相關的數據與功能。
例如在項目中,一層控制用戶操作相關接口,另一層處理底層數據操作。
為了保持風格的一致,模塊名稱應該保持簡短,小寫,不要使用特殊字符等。不叫用.符號,影響python路徑查找(my.spam.py的模塊名稱誤讓python以為要找my文件夾下的spam)。
不要使用下劃線來組織命名空間,使用子模塊更好:
# OK
import library.plugin.foo
# not OK
import library.foo_plugin
python導入模塊原理
import modu
會從當前文件夾尋找modu.py文件,如未找到,python解釋器會從’path’中遞歸去尋找,仍然未找到,則拋出ImportError。
找到該模塊,解釋器會在隔離的範圍執行該模塊,任何頂級modu聲明都會被執行,包括其他的imports,模塊中的函數和類的定義存儲在模塊的字典中,在模塊命名空間的調用者,可以直接調用模塊中的變量,函數,和類。
在其他很多語言中,導入文件的邏輯是,解釋器會將該文件的代碼複製一份到調用的文件中,這與python是有很大的不同的。python中,導入的module在一個獨立的命名空間中,這表示,不需要擔心覆蓋了當前同名的函數等。
不要使用*導入所有的模塊:from modu import *
,使用import *
使代碼難以閱讀和且無法很好的區分依賴。使用 from modu import func
清晰的說明想導入具體的哪個模塊,將其放入全局的命名空間中。
糟糕的導入方法:
[...]
from modu import *
[...]
x = sqrt(4) # Is sqrt part of modu? A builtin? Defined above?
好一些的導入方法:
from modu import sqrt
[...]
x = sqrt(4) # sqrt may be part of modu, if not redefined in between
最好的示範:
import modu
[...]
x = modu.sqrt(4) # sqrt is visibly part of modu's namespace
packages
python提供了非常直觀的包系統,即簡單地將模塊管理機制擴展到一個目錄上。任何包含了__init__.py
文件的目錄,組成了python的包。
import pack.modu
首先找到pach目錄下的__init__.py
文件,執行所有頂層聲明,然後再找到pack/modu.py
文件,執行其所有頂層聲明。執行完這些所有的操作,modu.py
中定義的variable, function, class,在pack.modu的命名空間中都變成了可用狀態。
一個常見的問題是__init__.py
添加太多的代碼,當項目的結構越來越複雜,包含子包,子包有包含更深層次的包,當導入深層次中的包時,就需要執行很多的__init__.py
文件。
留空__init__.py
是最好的做法,如果子包或者更深層次的包不需要共享任何代碼時。
當要導入嵌套的生層次的包時,可以給包命個別名,之後使用別名,不使用冗長的包名
import very.deep.module as mod
面向對象
In Python, everything is an object。
Functions, classes, strings,其它任意類型都是對象,他們有類型,可以作為參數傳遞,有方法,有屬性。
選擇編程範式:
- 使用面向對象:當有對象(windows, buttons, avatars)需要相對長的生命周期在計算機的內存中時
- 使用純函數:
- 純函數的結果是確定的:給定一個輸入,輸出總是固定相同。
- 當需要重構或優化時,純函數更易於更改或替換。
- 純函數更容易做單元測試:很少需要複雜的上下文配置和之後的數據清除工作。
- 純函數更容易操作、修飾和分發。
裝飾器
裝飾器是一個函數或類,它可以 包裝(或裝飾)一個函數或方法。被’裝飾’的函數或方法會替換原來的函數或方法。
原始方法寫裝飾器
def foo():
# do something
def decorator(func):
# 操作函數
return func
foo = decorator(foo) # 手動裝飾
使用@decorators語法更清晰
@decorator
def bar():
# Do something
# bar() 是裝飾器
這個機制對於分離概念和避免外部不相關邏輯「污染」主要邏輯很有用處。
您需要在table中儲存一個函數的結果,並且下次能直接使用該結果,而不是再計算一次。這顯然不屬於函數的邏輯部分。
Context Managers
為人熟知的示例
with open('file.txt') as f:
contents = f.read()
兩種方式實現
使用class(處理簡單操作的情況建議用這種)
class CustomOpen(object):
def __init__(self, filename): # 首先被實例化
self.file = open(filename)
def __enter__(self): # 然後,調用enter,返回值在 as f 語句中被賦給 f
return self.file
def __exit__(self, ctx_type, ctx_value, ctx_traceback): #with塊中的代碼執行完i調用exit
self.file.close()
# 使用自定義的context
with CustomOpen('file') as f:
contents = f.read()
使用generator(封裝的邏輯量很大建議用這種)
from contextlib import contextmanager
@contextmanager
def custom_open(filename): # custom_open 函數一直運行到 yield 語句
f = open(filename)
try:
yield f # 運行到這裡將控制權返回給 with 語句
finally: # with塊代碼執行完後,執行finally
f.close()
with custom_open('file') as f: # 控制權到with後 as f 部分將yield的 f 賦值給f
contents = f.read()
動態類型
python是動態類型語言,變量沒有固定的類型。變量不是計算機內存中的一段,而是指向該類型對象的某個名稱或者tag。
例如:’a’ 設置指向value 1, 隨後設置指向 value ‘a string’, 隨後設置指向一個function.
要避免同一個變量指向不同的東西!
Bad
a = 1
a = 'a string'
def a():
pass # Do something
Good
count = 1
msg = 'a string'
def func():
pass # Do something
可變和不可變類型
可變類型:可變類型允許內容的內部修改,有對應使其變化的對象函數,如:lists、dictionaries
不可變類型:無對應使其變化的對象函數,無對應使其變化的對象函數:tuple、x=2(變量x指向2)
string也是不可變類型,要連接字符串有以下幾種方法:
最差:使用+
操作符,效率最差
nums = ""
for n in range(20):
nums += str(n)
print nums
好:使用append方法
nums = []
for n in range(20):
nums.append(str(n))
print "".join(nums)
更好: 列表推導式,使用join
nums = [str(n) for n in range(20)]
print "".join(nums)
使用 join() 並不總是最好的選擇, 要分情況:
foo = 'foo'
bar = 'bar'
foobar = foo + bar # 好,預先 確定數量的字符串創建一個新的字符串時,更快
foo += 'ooo' # 不好,添加到已存在字符串的情況下,使用join更好
foo = ''.join([foo, 'ooo'])
最好:使用map
nums = map(str, range(20))
print "".join(nums)
還可以使用格式化字符串連接確定數量的字符串字符:
foo = 'foo'
bar = 'bar'
foobar = '%s%s' % (foo, bar) # OK
foobar = '{0}{1}'.format(foo, bar) # better
foobar = '{foo}{bar}'.format(foo=foo, bar=bar) # best