Python基礎入門筆記(二)

Jaybo發表於2019-03-01

前言

本文主要為 Python基礎入門筆記(一)內容的補充。

一、迭代器和生成器

1.1 Python迭代器

迭代器是一個可以記住遍歷的位置的物件。

迭代器物件從集合的第一個元素開始訪問,直到所有的元素被訪問完結束。

迭代器只能往前不會後退。

迭代器有兩個基本的方法:iter()next(),且字串、列表或元組物件都可用於建立迭代器,迭代器物件可以使用常規 for 語句進行遍歷,也可以使用 next() 函式來遍歷。

具體的例項:

# 1、字元創建立迭代器物件
str1 = 'jaybo'
iter1 = iter ( str1 )

# 2、list物件建立迭代器
list1 = [1,2,3,4]
iter2 = iter ( list1 )

# 3、tuple(元祖) 物件建立迭代器
tuple1 = ( 1,2,3,4 )
iter3 = iter ( tuple1 )

# for 迴圈遍歷迭代器物件
for x in iter1 :
    print ( x , end = ' ' )

print('\n------------------------')

# next() 函式遍歷迭代器
while True :
    try :
        print ( next ( iter3 ) )
    except StopIteration :
        break
複製程式碼

最後輸出的結果:

j a y b o
------------------------
1
2
3
4
複製程式碼

list(列表)生成式:

語法為:

[expr for iter_var in iterable] 
[expr for iter_var in iterable if cond_expr]
複製程式碼

第一種語法:首先迭代 iterable 裡所有內容,每一次迭代,都把 iterable 裡相應內容放到iter_var 中,再在表示式中應用該 iter_var 的內容,最後用表示式的計算值生成一個列表。

第二種語法:加入了判斷語句,只有滿足條件的內容才把 iterable 裡相應內容放到 iter_var 中,再在表示式中應用該 iter_var 的內容,最後用表示式的計算值生成一個列表。

例項,用一句程式碼列印九九乘法表:

print('\n'.join([' '.join ('%dx%d=%2d' % (x,y,x*y)  for x in range(1,y+1)) for y in range(1,10)]))
複製程式碼

輸出結果:

1x1= 1
1x2= 2 2x2= 4
1x3= 3 2x3= 6 3x3= 9
1x4= 4 2x4= 8 3x4=12 4x4=16
1x5= 5 2x5=10 3x5=15 4x5=20 5x5=25
1x6= 6 2x6=12 3x6=18 4x6=24 5x6=30 6x6=36
1x7= 7 2x7=14 3x7=21 4x7=28 5x7=35 6x7=42 7x7=49
1x8= 8 2x8=16 3x8=24 4x8=32 5x8=40 6x8=48 7x8=56 8x8=64
1x9= 9 2x9=18 3x9=27 4x9=36 5x9=45 6x9=54 7x9=63 8x9=72 9x9=81
複製程式碼

1.2 生成器

在 Python 中,使用了 yield 的函式被稱為生成器(generator)。

跟普通函式不同的是,生成器是一個返回迭代器的函式,只能用於迭代操作,更簡單點理解生成器就是一個迭代器。

在呼叫生成器執行的過程中,每次遇到 yield 時函式會暫停並儲存當前所有的執行資訊,返回 yield 的值。並在下一次執行 next() 方法時從當前位置繼續執行。

①建立:

生成器的建立:最簡單最簡單的方法就是把一個列表生成式的 [] 改成 ()

gen= (x * x for x in range(10))
print(gen)
複製程式碼

輸出結果:

generator object  at 0x0000000002734A40
複製程式碼

建立 List 和 generator 的區別僅在於最外層的 [] 和 () 。但是生成器並不真正建立數字列表, 而是返回一個生成器,這個生成器在每次計算出一個條目後,把這個條目“產生” ( yield ) 出來。 生成器表示式使用了“惰性計算” ( lazy evaluation,也有翻譯為“延遲求值”,我以為這種按需呼叫 call by need 的方式翻譯為惰性更好一些),只有在檢索時才被賦值( evaluated ),所以在列表比較長的情況下使用記憶體上更有效。

②以函式形式實現生成器:

其實生成器也是一種迭代器,但是你只能對其迭代一次。這是因為它們並沒有把所有的值存在記憶體中,而是在執行時生成值。你通過遍歷來使用它們,要麼用一個“for”迴圈,要麼將它們傳遞給任意可以進行迭代的函式和結構。而且實際運用中,大多數的生成器都是通過函式來實現的。

生成器和函式的不同:

函式是順序執行,遇到 return 語句或者最後一行函式語句就返回。而變成 generator 的函式,在每次呼叫 next() 的時候執行,遇到 yield語句返回,再次執行時從上次返回的 yield 語句處繼續執行。

舉個例子:

def odd():
    print ( 'step 1' )
    yield ( 1 )
    print ( 'step 2' )
    yield ( 3 )
    print ( 'step 3' )
    yield ( 5 )

o = odd()
print( next( o ) ) 
print( next( o ) ) 
print( next( o ) )
複製程式碼

輸出結果:

step 1
1
step 2
3
step 3
5
複製程式碼

可以看到,odd 不是普通函式,而是 generator,在執行過程中,遇到 yield 就中斷,下次又繼續執行。執行 3 次 yield 後,已經沒有 yield 可以執行了,如果你繼續列印 print( next( o ) ) ,就會報錯的。所以通常在 generator 函式中都要對錯誤進行捕獲。

列印楊輝三角:

def triangles( n ):         # 楊輝三角形
    L = [1]
    while True:
        yield L
        L.append(0)
        L = [ L [ i -1 ] + L [ i ] for i in range (len(L))]

n= 0
for t in triangles( 10 ):   # 直接修改函式名即可執行
    print(t)
    n = n + 1
    if n == 10:
        break
複製程式碼

輸出結果:

[1]
[1, 1]
[1, 2, 1]
[1, 3, 3, 1]
[1, 4, 6, 4, 1]
[1, 5, 10, 10, 5, 1]
[1, 6, 15, 20, 15, 6, 1]
[1, 7, 21, 35, 35, 21, 7, 1]
[1, 8, 28, 56, 70, 56, 28, 8, 1]
[1, 9, 36, 84, 126, 126, 84, 36, 9, 1]
複製程式碼

1.3 延伸

①反向迭代

使用 Python 中有內建的函式 reversed()

要注意一點就是:反向迭代僅僅當物件的大小可預先確定或者物件實現了 __reversed__() 的特殊方法時才能生效。 如果兩者都不符合,那你必須先將物件轉換為一個列表才行。

②同時迭代多個序列

為了同時迭代多個序列,使用 zip() 函式,具體示例:

