banner
Zoney

Zoney

一个励志成为Web安全工程师的女青年!

python 學習 3

五 函數式編程#

函數是 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

image

如果我們要添加自己的搜索目錄,有兩種方法:
一是直接修改 sys.path,添加要搜索的目錄:

>>> import sys
>>> sys.path.append('/Users/michael/my_py_scripts')

這種方法是在運行時修改,運行結束後失效。

二是設置環境變量 PYTHONPATH,該環境變量的內容會被自動添加到模塊搜索路徑中。設置方式與設置 Path 環境變量類似。注意只需要添加你自己的搜索路徑,Python 自己本身的搜索路徑不受影響。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。