­

更好的組織代碼

總覽

項目結構

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,其它任意類型都是對象,他們有類型,可以作為參數傳遞,有方法,有屬性。

選擇編程範式:

  1. 使用面向對象:當有對象(windows, buttons, avatars)需要相對長的生命周期在計算機的內存中時
  2. 使用純函數:
  • 純函數的結果是確定的:給定一個輸入,輸出總是固定相同。
  • 當需要重構或優化時,純函數更易於更改或替換。
  • 純函數更容易做單元測試:很少需要複雜的上下文配置和之後的數據清除工作。
  • 純函數更容易操作、修飾和分發。

裝飾器

裝飾器是一個函數或類,它可以 包裝(或裝飾)一個函數或方法。被’裝飾’的函數或方法會替換原來的函數或方法。

原始方法寫裝飾器

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