names = ['jaychou', 'zjl', '周杰倫']
ages = [18, 19, 20]
for name, age in zip(names, ages):
     print(name,age)
複製程式碼

輸出的結果:

jaychou 18
zjl 19
周杰倫 20
複製程式碼

其實 zip(a, b) 會生成一個可返回元組 (x, y) 的迭代器,其中 x 來自 a,y 來自 b。 一旦其中某個序列到底結尾,迭代宣告結束。 因此迭代長度跟引數中最短序列長度一致。注意理解這句話,也就是說如果 a , b 的長度不一致的話,以最短的為標準,遍歷完後就結束。

二、模組與包

2.1 模組

2.1.1 什麼是模組

在 Python 中,一個 .py 檔案就稱之為一個模組(Module)。

我們學習過函式,知道函式是實現一項或多項功能的一段程式 。其實模組就是函式功能的擴充套件。為什麼這麼說呢?那是因為模組其實就是實現一項或多項功能的程式塊。

通過上面的定義,不難發現,函式和模組都是用來實現功能的,只是模組的範圍比函式廣,在模組中,可以有多個函式。

模組的好處:

  • 模組使用的最大好處是大大提高了程式碼的可維護性,當然,還提高了程式碼的複用性。

  • 使用模組還可以避免函式名和變數名衝突,相同名字的變數完全可以分別存在不同的模組中。

    PS:但是也要注意,變數的名字儘量不要與內建函式名字衝突。常見的內建函式:連結直達

再這也順帶先延伸下關於包的內容吧:

當編寫的模組多了,模組的名字重複的概率就增加了。如何解決這個問題呢?

Python 引入了按目錄來組織模組,稱為包(Package),比如:

extensions
├─ __init__.py
├─ dog.py
└─ cat.py
複製程式碼

現在 dog.py 模組的名字就變成了 extensions.dog

PS:請注意,每一個 package 目錄下面都會有一個__init__.py 的檔案,這個檔案是必須有的,否則, Python 就把這個目錄當成普通目錄,而不是一個 package directory。

另外如何使用包中的模組(Module)呢?如下編寫一個dog.py模組:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

' a test module '

__author__ = 'jack guo'

import sys

