五 函數式編程#
函數是 Python 內建支持的一種封裝,我們透過把大段代碼拆成函數,透過一層一層的函數調用,就可以把複雜任務分解成簡單的任務,這種分解可以稱之為面向過程的程序設計。函數就是面向過程的程序設計的基本單元
函數式編程(請注意多了個 “式” 字)——Functional Programming,雖然也可以歸結到面向過程的程序設計,但其思想更接近數學計算。
我們首先要搞明白計算機(Computer)和計算(Compute)的概念。
在計算機的層次上,CPU 執行的是加減乘除的指令代碼,以及各種條件判斷和跳轉指令,所以,匯編語言是最貼近計算機的語言。
而計算則指數學意義上的計算,越是抽象的計算,離計算機硬體越遠。
對應到編程語言,就是越低級的語言,越貼近計算機,抽象程度低,執行效率高,比如 C 語言;越高級的語言,越貼近計算,抽象程度高,執行效率低,比如 Lisp 語言。
函數式編程就是一種抽象程度很高的編程範式,純粹的函數式編程語言編寫的函數沒有變量,因此,任意一個函數,只要輸入是確定的,輸出就是確定的,這種純函數我們稱之為沒有副作用。而允許使用變量的程序設計語言,由於函數內部的變量狀態不確定,相同的輸入,可能得到不同的輸出,因此,這種函數是有副作用的。
函數式編程的一個特點就是,允許把函數本身作為參數傳入另一個函數,還允許返回一個函數!
Python 對函數式編程提供部分支持。由於 Python 允許使用變量,因此,Python 不是純函數式編程語言。
高階函數 (Higher-order function)
變量可以指向函數
以 Python 內置的求絕對值的函數 abs () 為例,調用該函數用以下代碼:
>>> abs(-10)
10
但是,如果只寫 abs 呢?
>>> abs
<built-in function abs>
可見,abs (-10) 是函數調用,而 abs 是函數本身。
要獲得函數調用結果,我們可以把結果賦值給變量:
>>> x = abs(-10)
>>> x
10
但是,如果把函數本身賦值給變量呢?
>>> f = abs
>>> f
<built-in function abs>
結論:函數本身也可以賦值給變量,即:變量可以指向函數。
如果一個變量指向了一個函數,那麼,可否通過該變量來調用這個函數?用代碼驗證一下:
>>> f = abs
>>> f(-10)
10
成功!說明變量 f 現在已經指向了 abs 函數本身。直接調用 abs () 函數和調用變量 f () 完全相同。
函數名也是變量
那麼函數名是什麼呢?函數名其實就是指向函數的變量!對於 abs () 這個函數,完全可以把函數名 abs 看成變量,它指向一個可以計算絕對值的函數!
如果把 abs 指向其他對象,會有什麼情況發生?
>>> abs = 10
>>> abs(-10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable
把 abs 指向 10 後,就無法通過 abs (-10) 調用該函數了!因為 abs 這個變量已經不指向求絕對值函數而是指向一個整數 10!
當然實際代碼絕對不能這麼寫,這裡是為了說明函數名也是變量。要恢復 abs 函數,請重啟 Python 交互環境。
注:由於 abs 函數實際上是定義在import builtins模塊中的,所以要讓修改 abs 變量的指向在其他模塊也生效,要用 import builtins; builtins.abs = 10。
傳入函數
既然變量可以指向函數,函數的參數能接收變量,那麼一個函數就可以接收另一個函數作為參數,這種函數就稱之為高階函數。
一個最簡單的高階函數:
def add(x, y, f):
return f(x) + f(y)
當我們調用 add (-5, 6, abs) 時,參數 x,y 和 f 分別接收 - 5,6 和 abs,根據函數定義,我們可以推導計算過程為:
x = -5
y = 6
f = abs
f(x) + f(y) ==> abs(-5) + abs(6) ==> 11
return 11
編寫高階函數,就是讓函數的參數能夠接收別的函數。
小結
把函數作為參數傳入,這樣的函數稱為高階函數,函數式編程就是指這種高度抽象的編程範式。
1.1 map/reduce
Python 內建了 map () 和 reduce () 函數。
map () 函數接收兩個參數,一個是函數,一個是Iterable,map 將傳入的函數依次作用到序列的每個元素,並把結果作為新的 Iterator返回。
舉例說明,比如我們有一個函數 f (x)=x2,要把這個函數作用在一個 list [1, 2, 3, 4, 5, 6, 7, 8, 9] 上,就可以用 map () 實現如下
現在,我們用 Python 代碼實現:
>>> def f(x):
... return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]
map () 傳入的第一個參數是 f,即函數對象本身。由於結果 r 是一個 Iterator,Iterator 是惰性序列,因此通過 list () 函數讓它把整個序列都計算出來並返回一個 list。
你可能會想,不需要 map () 函數,寫一個循環,也可以計算出結果:
L = []
for n in [1, 2, 3, 4, 5, 6, 7, 8, 9]:
L.append(f(n))
print(L)
的確可以,但是,從上面的循環代碼,能一眼看明白 “把 f (x) 作用在 list 的每一個元素並把結果生成一個新的 list” 嗎?
所以,map () 作為高階函數,事實上它把運算規則抽象了,因此,我們不但可以計算簡單的 f (x)=x2,還可以計算任意複雜的函數,比如,把這個 list 所有數字轉為字符串:
>>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
['1', '2', '3', '4', '5', '6', '7', '8', '9']
只需要一行代碼。
再看 reduce 的用法。reduce 把一個函數作用在一個序列 [x1, x2, x3, ...] 上,這個函數必須接收兩個參數,reduce 把結果繼續和序列的下一個元素做累積計算,其效果就是:
reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
比方說對一個序列求和,就可以用 reduce 實現:
>>> from functools import reduce
>>> def add(x, y):
... return x + y
...
>>> reduce(add, [1, 3, 5, 7, 9])
25
當然求和運算可以直接用 Python 內建函數 sum (),沒必要動用 reduce。
但是如果要把序列 [1, 3, 5, 7, 9] 變換成整數 13579,reduce 就可以派上用場:
>>> from functools import reduce
>>> def fn(x, y):
... return x * 10 + y
...
>>> reduce(fn, [1, 3, 5, 7, 9])
13579
這個例子本身沒多大用處,但是,如果考慮到字符串 str 也是一個序列,對上面的例子稍加改動,配合 map (),我們就可以寫出把 str 轉換為 int 的函數:
>>> from functools import reduce
>>> def fn(x, y):
... return x * 10 + y
...
>>> def char2num(s):
... digits = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
... return digits[s]
...
>>> reduce(fn, map(char2num, '13579'))
13579
整理成一個 str2int 的函數就是:
from functools import reduce
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def str2int(s):
def fn(x, y):
return x * 10 + y
def char2num(s):
return DIGITS[s]
return reduce(fn, map(char2num, s))
還可以用 lambda 函數進一步簡化成:
from functools import reduce
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def char2num(s):
return DIGITS[s]
def str2int(s):
return reduce(lambda x, y: x * 10 + y, map(char2num, s))
也就是說,假設 Python 沒有提供 int () 函數,你完全可以自己寫一個把字符串轉化為整數的函數,而且只需要幾行代碼!
1.2 filter
Python 內建的 filter () 函數用於過濾序列。
和 map () 類似,filter () 也接收一個函數和一個序列。和 map () 不同的是,filter () 把傳入的函數依次作用於每個元素,然後根據返回值是 True 還是 False 決定保留還是丟棄該元素。
例如,在一個 list 中,刪掉偶數,只保留奇數,可以這麼寫:
def is_odd(n):
return n % 2 == 1
list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
# 結果: [1, 5, 9, 15]
把一個序列中的空字符串刪掉,可以這麼寫:
def not_empty(s):
return s and s.strip()
list(filter(not_empty, ['A', '', 'B', None, 'C', ' ']))
# 結果: ['A', 'B', 'C']
可見用 filter () 這個高階函數,關鍵在於正確實現一個 “篩選” 函數。
注意到 filter () 函數返回的是一個Iterator,也就是一個惰性序列,所以要強迫 filter () 完成計算結果,需要用 list () 函數獲得所有結果並返回 list。
filter () 的作用是從一個序列中篩出符合條件的元素。由於 filter () 使用了惰性計算,所以只有在取 filter () 結果的時候,才會真正篩選並每次返回下一個篩出的元素。
1.3 sorted
排序算法
排序也是在程序中經常用到的算法。無論使用冒泡排序還是快速排序,排序的核心是比較兩個元素的大小。如果是數字,我們可以直接比較,但如果是字符串或者兩個 dict 呢?直接比較數學上的大小是沒有意義的,因此,比較的過程必須通過函數抽象出來。
Python 內置的sorted() 函數就可以對 list 進行排序:
>>> sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]
此外,sorted () 函數也是一個高階函數,它還可以接收一個 key 函數來實現自定義的排序,例如按絕對值大小排序:
>>> sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]
key 指定的函數將作用於 list 的每一個元素上,並根據 key 函數返回的結果進行排序。對比原始的 list 和經過 key=abs 處理過的 list:
list = [36, 5, -12, 9, -21]
keys = [36, 5, 12, 9, 21]
然後 sorted () 函數按照 keys 進行排序,並按照對應關係返回 list 相應的元素:
keys排序結果 => [5, 9, 12, 21, 36]
| | | | |
最終結果 => [5, 9, -12, -21, 36]
我們再看一個字符串排序的例子:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'])
['Credit', 'Zoo', 'about', 'bob']
默認情況下,對字符串排序,是按照 ASCII 的大小比較的,由於 'Z' < 'a',結果,大寫字母 Z 會排在小寫字母 a 的前面。
現在,我們提出排序應該忽略大小寫,按照字母序排序。要實現這個算法,不必對現有代碼大改動,只要我們能用一個 key 函數把字符串映射為忽略大小寫排序即可。忽略大小寫來比較兩個字符串,實際上就是先把字符串都變成大寫(或者都變成小寫),再比較。
這樣,我們給 sorted 傳入 key 函數,即可實現忽略大小寫的排序:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
['about', 'bob', 'Credit', 'Zoo']
要進行反向排序,不必改動 key 函數,可以傳入第三個參數reverse=True:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
['Zoo', 'Credit', 'bob', 'about']
從上述例子可以看出,高階函數的抽象能力是非常強大的,而且,核心代碼可以保持得非常簡潔。
小結
sorted () 也是一個高階函數。用 sorted () 排序的關鍵在於實現一個映射函數。
返回函數
函數作為返回值
高階函數除了可以接受函數作為參數外,還可以把函數作為結果值返回。
我們來實現一個可變參數的求和。通常情況下,求和的函數是這樣定義的:
def calc_sum(*args):
ax = 0
for n in args:
ax = ax + n
return ax
但是,如果不需要立刻求和,而是在後面的代碼中,根據需要再計算怎麼辦?可以不返回求和的結果,而是返回求和的函數:
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
當我們調用 lazy_sum () 時,返回的並不是求和結果,而是求和函數:
>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>
調用函數 f 時,才真正計算求和的結果:
>>> f()
25
在這個例子中,我們在函數 lazy_sum 中又定義了函數 sum,並且,內部函數 sum 可以引用外部函數 lazy_sum 的參數和局部變量,當 lazy_sum 返回函數 sum 時,相關參數和變量都保存在返回的函數中,這種稱為 “閉包(Closure)” 的程序結構擁有極大的威力。
請再注意一點,當我們調用 lazy_sum () 時,每次調用都會返回一個新的函數,即使傳入相同的參數:
>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1==f2
False
f1 () 和 f2 () 的調用結果互不影響。
閉包
條件
1 外部函數中定義了內部函數
2 外部函數有返回值
3 返回的值是內部函數名
4 內部函數引用了外部函數的變量
def 外部函數():
...
def 內部函數():
...
return 內部函數
注意到返回的函數在其定義內部引用了局部變量 args,所以,當一個函數返回了一個函數後,其內部的局部變量還被新函數引用,所以,閉包用起來簡單,實現起來可不容易。
另一個需要注意的問題是,返回的函數並沒有立刻執行,而是直到調用了f() 才執行。
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count()
在上面的例子中,每次循環,都創建了一個新的函數,然後,把創建的 3 個函數都返回了。
你可能認為調用 f1 (),f2 () 和 f3 () 結果應該是 1,4,9,但實際結果是:
>>> f1()
9
>>> f2()
9
>>> f3()
9
全部都是 9!原因就在於返回的函數引用了變量 i,但它並非立刻執行。等到 3 個函數都返回時,它們所引用的變量 i 已經變成了 3,因此最終結果為 9。
返回閉包時牢記一點:返回函數不要引用任何循環變量,或者後續會發生變化的變量
如果一定要引用循環變量怎麼辦?方法是再創建一個函數,用該函數的參數綁定循環變量當前的值,無論該循環變量後續如何更改,已綁定到函數參數的值不變:
def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被執行,因此i的當前值被傳入f()
return fs
再看看結果:
>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
4
>>> f3()
9
缺點是代碼較長,可利用 lambda 函數縮短代碼。
nonlocal
使用閉包,就是內層函數引用了外層函數的局部變量。如果只是讀外層變量的值,我們會發現返回的閉包函數調用一切正常:
def inc():
x = 0
def fn():
# 仅读取x的值:
return x + 1
return fn
f = inc()
print(f()) # 1
print(f()) # 1
但是,如果對外層變量賦值,由於 Python 解釋器會把 x 當作函數 fn () 的局部變量,它會報錯:
錯誤
def inc():
x = 0
def fn():
# nonlocal x
x = x + 1
return x
return fn
f = inc()
print(f()) # 1
print(f()) # 2
原因是 x 作為局部變量並沒有初始化,直接計算 x+1 是不行的。但我們其實是想引用 inc () 函數內部的 x,所以需要在 fn () 函數內部加一個 nonlocal x 的聲明。加上這個聲明後,解釋器把 fn () 的 x 看作外層函數的局部變量,它已經被初始化了,可以正確計算 x+1。
使用閉包時,對外層變量賦值前,需要先使用nonlocal聲明該變量不是當前函數的局部變量。
小結
1 一個函數可以返回一個計算結果,也可以返回一個函數。
2 返回一個函數時,牢記該函數並未執行,返回函數中不要引用任何可能會變化的變量。
匿名函數
當我們在傳入函數時,有些時候,不需要顯式地定義函數,直接傳入匿名函數更方便。
在 Python 中,對匿名函數提供了有限支持。還是以 map () 函數為例,計算 f (x)=x2 時,除了定義一個 f (x) 的函數外,還可以直接傳入匿名函數:
>>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
[1, 4, 9, 16, 25, 36, 49, 64, 81]
通過對比可以看出,匿名函數 lambda x: x * x 實際上就是:
def f(x):
return x * x
關鍵字 lambda 表示匿名函數,冒號前面的 x 表示函數參數。
匿名函數有個限制,就是只能有一個表達式,不用寫 return,返回值就是該表達式的結果。
用匿名函數有個好處,因為函數沒有名字,不必擔心函數名衝突。此外,匿名函數也是一個函數對象,也可以把匿名函數賦值給一個變量,再利用變量來調用該函數:
>>> f = lambda x: x * x
>>> f
<function <lambda> at 0x101c6ef28>
>>> f(5)
25
同樣,也可以把匿名函數作為返回值返回,比如:
def build(x, y):
return lambda: x * x + y * y
Python 對匿名函數的支持有限,只有一些簡單的情況下可以使用匿名函數。
裝飾器
特點
1 函數 A 是作為參數出現的(函數 B 就接受函數 A 作為參數)
2 要有閉包的特點
#定義一個裝飾器
def decorate(func):
a = 100
print('wrapper外層打印測試')
def wrapper():
func()
print('--------->刷漆')
print('--------->鋪地板', a)
print('--------->裝門')
print('wrapper加載完成......')
return wrapper
# 使用裝飾器
@decorate
def house():
print('我是毛坯房....')
'''
默認執行的:
1. house為被裝飾函數,
2. 將被裝飾函數作為參數傳給裝飾器decorate
3. 執行decorate函數
4. 將返回值又賦值給house
'''
print(house)
house() # wrapper()
# def house1():
# house()
# print('刷漆')
# print('鋪地板')
# 調用函數house
# house()
由於函數也是一個對象,而且函數對象可以被賦值給變量,所以,通過變量也能調用該函數。
>>> def now():
... print('2015-3-25')
...
>>> f = now
>>> f()
2015-3-25
函數對象有一個name 屬性(注意:是前後各兩個下劃線),可以拿到函數的名字:
>>> now.__name__
'now'
>>> f.__name__
'now'
現在,假設我們要增強 now () 函數的功能,比如,在函數調用前後自動打印日誌,但又不希望修改 now () 函數的定義,這種在代碼運行期間動態增加功能的方式,稱之為 “裝飾器”(Decorator)。
本質上,decorator 就是一個返回函數的高階函數
多層裝飾器
如果裝飾器是多層的,誰距離函數最近就優先使用哪個裝飾器
# 裝飾器
def zhuang1(func):
print('------->1 start')
def wrapper(*args, **kwargs):
func()
print('刷漆')
print('------->1 end')
return wrapper
def zhuang2(func):# func=house
print('------->2 start')
def wrapper(*args, **kwargs):
func()
print('鋪地板,裝門.....')
print('------->2 end')
return wrapper
@zhuang2
@zhuang1
def house(): # house = wrapper
print('我是毛坯房.....')
house()
輸出
------->1 start
------->1 end
------->2 start
------->1 end
我是毛坯房.....
刷漆
鋪地板,裝門.....
帶參數的裝飾器
'''
帶參數的裝飾器是三層的
最外層的函數負責接收裝飾器參數
裡面的內容還是原裝飾器的內容
# 裝飾器帶參數
def outer(a): # 第一層: 負責接收裝飾器的參數
print('------------1 start')
def decorate(func): # 第二層 : 負責接收函數的
print('------------2 start')
def wrapper(*args, **kwargs): # 第三層 負責接收函數的參數
func(*args)
print("---->鋪地磚{}塊".format(a))
print('------------2 end')
return wrapper # 返出來的是:第三層
print('------------1 end')
return decorate # 返出來的是:第二層
@outer(10)
def house(time):
print('我{}日期拿到房子的鑰匙,是毛坯房....'.format(time))
# @outer(100)
# def street():
# print('新修街道名字是:黑泉路')
house('2019-6-12')
# street()
在面向對象(OOP)的設計模式中,decorator 被稱為裝飾模式。OOP 的裝飾模式需要通過繼承和組合來實現,而 Python 除了能支持 OOP 的 decorator 外,直接從語法層次支持 decorator。Python 的 decorator 可以用函數實現,也可以用類實現。
偏函數
Python 的functools模塊提供了很多有用的功能,其中一個就是偏函數(Partial function)。要注意,這裡的偏函數和數學意義上的偏函數不一樣。
在介紹函數參數的時候,我們講到,通過設置參數的默認值,可以降低函數調用的難度。而偏函數也可以做到這一點。舉例如下:
int () 函數可以把字符串轉換為整數,當僅傳入字符串時,int () 函數默認按十進制轉換:
>>> int('12345')
12345
但 int () 函數還提供額外的 base 參數,默認值為 10。如果傳入 base 參數,就可以做 N 進制的轉換:
>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565
假設要轉換大量的二進制字符串,每次都傳入 int (x, base=2) 非常麻煩,於是,我們想到,可以定義一個 int2 () 的函數,默認把 base=2 傳進去:
def int2(x, base=2):
return int(x, base)
這樣,我們轉換二進制就非常方便了:
>>> int2('1000000')
64
>>> int2('1010101')
85
functools.partial 就是幫助我們創建一個偏函數的,不需要我們自己定義 int2 (),可以直接使用下面的代碼創建一個新的函數 int2:
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85
所以,簡單總結 functools.partial 的作用就是,把一個函數的某些參數給固定住(也就是設置默認值),返回一個新的函數,調用這個新函數會更簡單。
注意到上面的新的 int2 函數,仅仅是把 base 參數重新設定默認值為 2,但也可以在函數調用時傳入其他值:
>>> int2('1000000', base=10)
1000000
最後,創建偏函數時,實際上可以接收 函數對象、*args 和 **kw 這 3 個參數,當傳入:
int2 = functools.partial(int, base=2)
實際上固定了 int () 函數的關鍵字參數 base,也就是:
int2('10010')
相當於:
kw = { 'base': 2 }
int('10010', **kw)
當傳入:
max2 = functools.partial(max, 10)
實際上會把 10 作為 * args 的一部分自動加到左邊,也就是:
max2(5, 6, 7)
相當於:
args = (10, 5, 6, 7)
max(*args)
結果為 10。
小結
當函數的參數個數太多,需要簡化時,使用 functools.partial 可以創建一個新的函數,這個新函數可以固定住原函數的部分參數,從而在調用時更簡單。
六 模塊#
在 Python 中,一個.py 文件就稱之為一個模塊(Module)
使用模塊有什麼好處?
1 最大的好處是大大提高了代碼的可維護性。其次,編寫代碼不必從零開始。當一個模塊編寫完畢,就可以被其他地方引用。我們在編寫程序的時候,也經常引用其他模塊,包括 Python內置的模塊和來自第三方的模塊。
2 使用模塊還可以避免函數名和變量名衝突。相同名字的函數和變量完全可以分別存在不同的模塊中,因此,我們自己在編寫模塊時,不必考慮名字會與其他模塊衝突。但是也要注意,儘量不要與內置函數名字衝突
你也許還想到,如果不同的人編寫的模塊名相同怎麼辦?為了避免模塊名衝突,Python 又引入了按目錄來組織模塊的方法,稱為包(Package)。
舉個例子,一個 abc.py 的文件就是一個名字叫 abc 的模塊,一個 xyz.py 的文件就是一個名字叫 xyz 的模塊。假設我們的 abc 和 xyz 這兩個模塊名字與其他模塊衝突了,於是我們可以通過包來組織模塊,避免衝突。方法是選擇一個頂層包名,比如 mycompany,按照如下目錄存放:
mycompany
├─ __init__.py
├─ abc.py
└─ xyz.py
引入了包以後,只要頂層的包名不與別人衝突,那所有模塊都不會與別人衝突。現在,abc.py 模塊的名字就變成了 mycompany.abc,類似的,xyz.py 的模塊名變成了 mycompany.xyz。
mycompany
├─ web
│ ├─ __init__.py
│ ├─ utils.py
│ └─ www.py
├─ __init__.py
├─ abc.py
└─ utils.py
文件 www.py 的模塊名就是 mycompany.web.www,兩個文件 utils.py 的模塊名分別是 mycompany.utils 和 mycompany.web.utils。
mycompany.web 也是一個模塊,請指出該模塊對應的.py 文件。
!!自己創建模塊時要注意命名,不能和 Python 自帶的模塊名稱衝突。例如,系統自帶了 sys 模塊,自己的模塊就不可命名為 sys.py,否則將無法導入系統自帶的 sys 模塊。
總結
1 模塊是一組 Python 代碼的集合,可以使用其他模塊,也可以被其他模塊使用。
2 創建自己的模塊時,要注意:
2.1 模塊名要遵循 Python 變量命名規範,不要使用中文、特殊字符;
2.2 模塊名不要和系統模塊名衝突,最好先查看系統是否已存在該模塊,檢查方法是在 Python 交互環境執行 import abc,若成功則說明系統存在此模塊。
使用模塊
Python 本身就內置了很多非常有用的模塊,只要安裝完畢,這些模塊就可以立刻使用。
我們以内建的 sys 模塊為例,編寫一個 hello 的模塊:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
' a test module '
__author__ = 'Michael Liao'
import sys
def test():
args = sys.argv
if len(args)==1:
print('Hello, world!')
elif len(args)==2:
print('Hello, %s!' % args[1])
else:
print('Too many arguments!')
if __name__=='__main__':
test()
第 1 行和第 2 行是標準註釋,第 1 行註釋可以讓這個 hello.py 文件直接在 Unix/Linux/Mac 上運行,第 2 行註釋表示.py 文件本身使用標準 UTF-8 編碼;
第 4 行是一個字符串,表示模塊的文檔註釋,任何模塊代碼的第一個字符串都被視為模塊的文檔註釋;
第 6 行使用__author__變量把作者寫進去,這樣當你公開源代碼後別人就可以瞻仰你的大名;
以上就是 Python 模塊的標準文件模板,當然也可以全部刪掉不寫,但是,按標準辦事肯定沒錯。
後面開始就是真正的代碼部分。
你可能注意到了,使用 sys 模塊的第一步,就是導入該模塊:
import sys
導入 sys 模塊後,我們就有了變量 sys 指向該模塊,利用 sys 這個變量,就可以訪問 sys 模塊的所有功能。
sys 模塊有一個argv 變量,用 list 存儲了命令行的所有參數。argv至少有一個元素,因為第一個參數永遠是該.py 文件的名稱。
例如:
運行 python3 hello.py 獲得的 sys.argv 就是 ['hello.py'];
運行 python3 hello.py Michael 獲得的 sys.argv 就是 ['hello.py', 'Michael']。
最後,注意到這兩行代碼:
if __name__=='__main__':
test()
當我們在命令行運行 hello 模塊文件時,Python 解釋器把一個特殊變量__name__置為__main__,而如果在其他地方導入該 hello 模塊時,if 判斷將失敗,因此,這種 if 測試可以讓一個模塊通過命令行運行時執行一些額外的代碼,最常見的就是運行測試。
我們可以用命令行運行 hello.py 看看效果:
$ python3 hello.py
Hello, world!
$ python hello.py Michael
Hello, Michael!
如果啟動 Python 交互環境,再導入 hello 模塊:
$ python3
Python 3.4.3 (v3.4.3:9b73f1c3e601, Feb 23 2015, 02:52:03)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import hello
>>>
導入時,沒有打印 Hello, word!,因為沒有執行 test () 函數。
調用 hello.test () 時,才能打印出 Hello, word!:
>>> hello.test()
Hello, world!
作用域
在一個模塊中,我們可能會定義很多函數和變量,但有的函數和變量我們希望給別人使用,有的函數和變量我們希望僅僅在模塊內部使用。在 Python 中,是通過 _前綴 來實現的。
1 正常的函數和變量名是公開的(public),可以被直接引用,比如:abc,x123,PI 等;
2 類似__xxx__這樣的變量是特殊變量,可以被直接引用,但是有特殊用途,比如上面的__author__,__name__就是特殊變量,hello 模塊定義的文檔註釋也可以用特殊變量__doc__訪問,我們自己的變量一般不要用這種變量名;
3 類似_xxx 和__xxx 這樣的函數或變量就是非公開的(private),不應該被直接引用,比如_abc,__abc 等;
之所以我們說,private 函數和變量 “不應該” 被直接引用,而不是 “不能” 被直接引用,是因為 Python 並沒有一種方法可以完全限制訪問 private 函數或變量,但是,從編程習慣上不應該引用 private 函數或變量。
private 函數或變量不應該被別人引用,那它們有什麼用呢?請看例子:
def _private_1(name):
return 'Hello, %s' % name
def _private_2(name):
return 'Hi, %s' % name
def greeting(name):
if len(name) > 3:
return _private_1(name)
else:
return _private_2(name)
我們在模塊裡公開 greeting () 函數,而把內部邏輯用 private 函數隱藏起來了,這樣,調用 greeting () 函數不用關心內部的 private 函數細節,這也是一種非常有用的代碼封裝和抽象的方法
即:?外部不需要引用的函數全部定義成 private,只有外部需要引用的函數才定義為 public。
安裝第三方模塊
在 Python 中,安裝第三方模塊,是通過包管理工具pip完成的。
一般來說,第三方庫都會在 Python 官方的 pypi.python.org 網站註冊,要安裝一個第三方庫,必須先知道該庫的名稱,可以在官網或者 pypi 上搜索,比如 Pillow 的名稱叫 Pillow,因此,安裝 Pillow 的命令就是:
pip install Pillow
模塊搜索路徑
當我們試圖加載一個模塊時,Python 會在指定的路徑下搜索對應的.py 文件,如果找不到,就會報錯:
>>> import mymodule
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named mymodule
默認情況下,Python 解釋器會搜索當前目錄、所有已安裝的內置模塊和第三方模塊,搜索路徑存放在 sys 模塊的 path 變量中:
>>> import sys
>>> sys.path
如果我們要添加自己的搜索目錄,有兩種方法:
一是直接修改 sys.path,添加要搜索的目錄:
>>> import sys
>>> sys.path.append('/Users/michael/my_py_scripts')
這種方法是在運行時修改,運行結束後失效。
二是設置環境變量 PYTHONPATH,該環境變量的內容會被自動添加到模塊搜索路徑中。設置方式與設置 Path 環境變量類似。注意只需要添加你自己的搜索路徑,Python 自己本身的搜索路徑不受影響。