def shout():
    args = sys.argv
    if len(args)==1:
        print('Hello, I'm afei, welcome to world!')
    elif len(args)==2:
        print('Hello, %s!' % args[1])
   else:
        print('Yes,sir')

if __name__=='__main__':
    shout()
複製程式碼

解釋下:

第1行註釋可以讓dog.py檔案直接在linux上執行;
第2行註釋表示.py檔案本身使用標準UTF-8編碼;
第4行表示模組的文件註釋;
第6行表示模組的作者;

注意最後兩行程式碼,當我們除錯dog.py時,shout()會呼叫,當在其他模組匯入dog.py時,shout()不執行。
複製程式碼

模組的一種標準模板:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

' a test module '

__author__ = 'jack guo'
複製程式碼

以上是模組的標準模板,當然,你也可以不這樣做。

2.1.2 模組的匯入

匯入模組我們使用關鍵字 import,語法格式如下:import module1[, module2[,... moduleN]

如:import math 匯入標準模組中的 math 模組。

一個模組只會被匯入一次,不管你執行了多少次 import。這樣可以防止匯入模組被一遍又一遍地執行。

Python 直譯器是怎樣找到對應的檔案的呢?

搜尋路徑:由一系列目錄名組成的。Python 直譯器就依次從這些目錄中去尋找所引入的模組。這看起來很像環境變數,事實上,也可以通過定義環境變數的方式來確定搜尋路徑。搜尋路徑是在 Python 編譯或安裝的時候確定的,安裝新的庫應該也會修改。搜尋路徑被儲存在 sys 模組中的 path 變數 。可以列印出來:

import sys

print(sys.path)
複製程式碼

2.1.3 匯入模組中的屬性和方法及呼叫

①匯入模組的方法

  • import 模組名
  • import 模組名 as 新名字
  • from 模組名 import 函式名:大型專案中應儘量避免使用此方法,除非你非常確定不會造成命名衝突;它有一個好處就是可直接使用function()而不用加module.function()了。

PS1:匯入模組並不意味著在匯入時執行某些操作,它們主要用於定義,比如變數、函式和類等。

PS2:可以使用 from ··· import * 語句把某個模組中的所有方法屬性都匯入。

②模組中變數、函式以及類的屬性和方法的呼叫

  1. module.variable
  2. module.function()
  3. module.class.variable

2.1.4 模組的搜尋路徑sys模組的使用)

(1)程式所在目錄

(2)標準庫的安裝路徑

(3)作業系統環境變數 PYTHONPATH 指向的路徑

  • 獲得當前 Python 搜尋路徑的方法:

    import sys
    print(sys.path)
    複製程式碼

    輸出:

    ['D:\\workspace_pycharm', 'D:\\workspace_pycharm', 'D:\\python-practice', 'D:\\devInstall\\devPython\\Python36\\python36.zip', 'D:\\devInstall\\devPython\\Python36\\DLLs', 'D:\\devInstall\\devPython\\Python36\\lib', 'D:\\devInstall\\devPython\\Python36', 'D:\\devInstall\\devPython\\Python36\\lib\\site-packages']
    複製程式碼
  • sys 模組的 argv 變數的用法:

    • sys 模組有一個 argv(argument values) 變數,用 list 儲存了命令列的所有引數。
    • argv 至少有一個元素,因為第一個元素永遠都是.py檔案的名稱。
    $ python solve.py 0    # 命令列語句
    # 獲得argv變數的值
    sys.argv = ['solve.py', '0']
    sys.argv[0] = 'solve.py'
    sys.argv[1] = '0'
    複製程式碼

2.1.5 主模組和非主模組

在 Python 函式中,如果一個函式呼叫了其他函式完成一項功能,我們稱這個函式為主函式,如果一個函式沒有呼叫其他函式,我們稱這種函式為非主函式。主模組和非主模組的定義也類似,如果一個模組被直接使用,而沒有被別人呼叫,我們稱這個模組為主模組,如果一個模組被別人呼叫,我們稱這個模組為非主模組。

怎麼區分主模組和非主模組呢?

可以利用 __name__屬性。如果一個屬性的值是 __main__ ,那麼就說明這個模組是主模組,反之亦然。但是要注意了:這個 __main__ 屬性只是幫助我們判斷是否是主模組,並不是說這個屬性決定他們是否是主模組,決定是否是主模組的條件只是這個模組有沒有被人呼叫。如下:

if __name__ == '__main__':
    print('main')
else:
    print('not main')
複製程式碼

如果輸出結果為 main 則該模組為主模組。

!!!補充: 在初學 Python 過程中,總能遇到 if __name__ == 'main'語句,我們正好來好好了解下。

先舉例子,假如 A.py 檔案內容如下:

def sayhello():
    print('Hello!')
print('Hi!')
print(__name__)
複製程式碼

輸出結果:

Hi!
__main__
複製程式碼

結果很簡單,說明在執行 A.py 本身檔案時,變數__name__的值是__main__

現有個 B.py 檔案,程式碼如下:

import A
A.sayhello()
print('End')
複製程式碼

可以看到,在 B.py 檔案中,模組 A 被匯入,執行結果如下:

Hi!
A
Hello!
End
複製程式碼

這裡涉及一些語句執行順序問題,在 B.py 檔案中,模組 A 中的 sayhello 函式是呼叫時才執行的,但是 A 中的 print 語句會立刻執行(因為沒有縮排,因此與def是平行級別的)。因此會先依次執行:

print('Hi!')
print(__name__)
複製程式碼

然後執行:

A.sayhello()
print('End')
複製程式碼

執行結果中Hi!對應於 A 模組中的 print('Hi!'),而結果 A 對應於 print(__name__),可見當在 B 檔案中呼叫 A 模組時,變數__name__的值由__main__變為了模組 A 的名字。

這樣的好處是我們可以在 A.py 檔案中進行一些測試,而避免在模組呼叫的時候產生干擾,比如將 A 檔案改為:

def sayhello():
    print('Hello!')
print('Hi!')
print(__name__)

if __name__ == '__main__':
    print('I am module A')
複製程式碼

再次單獨執行 A.py 檔案時,結果中會多出I am module A語句:

Hi!
__main__
I am module A
複製程式碼

而執行 B.py 檔案,即呼叫 A 模組時,卻不會顯示該語句:

Hi!
A
Hello!
End
複製程式碼

簡短總結下:

模組屬性__name__,它的值由 Python 直譯器設定。如果 Python 程式是作為主程式呼叫,其值就設為__main__,如果是作為模組被其他檔案匯入,它的值就是其檔名。

每個模組都有自己的私有符號表,所有定義在模組裡面的函式把它當做全域性符號表使用。

2.2 包

2.2.1 什麼是包

我們自己在編寫模組時,不必考慮名字會與其他模組衝突。但是也要注意,儘量不要與內建函式名字衝突。但是這裡也有個問題,如果不同的人編寫的模組名相同怎麼辦?為了避免模組名衝突,Python 又引入了按目錄來組織模組的方法,稱為包(Package)。

仔細觀察的人,基本會發現,每一個包目錄下面都會有一個 __init__.py 的檔案。這個檔案是必須的,否則,Python 就把這個目錄當成普通目錄,而不是一個包。 __init__.py 可以是空檔案,也可以有 Python 程式碼,因為 __init__.py 本身就是一個模組,而它對應的模組名就是它的包名。

2.2.2 包的定義和優點

  • Python 把同類的模組放在一個資料夾中統一管理,這個資料夾稱之為一個
  • 如果把所有模組都放在一起顯然不好管理,並且有命名衝突的可能。
  • 包其實就是把模組分門別類地存放在不同的資料夾,然後把各個資料夾的位置告訴Python。
  • Python 的包是按目錄來組織模組的,也可以有多級目錄,組成多級層次的包結構。

2.2.3 包的建立

  • 建立一個資料夾,用於存放相關的模組,資料夾的名字即為包的名字
  • 在資料夾中建立一個__init__.py的模組檔案,內容可以為空(普通資料夾和包的區別)。
  • 將相關模組放入資料夾中

2.3.4 包的存放路徑及包中模組的匯入與呼叫

①包的存放

  • 如果不想把相關的模組檔案放在所建立的資料夾中,那麼最好的選擇就是:放在預設的site-packages資料夾裡,因為它就是用來存放你的模組檔案的。
  • sys.path.append(‘模組的存放位置’)只是在執行時生效,執行結束後失效。
  • 將包的存放路徑加入使用者系統環境變數中的 PYTHONPYTH 中去,這樣在任何位置都可以呼叫包了(推薦)。

②包中模組的匯入

  1. import 包名.模組名
  2. import 包名.模組名 as 新名字
  3. from 包名 import 模組名

③包中模組的變數、函式以及類的屬性和方法的呼叫

  1. package.module.variable
  2. package.module.function()
  3. package.module.class.variable

2.3 作用域

學習過 Java 的同學都知道,Java 的類裡面可以給方法和屬性定義公共的( public )或者是私有的 ( private ),這樣做主要是為了我們希望有些函式和屬效能給別人使用或者只能內部使用。 通過學習 Python 中的模組,其實和 Java 中的類相似,那麼我們怎麼實現在一個模組中,有的函式和變數給別人使用,有的函式和變數僅僅在模組內部使用呢?

在 Python 中,是通過 _ 字首來實現的。正常的函式和變數名是公開的(public),可以被直接引用,比如:abcni12PI 等。

類似__xxx__這樣的變數是特殊變數,可以被直接引用,但是有特殊用途,比如上面的 __name__ 就是特殊變數,還有 __author__ 也是特殊變數,用來標明作者。注意,我們自己的變數一般不要用這種變數名;類似_xxx__xxx 這樣的函式或變數就是非公開的(private),不應該被直接引用,比如 _abc__abc 等.

注意:這裡是說不應該,而不是不能。因為 Python 種並沒有一種方法可以完全限制訪問 private 函式或變數,但是,從程式設計習慣上不應該引用 private 函式或變數。

三、物件導向

Python 對屬性的訪問控制是靠程式設計師自覺的。

我們也可以把方法看成是類的屬性的,那麼方法的訪問控制也是跟屬性是一樣的,也是沒有實質上的私有方法。一切都是靠程式設計師自覺遵守 Python 的程式設計規範。

3.1 類

3.1.1 方法的裝飾器

  • @classmethod:呼叫的時候直接使用類名類呼叫,而不是某個物件

  • @property:可以像訪問屬性一樣呼叫方法

class UserInfo:

    ...

    @classmethod
    def get_name(cls):
        return cls.lv

    @property
    def get_age(self):
        return self._age

    if __name__ == '__main__':   
        ...

        # 直接使用類名類呼叫,而不是某個物件
        print(UserInfo.lv)
        # 像訪問屬性一樣呼叫方法(注意看get_age是沒有括號的)
        print(userInfo.get_age)
複製程式碼

3.1.2 繼承

語法格式:

class ClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>
複製程式碼

當然上面的是單繼承,Python 也是支援多繼承的(注意: Java 是單繼承、多實現),具體的語法如下:

class ClassName(Base1,Base2,Base3):
    <statement-1>
    .
    .
    .
    <statement-N>
複製程式碼

多繼承有一點需要注意的:若是父類中有相同的方法名,而在子類使用時未指定,Python 在圓括號中父類的順序,從左至右搜尋 , 即方法在子類中未找到時,從左到右查詢父類中是否包含方法。

繼承的子類的好處:

  • 會繼承父類的屬性和方法
  • 可以自己定義,覆蓋父類的屬性和方法

3.1.3 多型

看個例子就好了:

class User(object):
    def __init__(self, name):
        self.name = name

    def printUser(self):
        print('Hello !' + self.name)

class UserVip(User):
    def printUser(self):
        print('Hello ! 尊敬的Vip使用者:' + self.name)

class UserGeneral(User):
    def printUser(self):
        print('Hello ! 尊敬的使用者:' + self.name)

def printUserInfo(user):
    user.printUser()

if __name__ == '__main__':
    userVip = UserVip('大金主')
    printUserInfo(userVip)
    userGeneral = UserGeneral('水貨')
    printUserInfo(userGeneral)
複製程式碼

輸出結果:

Hello ! 尊敬的Vip使用者:大金主
Hello ! 尊敬的使用者:水貨
複製程式碼

可以看到,userVip 和 userGeneral 是兩個不同的物件,對它們呼叫 printUserInfo 方法,它們會自動呼叫實際型別的 printUser 方法,作出不同的響應。這就是多型的魅力。

PS:有了繼承,才有了多型,也會有不同類的物件對同一訊息會作出不同的相應。

3.1.4 Python中的魔法方法

在 Python 中,所有以 "**" 雙下劃線包起來的方法,都統稱為"魔術方法"。比如我們接觸最多的 init__ 。魔術方法有什麼作用呢?

使用這些魔術方法,我們可以構造出優美的程式碼,將複雜的邏輯封裝成簡單的方法。

我們可以使用 Python 內建的方法 dir() 來列出類中所有的魔術方法。示例如下:

class User(object):
    pass


if __name__ == '__main__':
    print(dir(User()))
複製程式碼

輸出的結果:

Python基礎入門筆記(二)

可以看到,一個類的魔術方法還是挺多的,截圖沒有截全。不過我們只需要瞭解一些常見和常用的魔術方法就好了。

1、屬性的訪問控制

Python 沒有真正意義上的私有屬性。然後這就導致了對 Python 類的封裝性比較差。我們有時候會希望 Python 能夠定義私有屬性,然後提供公共可訪問的 get 方法和 set 方法。Python 其實可以通過魔術方法來實現封裝。

方法 說明
__getattr__(self, name) 該方法定義了你試圖訪問一個不存在的屬性時的行為。因此,過載該方法可以實現捕獲錯誤拼寫然後進行重定向,或者對一些廢棄的屬性進行警告。
__setattr__(self, name, value) 定義了對屬性進行賦值和修改操作時的行為。不管物件的某個屬性是否存在,都允許為該屬性進行賦值。有一點需要注意,實現 __setattr__ 時要避免"無限遞迴"的錯誤
__delattr__(self, name) __delattr____setattr__ 很像,只是它定義的是你刪除屬性時的行為。實現 __delattr__ 是同時要避免"無限遞迴"的錯誤
__getattribute__(self, name) __getattribute__ 定義了你的屬性被訪問時的行為,相比較,__getattr__ 只有該屬性不存在時才會起作用。因此,在支援 __getattribute__的 Python 版本,呼叫__getattr__ 前必定會呼叫 __getattribute____getattribute__ 同樣要避免"無限遞迴"的錯誤。

2、物件的描述器

一般來說,一個描述器是一個有“繫結行為”的物件屬性 (object attribute),它的訪問控制被描述器協議方法重寫。這些方法是 __get__()__set__()__delete__()。有這些方法的物件叫做描述器。

預設對屬性的訪問控制是從物件的字典裡面 (__dict__) 中獲取 (get) , 設定 (set) 和刪除 (delete) 。舉例來說, a.x 的查詢順序是 a.__dict__['x'],然後 type(a).__dict__['x'],然後找 type(a) 的父類 ( 不包括元類 (metaclass) )。如果查詢到的值是一個描述器,Python 就會呼叫描述器的方法來重寫預設的控制行為。這個重寫發生在這個查詢環節的哪裡取決於定義了哪個描述器方法。注意,只有在新式類中時描述器才會起作用。

至於新式類最大的特點就是所有類都繼承自 type 或者 object 的類。

在物件導向程式設計時,如果一個類的屬性有相互依賴的關係時,使用描述器來編寫程式碼可以很巧妙的組織邏輯。在 Django 的 ORM 中,models.Model 中的 InterField 等欄位,就是通過描述器來實現功能的。

看一個例子:

class User(object):
    def __init__(self, name='小明', sex='男'):
        self.sex = sex
        self.name = name

    def __get__(self, obj, objtype):
        print('獲取 name 值')
        return self.name

    def __set__(self, obj, val):
        print('設定 name 值')
        self.name = val

class MyClass(object):
    x = User('小明', '男')
    y = 5

if __name__ == '__main__':
    m = MyClass()
    print(m.x)

    print('\n')

    m.x = '大明'
    print(m.x)

    print('\n')

    print(m.x)

    print('\n')

    print(m.y)
複製程式碼

輸出結果:

獲取 name 值
小明


設定 name 值
獲取 name 值
大明


獲取 name 值
大明


5
複製程式碼

3、自定義容器(Container)

我們知道在 Python 中,常見的容器型別有:dict、tuple、list、string。其中也提到過可容器和不可變容器的概念。其中 tuple、string 是不可變容器,dict、list 是可變容器。

可變容器和不可變容器的區別在於,不可變容器一旦賦值後,不可對其中的某個元素進行修改。

那麼這裡先提出一個問題,這些資料結構就夠我們開發使用嗎?不夠的時候,或者說有些特殊的需求不能單單隻使用這些基本的容器解決的時候,該怎麼辦呢?

這個時候就需要自定義容器了,那麼具體我們該怎麼做呢?

功能 說明
自定義不可變容器型別 需要定義 __len____getitem__方法
自定義可變型別容器 在不可變容器型別的基礎上增加定義 __setitem____delitem__
自定義的資料型別需要迭代 需定義 __iter__
返回自定義容器的長度 需實現 __len__(self)
自定義容器可以呼叫 self[key],如果 key 型別錯誤,丟擲 TypeError,如果沒法返回 key對應的數值時,該方法應該丟擲 ValueError 需要實現 __getitem__(self, key)
當執行 self[key] = value 呼叫是 __setitem__(self, key, value)這個方法
當執行 del self[key] 方法 其實呼叫的方法是 __delitem__(self, key)
當你想你的容器可以執行 for x in container: 或者使用 iter(container) 需要實現 __iter__(self) ,該方法返回的是一個迭代器

還有很多魔術方法,比如運算子相關的模式方法,就不在該文展開了。

3.2 列舉類

3.2.1 什麼是列舉

舉例,直接看程式碼:

from enum import Enum

Month = Enum('Month1', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))

# 遍歷列舉型別
for name, member in Month.__members__.items():
    print(name, '---------', member, '----------', member.value)

# 直接引用一個常量
print('\n', Month.Jan)
複製程式碼

輸出結果:

Jan --------- Month1.Jan ---------- 1
Feb --------- Month1.Feb ---------- 2
Mar --------- Month1.Mar ---------- 3
Apr --------- Month1.Apr ---------- 4
May --------- Month1.May ---------- 5
Jun --------- Month1.Jun ---------- 6
Jul --------- Month1.Jul ---------- 7
Aug --------- Month1.Aug ---------- 8
Sep --------- Month1.Sep ---------- 9
Oct --------- Month1.Oct ---------- 10
Nov --------- Month1.Nov ---------- 11
Dec --------- Month1.Dec ---------- 12

Month.Jan
複製程式碼

可見,我們可以直接使用 Enum 來定義一個列舉類。上面的程式碼,我們建立了一個有關月份的列舉型別 Month,這裡要注意的是構造引數,第一個引數 Month 表示的是該列舉類的類名,第二個 tuple 引數,表示的是列舉類的值; 當然,列舉類通過 __members__ 遍歷它的所有成員的方法。

注意的一點是 , member.value 是自動賦給成員的 int 型別的常量,預設是從 1 開始的。而且 Enum 的成員均為單例(Singleton),並且不可例項化,不可更改。

3.2.2 自定義列舉型別

有時候我們需要控制列舉的型別,那麼我們可以 Enum 派生出自定義類來滿足這種需要。修改上面的例子:

from enum import Enum, unique

Enum('Month1', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))

# @unique 裝飾器可以幫助我們檢查保證沒有重複值
@unique
class Month1(Enum):
    Jan = 'January'
    Feb = 'February'
    Mar = 'March'
    Apr = 'April'
    May = 'May'
    Jun = 'June'
    Jul = 'July'
    Aug = 'August'
    Sep = 'September '
    Oct = 'October'
    Nov = 'November'
    Dec = 'December'

if __name__ == '__main__':
    print(Month1.Jan, '----------',
          Month1.Jan.name, '----------', Month1.Jan.value)
    for name, member in Month1.__members__.items():
        print(name, '----------', member, '----------', member.value)
複製程式碼

輸出結果:

Month1.Jan ---------- Jan ---------- January
Jan ---------- Month1.Jan ---------- January
Feb ---------- Month1.Feb ---------- February
Mar ---------- Month1.Mar ---------- March
Apr ---------- Month1.Apr ---------- April
May ---------- Month1.May ---------- May
Jun ---------- Month1.Jun ---------- June
Jul ---------- Month1.Jul ---------- July
Aug ---------- Month1.Aug ---------- August
Sep ---------- Month1.Sep ---------- September 
Oct ---------- Month1.Oct ---------- October
Nov ---------- Month1.Nov ---------- November
Dec ---------- Month1.Dec ---------- December
複製程式碼

4.2.3 列舉類的比較

因為列舉成員不是有序的,所以它們只支援通過標識(identity) 和相等性 (equality) 進行比較。下面來看看 ==is 的使用:

from enum import Enum

class User(Enum):
    Twowater = 98
    Liangdianshui = 30
    Tom = 12

Twowater = User.Twowater
Liangdianshui = User.Liangdianshui

print(Twowater == Liangdianshui, Twowater == User.Twowater)
print(Twowater is Liangdianshui, Twowater is User.Twowater)

try:
    print('\n'.join('  ' + s.name for s in sorted(User)))
except TypeError as err:
    print(' Error : {}'.format(err))
複製程式碼

輸出結果:

False True
False True
 Error : '<' not supported between instances of 'User' and 'User'
複製程式碼

可以看看最後的輸出結果,報了個異常,那是因為大於和小於比較運算子引發 TypeError 異常。也就是 Enum 類的列舉是不支援大小運算子的比較的。

但是使用 IntEnum 類進行列舉,就支援比較功能。

import enum

class User(enum.IntEnum):
    Twowater = 98
    Liangdianshui = 30
    Tom = 12
    
try:
    print('\n'.join(s.name for s in sorted(User)))
except TypeError as err:
    print(' Error : {}'.format(err))
複製程式碼

輸出結果:

Tom
Liangdianshui
Twowater
複製程式碼

通過輸出的結果可以看到,列舉類的成員通過其值得大小進行了排序。也就是說可以進行大小的比較。

3.3 元類

3.3.1 Python 中類也是物件

在大多數程式語言中,類就是一組用來描述如何生成一個物件的程式碼段。在 Python 中這一點也是一樣的。但是,Python 中的類有一點跟大多數的程式語言不同,在 Python 中,可以把類理解成也是一種物件。對的,這裡沒有寫錯,就是物件。

因為只要使用關鍵字 class,Python 直譯器在執行的時候就會建立一個物件。如:

class ObjectCreator(object):
    pass
複製程式碼

當程式執行這段程式碼的時候,就會在記憶體中建立一個物件,名字就是ObjectCreator。這個物件(類)自身擁有建立物件(類例項)的能力,而這就是為什麼它是一個類的原因。

3.3.2 使用type()動態建立類

因為類也是物件,所以我們可以在程式執行的時候建立類。Python 是動態語言。動態語言和靜態語言最大的不同,就是函式和類的定義,不是編譯時定義的,而是執行時動態建立的。在之前,我們先了瞭解下 type() 函式。

class Hello(object):
    def hello(self, name='Py'):
        print('Hello,', name)
複製程式碼

然後再另外一個模組引用 hello 模組,輸出相應資訊。(其中 type() 函式的作用是可以檢視一個型別和變數的型別。)

from com.strivebo.hello import Hello

h = Hello()
h.hello()

print(type(Hello))
print(type(h))
複製程式碼

輸出資訊:

Hello, Py
<class 'type'>
<class 'com.twowater.hello.Hello'>
複製程式碼

上面也提到過,type() 函式可以檢視一個型別或變數的型別,Hello 是一個 class ,它的型別就是 type ,而 h 是一個例項,它的型別就是 com.strivebo.hello.Hello。前面的 com.strivebo 是我的包名,hello 模組在該包名下。

在這裡還要細想一下,上面的例子中,我們使用 type() 函式檢視一個型別或者變數的型別。其中檢視了一個 Hello class 的型別,列印的結果是: <class 'type'>。其實 type() 函式不僅可以返回一個物件的型別,也可以建立出新的型別。class 的定義是執行時動態建立的,而建立 class 的方法就是使用 type() 函式。比如我們可以通過 type() 函式建立出上面例子中的 Hello 類,具體看下面的程式碼:

def printHello(self, name='Py'):
    # 定義一個列印 Hello 的函式
    print('Hello,', name)

# 建立一個 Hello 類
Hello = type('Hello', (object,), dict(hello=printHello))

# 例項化 Hello 類
h = Hello()
# 呼叫 Hello 類的方法
h.hello()
# 檢視 Hello class 的型別
print(type(Hello))
# 檢視例項 h 的型別
print(type(h))
複製程式碼

輸出結果:

Hello, Py
<class 'type'>
<class '__main__.Hello'>
複製程式碼

在這裡,需先了解下通過 type() 函式建立 class 物件的引數說明:

  1. class 的名稱,比如例子中的起名為 Hello
  2. 繼承的父類集合,注意 Python 支援多重繼承,如果只有一個父類,tuple 要使用單元素寫法;例子中繼承 object 類,因為是單元素的 tuple ,所以寫成 (object,)
  3. class 的方法名稱與函式繫結;例子中將函式 printHello 繫結在方法名 hello

具體的模式如下:type(類名, 父類的元組(針對繼承的情況,可以為空),包含屬性的字典(名稱和值))

好了,瞭解完具體的引數使用之外,我們看看輸出的結果,可以看到,通過 type() 函式建立的類和直接寫 class 是完全一樣的,因為 Python 直譯器遇到 class 定義時,僅僅是掃描一下 class 定義的語法,然後呼叫 type() 函式建立出 class 的。

3.3.3 什麼是元類

我們建立類的時候,大多數是為了建立類的例項物件。那麼元類呢?元類就是用來建立類的。也可以換個理解方式就是:元類就是類的類。

通過上面 type() 函式的介紹,我們知道可以通過 type() 函式建立類:MyClass = type('MyClass', (), {})

實際上 type() 函式是一個元類。type() 就是 Python 在背後用來建立所有類的元類。

那麼現在我們也可以猜到一下為什麼 type() 函式是 type 而不是 Type呢?

這可能是為了和 str 保持一致性,str 是用來建立字串物件的類,而 int 是用來建立整數物件的類。type 就是建立類物件的類。

可以看到,上面的所有東西,也就是所有物件都是通過類來建立的,那麼我們可能會好奇,__class____class__ 會是什麼呢?換個說法就是,建立這些類的類是什麼呢?

print(age.__class__.__class__)
print(name.__class__.__class__)
print(fu.__class__.__class__)
print(mEat.__class__.__class__)
複製程式碼

輸出結果:

<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>
複製程式碼

可以看出,把他們類的類列印結果。發現列印出來的 class 都是 type 。

一開始也提到了,元類就是類的類。也就是元類就是負責建立類的一種東西。你也可以理解為,元類就是負責生成類的。而 type 就是內建的元類。也就是 Python 自帶的元類。

3.3.4 自定義元類

連線起來就是:先定義 metaclass,就可以建立類,最後建立例項。

所以,metaclass 允許你建立類或者修改類。換句話說,你可以把類看成是 metaclass 建立出來的“例項”。

class MyObject(object):
    __metaclass__ = something…
[…]
複製程式碼

如果是這樣寫的話,Python 就會用元類來建立類 MyObject。當你寫下 class MyObject(object),但是類物件 MyObject 還沒有在記憶體中建立。Python 會在類的定義中尋找 __metaclass__ 屬性,如果找到了,Python 就會用它來建立類 MyObject,如果沒有找到,就會用內建的 type 函式來建立這個類。如果還不怎麼理解,看下下面的流程圖:

Python基礎入門筆記(二)

舉個例項:

class Foo(Bar):
    pass
複製程式碼

它的流程是怎樣的呢?

首先判斷 Foo 中是否有 __metaclass__ 這個屬性?如果有,Python 會在記憶體中通過 __metaclass__ 建立一個名字為 Foo 的類物件(注意,這裡是類物件)。如果 Python 沒有找到__metaclass__ ,它會繼續在 Bar(父類)中尋找__metaclass__ 屬性,並嘗試做和前面同樣的操作。如果 Python在任何父類中都找不到 __metaclass__,它就會在模組層次中去尋找__metaclass__ ,並嘗試做同樣的操作。如果還是找不到__metaclass__ ,Python 就會用內建的 type 來建立這個類物件。

其實 __metaclass__ 就是定義了 class 的行為。類似於 class 定義了 instance 的行為,metaclass 則定義了 class 的行為。可以說,class 是 metaclass 的 instance。

現在,我們基本瞭解了 __metaclass__ 屬性,但是,也沒講過如何使用這個屬性,或者說這個屬性可以放些什麼?

答案就是:可以建立一個類的東西。那麼什麼可以用來建立一個類呢?type,或者任何使用到 type 或者子類化 type 的東東都可以。

3.4.5 元類的作用

元類的主要目的就是為了當建立類時能夠自動地改變類。通常,你會為 API 做這樣的事情,你希望可以建立符合當前上下文的類。

假想一個很傻的例子,你決定在你的模組裡所有的類的屬性都應該是大寫形式。有好幾種方法可以辦到,但其中一種就是通過在模組級別設定__metaclass__ 。採用這種方法,這個模組中的所有類都會通過這個元類來建立,我們只需要告訴元類把所有的屬性都改成大寫形式就萬事大吉了。

總結:Python 中的一切都是物件,它們要麼是類的例項,要麼是元類的例項,除了 type。type 實際上是它自己的元類,在純 Python 環境中這可不是你能夠做到的,這是通過在實現層面耍一些小手段做到的。

四、執行緒與程式

執行緒和程式的概念我就不多贅述了。可自行網上搜尋查詢資料瞭解下。

直接看問題:在 Python 中我們要同時執行多個任務怎麼辦?

有兩種解決方案:

  1. 一種是啟動多個程式,每個程式雖然只有一個執行緒,但多個程式可以一塊執行多個任務。
  2. 還有一種方法是啟動一個程式,在一個程式內啟動多個執行緒,這樣,多個執行緒也可以一塊執行多個任務。

當然還有第三種方法,就是啟動多個程式,每個程式再啟動多個執行緒,這樣同時執行的任務就更多了,當然這種模型更復雜,實際很少採用。

總結一下就是,多工的實現有3種方式:

  • 多程式模式;
  • 多執行緒模式;
  • 多程式+多執行緒模式。

同時執行多個任務通常各個任務之間並不是沒有關聯的,而是需要相互通訊和協調,有時,任務 1 必須暫停等待任務 2 完成後才能繼續執行,有時,任務 3 和任務 4 又不能同時執行,所以,多程式和多執行緒的程式的複雜度要遠遠高於我們前面寫的單程式單執行緒的程式。

4.1 多執行緒程式設計

其實建立執行緒之後,執行緒並不是始終保持一個狀態的,其狀態大概如下:

  • New 建立
  • Runnable 就緒。等待排程
  • Running 執行
  • Blocked 阻塞。阻塞可能在 Wait Locked Sleeping
  • Dead 消亡

執行緒有著不同的狀態,也有不同的型別。大致可分為:

  • 主執行緒
  • 子執行緒
  • 守護執行緒(後臺執行緒)
  • 前臺執行緒

執行緒的建立:

Python 提供兩個模組進行多執行緒的操作,分別是 threadthreading

前者是比較低階的模組,用於更底層的操作,一般應用級別的開發不常用。

import time
import threading


class MyThread(threading.Thread):
    def run(self):
        for i in range(5):
            print('thread {}, @number: {}'.format(self.name, i))
            time.sleep(1)

def main():
    print("Start main threading")

    # 建立三個執行緒
    threads = [MyThread() for i in range(3)]
    # 啟動三個執行緒
    for t in threads:
        t.start()

    print("End Main threading")


if __name__ == '__main__':
    main()
複製程式碼

這塊的內容還有很多,由於該文重點還是為講解 Python 的基礎知識。執行緒和程式的內容更多還是到網上搜尋資料學習,亦或是日後有時間我再更新於此。

五、Python 正規表示式

正規表示式是一個特殊的字元序列,用於判斷一個字串是否與我們所設定的字元序列是否匹配,也就是說檢查一個字串是否與某種模式匹配。

Python 自 1.5 版本起增加了 re 模組,它提供 Perl 風格的正規表示式模式。re 模組使 Python 語言擁有全部的正規表示式功能。

如下程式碼:

# 設定一個常量
a = '學習Python不難'

# 判斷是否有 “Python” 這個字串,使用 PY 自帶函式

print('是否含有“Python”這個字串:{0}'.format(a.index('Python') > -1))
print('是否含有“Python”這個字串:{0}'.format('Python' in a))
複製程式碼

輸出結果:

是否含有“Python”這個字串:True
是否含有“Python”這個字串:True
複製程式碼

上面用 Python 自帶函式就能解決的問題,我們就沒必要使用正規表示式了,這樣做多此一舉。

直接舉個 Python 中正規表示式使用例子好了:找出字串中的所有小寫字母。

首先我們在 findall 函式中第一個引數寫正規表示式的規則,其中[a-z]就是匹配任何小寫字母,第二個引數只要填寫要匹配的字串就行了。具體如下:

import re

# 設定一個常量
a = '學習Python不難'

# 選擇 a 裡面的所有小寫英文字母

re_findall = re.findall('[a-z]', a)

print(re_findall)
複製程式碼

輸出結果:

['y', 't', 'h', 'o', 'n']
複製程式碼

這樣我們就拿到了字串中的所有小寫字母了。

補充:

  • 貪婪模式:它的特性是一次性地讀入整個字串,如果不匹配就吐掉最右邊的一個字元再匹配,直到找到匹配的字串或字串的長度為 0 為止。它的宗旨是讀儘可能多的字元,所以當讀到第一個匹配時就立刻返回。
  • 懶惰模式:它的特性是從字串的左邊開始,試圖不讀入字串中的字元進行匹配,失敗,則多讀一個字元,再匹配,如此迴圈,當找到一個匹配時會返回該匹配的字串,然後再次進行匹配直到字串結束。

關於正規表示式的更多的學習還是找網上資料看看吧。

六、閉包

通過解決一個需求問題來了解閉包。

這個需求是這樣的,我們需要一直記錄自己的學習時間,以分鐘為單位。就好比我學習了 2 分鐘,就返回 2 ,然後隔了一陣子,我學習了 10 分鐘,那麼就返回 12 ,像這樣把學習時間一直累加下去。

面對這個需求,我們一般都會建立一個全域性變數來記錄時間,然後用一個方法來新增每次的學習時間,通常都會寫成下面這個形式:

time = 0

def insert_time(min):
    time = time + min
    return  time

print(insert_time(2))
print(insert_time(10))
複製程式碼

其實,這個在 Python 裡面是會報錯的。會報如下錯誤:UnboundLocalError: local variable 'time' referenced before assignment

那是因為,在 Python 中,如果一個函式使用了和全域性變數相同的名字且改變了該變數的值,那麼該變數就會變成區域性變數,那麼就會造成在函式中我們沒有進行定義就引用了,所以會報該錯誤。

我們可以使用 global 關鍵字,具體修改如下:

time = 0

def insert_time(min):
    global  time
    time = time + min
    return  time

print(insert_time(2))
print(insert_time(10))
複製程式碼

輸出結果如下:

2
12
複製程式碼

可是啊,這裡使用了全域性變數,我們在開發中能儘量避免使用全域性變數的就儘量避免使用。因為不同模組,不同函式都可以自由的訪問全域性變數,可能會造成全域性變數的不可預知性。比如程式設計師甲修改了全域性變數 time 的值,然後程式設計師乙同時也對 time 進行了修改,如果其中有錯誤,這種錯誤是很難發現和更正的。

這時候我們使用閉包來解決一下,先直接看程式碼:

time = 0

def study_time(time):
    def insert_time(min):
        nonlocal  time
        time = time + min
        return time

    return insert_time

f = study_time(time)
print(f(2))
print(time)
print(f(10))
print(time)
複製程式碼

輸出結果如下:

2
0
12
0
複製程式碼

這裡最直接的表現就是全域性變數 time 至此至終都沒有修改過,這裡還是用了 nonlocal 關鍵字,表示在函式或其他作用域中使用外層(非全域性)變數。那麼上面那段程式碼具體的執行流程是怎樣的。我們可以看下下圖:

Python基礎入門筆記(二)

這種內部函式的區域性作用域中可以訪問外部函式區域性作用域中變數的行為,我們稱為: 閉包。 更加直接的表達方式就是,當某個函式被當成物件返回時,夾帶了外部變數,就形成了一個閉包。

有沒有什麼辦法來驗證一下這個函式就是閉包呢?

有的,所有函式都有一個 __closure__ 屬性,如果函式是閉包的話,那麼它返回的是一個由 cell 組成的元組物件。cell 物件的 cell_contents 屬性就是儲存在閉包中的變數。看程式碼:

ime = 0


def study_time(time):
    def insert_time(min):
        nonlocal  time
        time = time + min
        return time

    return insert_time


f = study_time(time)
print(f.__closure__)
print(f(2))
print(time)
print(f.__closure__[0].cell_contents)
print(f(10))
print(time)
print(f.__closure__[0].cell_contents)
複製程式碼

列印結果為:

(<cell at 0x0000000000410C48: int object at 0x000000001D6AB420>,)
2
0
2
12
0
12
複製程式碼

從列印結果可見,傳進來的值一直儲存在閉包的 cell_contents 中,因此,這也就是閉包的最大特點,可以將父函式的變數與其內部定義的函式繫結。就算生成閉包的父函式已經釋放了,閉包仍然存在。

閉包的過程其實好比類(父函式)生成例項(閉包),不同的是父函式只在呼叫時執行,執行完畢後其環境就會釋放,而類則在檔案執行時建立,一般程式執行完畢後作用域才釋放,因此對一些需要重用的功能且不足以定義為類的行為,使用閉包會比使用類佔用更少的資源,且更輕巧靈活。

七、裝飾器

7.1 什麼是裝飾器

通過一個需求,一步一步來了解 Python 裝飾器。首先有這麼一個輸出員工打卡資訊的函式:

def punch():
    print('暱稱:小明  部門:研發部 上班打卡成功')

punch()
複製程式碼

輸出的結果:

暱稱:小明  部門:研發部 上班打卡成功
複製程式碼

然後,產品反饋,不行啊,怎麼上班打卡沒有具體的日期,加上打卡的具體日期吧,這應該很簡單,分分鐘解決啦。好吧,那就直接新增列印日期的程式碼吧,如下:

import time

def punch():
    print(time.strftime('%Y-%m-%d', time.localtime(time.time())))
    print('暱稱:小明  部門:研發部 上班打卡成功')

punch()
複製程式碼

輸出的結果:

2018-01-09
暱稱:小明  部門:研發部 上班打卡成功
複製程式碼

這樣改是可以,可是這樣改是改變了函式的功能結構的,本身這個函式定義的時候就是列印某個員工的資訊和提示打卡成功,現在增加列印日期的程式碼,可能會造成很多程式碼重複的問題。比如,還有一個地方只需要列印員工資訊和打卡成功就行了,不需要日期,那麼你又要重寫一個函式嗎?而且列印當前日期的這個功能方法是經常使用的,是可以作為公共函式給各個模組方法呼叫的。當然,這都是作為一個整體專案來考慮的。

既然是這樣,我們可以使用函數語言程式設計來修改這部分的程式碼。因為通過之前的學習,我們知道 Python 函式有兩個特點,函式也是一個物件,而且函式裡可以巢狀函式,那麼修改一下程式碼變成下面這個樣子:

import time

def punch():
    print('暱稱:小明  部門:研發部 上班打卡成功')

def add_time(func):
    print(time.strftime('%Y-%m-%d', time.localtime(time.time())))
    func()

add_time(punch)
複製程式碼

輸出的結果:

2018-01-09
暱稱:小明  部門:研發部 上班打卡成功
複製程式碼

這樣是不是發現,這樣子就沒有改動 punch 方法,而且任何需要用到列印當前日期的函式都可以把函式傳進 add_time 就可以了。

使用函式程式設計是不是很方便,但是,我們每次呼叫的時候,我們都不得不把原來的函式作為引數傳遞進去,還能不能有更好的實現方式呢?有的,就是本文要介紹的裝飾器,因為裝飾器的寫法其實跟閉包是差不多的,不過沒有了自由變數,那麼這裡直接給出上面那段程式碼的裝飾器寫法,來對比一下,裝飾器的寫法和函數語言程式設計有啥不同。

import time

def decorator(func):
    def punch():
        print(time.strftime('%Y-%m-%d', time.localtime(time.time())))
        func()

    return punch

def punch():
    print('暱稱:小明  部門:研發部 上班打卡成功')

f = decorator(punch)
f()
複製程式碼

輸出的結果:

2018-01-09
暱稱:小明  部門:研發部 上班打卡成功
複製程式碼

通過程式碼,能知道裝飾器函式一般做這三件事:

  1. 接收一個函式作為引數
  2. 巢狀一個包裝函式, 包裝函式會接收原函式的相同引數,並執行原函式,且還會執行附加功能
  3. 返回巢狀函式

7.2 語法糖

我們看上面的程式碼可以知道, Python 在引入裝飾器 (Decorator) 的時候,沒有引入任何新的語法特性,都是基於函式的語法特性。這也就說明了裝飾器不是 Python 特有的,而是每個語言通用的一種程式設計思想。只不過 Python 設計出了 @ 語法糖,讓定義裝飾器,把裝飾器呼叫原函式再把結果賦值為原函式的物件名的過程變得更加簡單,方便,易操作,所以 Python 裝飾器的核心可以說就是它的語法糖。

那麼怎麼使用它的語法糖呢?很簡單,根據上面的寫法寫完裝飾器函式後,直接在原來的函式上加 @ 和裝飾器的函式名。如下:

import time

def decorator(func):
    def punch():
        print(time.strftime('%Y-%m-%d', time.localtime(time.time())))
        func()

    return punch

@decorator
def punch():
    print('暱稱:小明  部門:研發部 上班打卡成功')

punch()
複製程式碼

輸出結果:

2018-01-09
暱稱:小明  部門:研發部 上班打卡成功
複製程式碼

那麼這就很方便了,方便在我們的呼叫上,比如例子中的,使用了裝飾器後,直接在原本的函式上加上裝飾器的語法糖就可以了,本函式也無虛任何改變,呼叫的地方也不需修改。

不過這裡一直有個問題,就是輸出打卡資訊的是固定的,那麼我們需要通過引數來傳遞,裝飾器該怎麼寫呢?裝飾器中的函式可以使用 *args 可變引數,可是僅僅使用 *args 是不能完全包括所有引數的情況,比如關鍵字引數就不能了,為了能相容關鍵字引數,我們還需要加上 **kwargs

因此,裝飾器的最終形式可以寫成這樣:

import time

def decorator(func):
    def punch(*args, **kwargs):
        print(time.strftime('%Y-%m-%d', time.localtime(time.time())))
        func(*args, **kwargs)

    return punch

 
@decorator
def punch(name, department):
    print('暱稱:{0}  部門:{1} 上班打卡成功'.format(name, department))

@decorator
def print_args(reason, **kwargs):
    print(reason)
    print(kwargs)

punch('小明', '研發部')
print_args('小明', sex='男', age=99)
複製程式碼

輸出的結果:

2018-01-09
暱稱:小明  部門:研發部 上班打卡成功
2018-01-09
小明
{'sex': '男', 'age': 99}
複製程式碼

本文內容大部分來源:

相關文章