Python進階

m0_53964559發表於2020-12-25

1521354-2f8b1795a6ad7832.png
Python進階框架

希望大家喜歡,點贊哦
首先感謝廖雪峰老師對於該課程的講解

一、函數語言程式設計

1.1 函數語言程式設計簡介

函數語言程式設計:一種程式設計正規化
函數語言程式設計特點:

  • 把計算是為函式而非指令
  • 純函數語言程式設計:不需要變數,沒有副作用,測試簡單
  • 支援高階函式,程式碼簡潔

Python支援的函數語言程式設計:

  • 不是純函數語言程式設計:允許有變數
  • 支援高階函式:函式也可作為變數傳入
  • 支援閉包:有了閉包就能返回函式
  • 有限度的支援匿名函式

1.2 高階函式

  • 變數可以指向函式
  • 函式名其實就是指向函式的變數

高階函式:能接收函式做引數的函式

  • 變數可以指向函式
  • 函式的引數可以接受變數
  • 一個函式可以接受另一個函式作為引數
  • 能接收函式做引數的函式就是高階函式

1.3 把函式作為引數

我們講了高階函式的概念,編寫一個簡單的高階函式:

def add(x, y, f):
    return f(x) + f(y)

如果傳入abs作為引數f的值:

add(-5, 9, abs)

根據函式的定義,函式執行的程式碼實際上是:

abs(-5) + abs(9)

由於引數 x, y 和 f 都可以任意傳入,如果 f 傳入其他函式,就可以得到不同的返回值。

1.4 map()函式

map()是 Python 內建的高階函式,它接收一個函式 f 和一個 list,並通過把函式 f 依次作用在 list 的每個元素上,得到一個新的 list 並返回。

例如,對於list [1, 2, 3, 4, 5, 6, 7, 8, 9]

如果希望把list的每個元素都作平方,就可以用map()函式:
因此,我們只需要傳入函式f(x)=x*x,就可以利用map()函式完成這個計算:

def f(x):
    return x*x
print map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])

輸出結果:

`[1, 4, 9, 10, 25, 36, 49, 64, 81]
注意:map()函式不改變原有的 list,而是返回一個新的 list。

利用map()函式,可以把一個 list 轉換為另一個 list,只需要傳入轉換函式。

由於list包含的元素可以是任何型別,因此,map() 不僅僅可以處理只包含數值的 list,事實上它可以處理包含任意型別的 list,只要傳入的函式f可以處理這種資料型別。

1.5 reduce()函式

reduce()函式也是Python內建的一個高階函式。reduce()函式接收的引數和 map()類似,一個函式 f,一個list,但行為和 map()不同,reduce()傳入的函式 f 必須接收兩個引數,reduce()對list的每個元素反覆呼叫函式f,並返回最終結果值。

例如,編寫一個f函式,接收x和y,返回x和y的和:

def f(x, y):
    return x + y

呼叫 reduce(f, [1, 3, 5, 7, 9])時,reduce函式將做如下計算:

先計算頭兩個元素:f(1, 3),結果為4;
再把結果和第3個元素計算:f(4, 5),結果為9;
再把結果和第4個元素計算:f(9, 7),結果為16;
再把結果和第5個元素計算:f(16, 9),結果為25;
由於沒有更多的元素了,計算結束,返回結果25。
上述計算實際上是對 list 的所有元素求和。雖然Python內建了求和函式sum(),但是,利用reduce()求和也很簡單。

reduce()還可以接收第3個可選引數,作為計算的初始值。如果把初始值設為100,計算:

reduce(f, [1, 3, 5, 7, 9], 100)
結果將變為125,因為第一輪計算是:

計算初始值和第一個元素:f(100, 1),結果為101。

1.6 filter()函式

filter()函式是 Python 內建的另一個有用的高階函式,filter()函式接收一個函式 f 和一個list,這個函式 f 的作用是對每個元素進行判斷,返回 True或 False,filter()根據判斷結果自動過濾掉不符合條件的元素,返回由符合條件元素組成的新list。

例如,要從一個list [1, 4, 6, 7, 9, 12, 17]中刪除偶數,保留奇數,首先,要編寫一個判斷奇數的函式:

def is_odd(x):
    return x % 2 == 1

然後,利用filter()過濾掉偶數:

filter(is_odd, [1, 4, 6, 7, 9, 12, 17])

結果:[1, 7, 9, 17]

利用filter(),可以完成很多有用的功能,例如,刪除 None 或者空字串:

def is_not_empty(s):
    return s and len(s.strip()) > 0
filter(is_not_empty, ['test', None, '', 'str', '  ', 'END'])

結果:['test', 'str', 'END']

注意: s.strip(rm) 刪除 s 字串中開頭、結尾處的 rm 序列的字元。

當rm為空時,預設刪除空白符(包括'\n', '\r', '\t', ' '),如下:

a = ' 123'
a.strip()
結果: '123'

1.7 自定義排序函式

Python內建的 sorted()函式可對list進行排序:
>>>sorted([36, 5, 12, 9, 21])
[5, 9, 12, 21, 36]
但 sorted()也是一個高階函式,它可以接收一個比較函式來實現自定義排序,比較函式的定義是,傳入兩個待比較的元素 x, y,如果 x 應該排在 y 的前面,返回 -1,如果 x 應該排在 y 的後面,返回 1。如果 x 和 y 相等,返回 0

因此,如果我們要實現倒序排序,只需要編寫一個reversed_cmp函式:

def reversed_cmp(x, y):
if x > y:
return -1
if x < y:
return 1
return 0
這樣,呼叫 sorted() 並傳入 reversed_cmp 就可以實現倒序排序:

>>> sorted([36, 5, 12, 9, 21], reversed_cmp)
[36, 21, 12, 9, 5]

sorted()也可以對字串進行排序,字串預設按照ASCII大小來比較:

>>> sorted(['bob', 'about', 'Zoo', 'Credit'])
['Credit', 'Zoo', 'about', 'bob']

'Zoo'排在'about'之前是因為'Z'的ASCII碼比'a'小。

1.8 返回函式

Python的函式不但可以返回int、str、list、dict等資料型別,還可以返回函式!

例如,定義一個函式 f(),我們讓它返回一個函式 g,可以這樣寫:

def f():
    print 'call f()...'
    # 定義函式g:
    def g():
        print 'call g()...'
    # 返回函式g:
    return g

仔細觀察上面的函式定義,我們在函式 f 內部又定義了一個函式 g。由於函式 g 也是一個物件,函式名 g 就是指向函式 g 的變數,所以,最外層函式 f 可以返回變數 g,也就是函式 g 本身

呼叫函式 f,我們會得到 f 返回的一個函式:

>>> x = f()   # 呼叫f()
call f()...
>>> x   # 變數x是f()返回的函式:
<function g at 0x1037bf320>
>>> x()   # x指向函式,因此可以呼叫
call g()...   # 呼叫x()就是執行g()函式定義的程式碼

請注意區分返回函式和返回值:

def myabs():
    return abs   # 返回函式
def myabs2(x):
    return abs(x)   # 返回函式呼叫的結果,返回值是一個數值

返回函式可以把一些計算延遲執行。例如,如果定義一個普通的求和函式:

def calc_sum(lst):
    return sum(lst)

呼叫calc_sum()函式時,將立刻計算並得到結果:

>>> calc_sum([1, 2, 3, 4])10
但是,如果返回一個函式,就可以“延遲計算”:

def calc_sum(lst):
    def lazy_sum():
        return sum(lst)
    return lazy_sum
# 呼叫calc_sum()並沒有計算出結果,而是返回函式:

>>> f = calc_sum([1, 2, 3, 4])
>>> f
<function lazy_sum at 0x1037bfaa0>
# 對返回的函式進行呼叫時,才計算出結果:

>>> f()
10

由於可以返回函式,我們在後續程式碼裡就可以決定到底要不要呼叫該函式。

1.9 閉包

在函式內部定義的函式和外部定義的函式是一樣的,只是他們無法被外部訪問:

def g():
    print 'g()...'

def f():
    print 'f()...'
    return g

將 g 的定義移入函式 f 內部,防止其他程式碼呼叫 g:

def f():
    print 'f()...'
    def g():
        print 'g()...'
    return g

但是,考察上一小節定義的 calc_sum 函式:

def calc_sum(lst):
    def lazy_sum():
        return sum(lst)
    return lazy_sum

注意: 發現沒法把 lazy_sum 移到 calc_sum 的外部,因為它引用了 calc_sum 的引數 lst。

像這種內層函式引用了外層函式的變數(引數也算變數),然後返回內層函式的情況,稱為閉包(Closure)。

閉包的特點是返回的函式還引用了外層函式的區域性變數,所以,要正確使用閉包,就要確保引用的區域性變數在函式返回後不能變。舉例如下:

# 希望一次返回3個函式,分別計算1x1,2x2,3x3:
def count():
    fs = []
    for i in range(1, 4):
        def f():
             return i*i
        fs.append(f)
    return fs

f1, f2, f3 = count()

你可能認為呼叫f1(),f2()和f3()結果應該是1,4,9,但實際結果全部都是 9(請自己動手驗證)。

原因就是當count()函式返回了3個函式時,這3個函式所引用的變數 i 的值已經變成了3。由於f1、f2、f3並沒有被呼叫,所以,此時他們並未計算 i*i,當 f1 被呼叫時:

>>> f1()
9     # 因為f1現在才計算i*i,但現在i的值已經變為3

因此,返回函式不要引用任何迴圈變數,或者後續會發生變化的變數。

正確寫法:

def count():
    fs = []
    for i in range(1, 4):
        def f(j):
            def g():
                return j*j
            return g
        r=f(i)
        fs.append(r)
    return fs

f1, f2, f3 = count()
print f1(), f2(), f3()

1.10 匿名函式

高階函式可以接收函式做引數,有些時候,我們不需要顯式地定義函式,直接傳入匿名函式更方便。
在Python中,對匿名函式提供了有限支援。還是以map()函式為例,計算 f(x)=x2 時,除了定義一個f(x)的函式外,還可以直接傳入匿名函式:

>>> 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,返回值就是該表示式的結果。
使用匿名函式,可以不必定義函式名,直接建立一個函式物件,很多時候可以簡化程式碼:

>>> sorted([1, 3, 9, 5, 0], lambda x,y: -cmp(x,y))
[9, 5, 3, 1, 0]
返回函式的時候,也可以返回匿名函式:
>>> myabs = lambda x: -x if x < 0 else x 
>>> myabs(-1)
1
>>> myabs(1)
1

1.11 decorator裝飾器

認識裝飾器:

  1. 裝飾器用來裝飾函式
  2. 返回一個函式物件
  3. 被裝飾函式識別符號指向的函式物件
  4. 語法糖 @deco

1.12 編寫無引數decorator

Python的 decorator 本質上就是一個高階函式,它接收一個函式作為引數,然後,返回一個新函式。
使用 decorator 用Python提供的 @ 語法,這樣可以避免手動編寫 f = decorate(f) 這樣的程式碼。
考察一個@log的定義:

def log(f):
    def fn(x):
        print 'call ' + f.__name__ + '()...'
        return f(x)
    return fn

對於階乘函式,@log工作得很好:

@log
def factorial(n):
    return reduce(lambda x,y: x*y, range(1, n+1))
print factorial(10)

結果:
call factorial()...
3628800
但是,對於引數不是一個的函式,呼叫將報錯:

@log
def add(x, y):
    return x + y
print add(1, 2)

結果:

Traceback (most recent call last):
  File "test.py", line 15, in <module>
    print add(1,2)
TypeError: fn() takes exactly 1 argument (2 given)

因為 add() 函式需要傳入兩個引數,但是 @log 寫死了只含一個引數的返回函式。
要讓 @log 自適應任何引數定義的函式,可以利用Python的 *args 和 **kw,保證任意個數的引數總是能正常呼叫:

def log(f):
    def fn(*args, **kw):
        print 'call ' + f.__name__ + '()...'
        return f(*args, **kw)
    return fn

現在,對於任意函式,@log 都能正常工作。

1.13 編寫帶引數decorator

考察上一節的 @log 裝飾器:

def log(f):
    def fn(x):
        print 'call ' + f.__name__ + '()...'
        return f(x)
    return fn

發現對於被裝飾的函式,log列印的語句是不能變的(除了函式名)。
如果有的函式非常重要,希望列印出'[INFO] call xxx()...',有的函式不太重要,希望列印出'[DEBUG] call xxx()...',這時,log函式本身就需要傳入'INFO'或'DEBUG'這樣的引數,類似這樣:

@log('DEBUG')
def my_func():
    pass

把上面的定義翻譯成高階函式的呼叫,就是:

my_func = log('DEBUG')(my_func)

上面的語句看上去還是比較繞,再展開一下:

log_decorator = log('DEBUG')
my_func = log_decorator(my_func)

上面的語句又相當於:

log_decorator = log('DEBUG')
@log_decorator
def my_func():
    pass

所以,帶引數的log函式首先返回一個decorator函式,再讓這個decorator函式接收my_func並返回新函式:

def log(prefix):
    def log_decorator(f):
        def wrapper(*args, **kw):
            print '[%s] %s()...' % (prefix, f.__name__)
            return f(*args, **kw)
        return wrapper
    return log_decorator

@log('DEBUG')
def test():
    pass
print test()
#執行結果:
[DEBUG] test()...
None

對於這種3層巢狀的decorator定義,你可以先把它拆開:

# 標準decorator:
def log_decorator(f):
    def wrapper(*args, **kw):
        print '[%s] %s()...' % (prefix, f.__name__)
        return f(*args, **kw)
    return wrapper
return log_decorator

# 返回decorator:
def log(prefix):
    return log_decorator(f)

拆開以後會發現,呼叫會失敗,因為在3層巢狀的decorator定義中,最內層的wrapper引用了最外層的引數prefix,所以,把一個閉包拆成普通的函式呼叫會比較困難。不支援閉包的程式語言要實現同樣的功能就需要更多的程式碼。

1.14 完善decorator

@decorator可以動態實現函式功能的增加,但是,經過@decorator“改造”後的函式,和原函式相比,除了功能多一點外,有沒有其它不同的地方?
在沒有decorator的情況下,列印函式名:
def f1(x):
pass
print f1.name
輸出: f1
有decorator的情況下,再列印函式名:
def log(f):
def wrapper(*args, *kw):
print 'call...'
return f(
args, *kw)
return wrapper
@log
def f2(x):
pass
print f2.name
輸出: wrapper
可見,由於decorator返回的新函式函式名已經不是'f2',而是@log內部定義的'wrapper'。這對於那些依賴函式名的程式碼就會失效。decorator還改變了函式的doc等其它屬性。如果要讓呼叫者看不出一個函式經過了@decorator的“改造”,就需要把原函式的一些屬性複製到新函式中:
def log(f):
def wrapper(
args, *kw):
print 'call...'
return f(
args, *kw)
wrapper.name = f.name
wrapper.doc = f.doc
return wrapper
這樣寫decorator很不方便,因為我們也很難把原函式的所有必要屬性都一個一個複製到新函式上,所以Python內建的functools可以用來自動化完成這個“複製”的任務:
import functools
def log(f):
@functools.wraps(f)
def wrapper(
args, *kw):
print 'call...'
return f(
args, *kw)
return wrapper
最後需要指出,由於我們把原函式簽名改成了(
args, **kw),因此,無法獲得原函式的原始引數資訊。即便我們採用固定引數來裝飾只有一個引數的函式:
def log(f):
@functools.wraps(f)
def wrapper(x):
print 'call...'
return f(x)
return wrapper
也可能改變原函式的引數名,因為新函式的引數名始終是 'x',原函式定義的引數名不一定叫 'x'。

1.15 偏函式

當一個函式有很多引數時,呼叫者就需要提供多個引數。如果減少引數個數,就可以簡化呼叫者的負擔。
比如,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可以把一個引數多的函式變成一個引數少的新函式,少的引數需要在建立時指定預設值,這樣,新函式呼叫的難度就降低了。

二、模組

2.1 模組和包的概念

在檔案系統中:

  • 包就是資料夾
  • 模組就是xxx.py
  • 包也可以有多級
    區分包和普通目錄:
    包下面有個init.py檔案,每層都必須要有

2.2 匯入模組

要使用一個模組,我們必須首先匯入該模組。Python使用import語句匯入一個模組。例如,匯入系統自帶的模組 math:
import math
你可以認為math就是一個指向已匯入模組的變數,通過該變數,我們可以訪問math模組中所定義的所有公開的函式、變數和類:
>>> math.pow(2, 0.5) # pow是函式1.4142135623730951

>>> math.pi # pi是變數3.141592653589793
如果我們只希望匯入用到的math模組的某幾個函式,而不是所有函式,可以用下面的語句:
from math import pow, sin, log 這樣,可以直接引用pow, sin,log` 這3個函式,但math的其他函式沒有匯入進來:

>>> pow(2, 10)
1024.0
>>> sin(3.14)
0.0015926529164868282

如果遇到名字衝突怎麼辦?比如math模組有一個log函式,logging模組也有一個log函式,如果同時使用,如何解決名字衝突?
如果使用import匯入模組名,由於必須通過模組名引用函式名,因此不存在衝突:
import math, logging
print math.log(10) # 呼叫的是math的log函式
logging.log(10, 'something') # 呼叫的是logging的log函式
如果使用 from...import 匯入 log 函式,勢必引起衝突。這時,可以給函式起個“別名”來避免衝突:
from math import log
from logging import log as logger # logging的log現在變成了logger

print log(10)   # 呼叫的是math的log
logger(10, 'import from logging')   # 呼叫的是logging的log

2.3 動態匯入模組

如果匯入的模組不存在,Python直譯器會報 ImportError 錯誤:

>>> import something
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named something

有的時候,兩個不同的模組提供了相同的功能,比如 StringIO 和 cStringIO 都提供了StringIO這個功能。
這是因為Python是動態語言,解釋執行,因此Python程式碼執行速度慢。
如果要提高Python程式碼的執行速度,最簡單的方法是把某些關鍵函式用 C 語言重寫,這樣就能大大提高執行速度。
同樣的功能,StringIO 是純Python程式碼編寫的,而 cStringIO 部分函式是 C 寫的,因此 cStringIO 執行速度更快。
利用ImportError錯誤,我們經常在Python中動態匯入模組:

try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO

上述程式碼先嚐試從cStringIO匯入,如果失敗了(比如cStringIO沒有被安裝),再嘗試從StringIO匯入。這樣,如果cStringIO模組存在,則我們將獲得更快的執行速度,如果cStringIO不存在,則頂多程式碼執行速度會變慢,但不會影響程式碼的正常執行。
try 的作用是捕獲錯誤,並在捕獲到指定錯誤時執行 except 語句。

2.4 使用future

Python的新版本會引入新的功能,但是,實際上這些功能在上一個老版本中就已經存在了。要“試用”某一新的特性,就可以通過匯入future模組的某些功能來實現。
例如,Python 2.7的整數除法運算結果仍是整數:

>>> 10 / 3
3

但是,Python 3.x已經改進了整數的除法運算,“/”除將得到浮點數,“//”除才仍是整數:

>>> 10 / 3
3.3333333333333335
>>> 10 // 3
3

要在Python 2.7中引入3.x的除法規則,匯入future的division:

>>> from __future__ import division
>>> print 10 / 3
3.3333333333333335

當新版本的一個特性與舊版本不相容時,該特性將會在舊版本中新增到future中,以便舊的程式碼能在舊版本中測試新特性。

2.5 安裝第三方模組

方法一:
easy_install
方法二:
pip(推薦,已內建到Python2.7.9)

要想找到合適的第三方模組的名字可以去pypi.python.org搜尋

三、物件導向程式設計基礎

3.1 物件導向程式設計

什麼是物件導向程式設計:

  1. 物件導向程式設計是一種程式設計正規化
  2. 把程式看作不同物件的相互呼叫
  3. 對現實世界建立物件模型

物件導向程式設計的基本思想:
類和例項:
類用於定義抽象型別
例項根據類的定義被建立出來

物件導向程式設計:資料封裝

3.2 定義類並建立例項

在Python中,類通過 class 關鍵字定義。以 Person 為例,定義一個Person類如下:

class Person(object):
    pass

按照 Python 的程式設計習慣,類名以大寫字母開頭,緊接著是(object),表示該類是從哪個類繼承下來的。類的繼承將在後面的章節講解,現在我們只需要簡單地從object類繼承。
有了Person類的定義,就可以建立出具體的xiaoming、xiaohong等例項。建立例項使用 類名+(),類似函式呼叫的形式建立:

xiaoming = Person()
xiaohong = Person()

3.3 建立例項屬性

雖然可以通過Person類建立出xiaoming、xiaohong等例項,但是這些例項看上除了地址不同外,沒有什麼其他不同。在現實世界中,區分xiaoming、xiaohong要依靠他們各自的名字、性別、生日等屬性。
如何讓每個例項擁有各自不同的屬性?由於Python是動態語言,對每一個例項,都可以直接給他們的屬性賦值,例如,給xiaoming這個例項加上name、gender和birth屬性:

xiaoming = Person()
xiaoming.name = 'Xiao Ming'
xiaoming.gender = 'Male'
xiaoming.birth = '1990-1-1'

給xiaohong加上的屬性不一定要和xiaoming相同:

xiaohong = Person()
xiaohong.name = 'Xiao Hong'
xiaohong.school = 'No. 1 High School'
xiaohong.grade = 2

例項的屬性可以像普通變數一樣進行操作:

xiaohong.grade = xiaohong.grade + 1

3.4 初始化例項屬性

雖然我們可以自由地給一個例項繫結各種屬性,但是,現實世界中,一種型別的例項應該擁有相同名字的屬性。例如,Person類應該在建立的時候就擁有 name、gender 和 birth 屬性,怎麼辦?
在定義 Person 類時,可以為Person類新增一個特殊的init()方法,當建立例項時,init()方法被自動呼叫,我們就能在此為每個例項都統一加上以下屬性:

class Person(object):
    def __init__(self, name, gender, birth):
        self.name = name
        self.gender = gender
        self.birth = birth

init() 方法的第一個引數必須是 self(也可以用別的名字,但建議使用習慣用法),後續引數則可以自由指定,和定義函式沒有任何區別。
相應地,建立例項時,就必須要提供除 self 以外的引數:

xiaoming = Person('Xiao Ming', 'Male', '1991-1-1')
xiaohong = Person('Xiao Hong', 'Female', '1992-2-2')

有了init()方法,每個Person例項在建立時,都會有 name、gender 和 birth 這3個屬性,並且,被賦予不同的屬性值,訪問屬性使用.操作符:

print xiaoming.name
# 輸出 'Xiao Ming'
print xiaohong.birth
# 輸出 '1992-2-2'

要特別注意的是,初學者定義init()方法常常忘記了 self 引數:

>>> class Person(object):
...     def __init__(name, gender, birth):
...         pass
... 
>>> xiaoming = Person('Xiao Ming', 'Male', '1990-1-1')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() takes exactly 3 arguments (4 given)

這會導致建立失敗或執行不正常,因為第一個引數name被Python直譯器傳入了例項的引用,從而導致整個方法的呼叫引數位置全部沒有對上。

3.5 訪問限制

我們可以給一個例項繫結很多屬性,如果有些屬性不希望被外部訪問到怎麼辦?
Python對屬性許可權的控制是通過屬性名來實現的,如果一個屬性由雙下劃線開頭(__),該屬性就無法被外部訪問。看例子:

class Person(object):
    def __init__(self, name):
        self.name = name
        self._title = 'Mr'
        self.__job = 'Student'
p = Person('Bob')
print p.name
# => Bob
print p._title
# => Mr
print p.__job
# => Error
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute '__job'

可見,只有以雙下劃線開頭的"__job"不能直接被外部訪問。
但是,如果一個屬性以"xxx"的形式定義,那它又可以被外部訪問了,以"xxx"定義的屬性在Python的類中被稱為特殊屬性,有很多預定義的特殊屬性可以使用,通常我們不要把普通屬性用"xxx"定義。
以單下劃線開頭的屬性"_xxx"雖然也可以被外部訪問,但是,按照習慣,他們不應該被外部訪問。

3.6 建立類屬性

類是模板,而例項則是根據類建立的物件。
繫結在一個例項上的屬性不會影響其他例項,但是,類本身也是一個物件,如果在類上繫結一個屬性,則所有例項都可以訪問類的屬性,並且,所有例項訪問的類屬性都是同一個!也就是說,例項屬性每個例項各自擁有,互相獨立,而類屬性有且只有一份。
定義類屬性可以直接在 class 中定義:

class Person(object):
    address = 'Earth'
    def __init__(self, name):
        self.name = name

因為類屬性是直接繫結在類上的,所以,訪問類屬性不需要建立例項,就可以直接訪問:

print Person.address
# => Earth

對一個例項呼叫類的屬性也是可以訪問的,所有例項都可以訪問到它所屬的類的屬性:

p1 = Person('Bob')
p2 = Person('Alice')
print p1.address
# => Earth
print p2.address
# => Earth

由於Python是動態語言,類屬性也是可以動態新增和修改的:

Person.address = 'China'
print p1.address
# => 'China'
print p2.address
# => 'China'

因為類屬性只有一份,所以,當Person類的address改變時,所有例項訪問到的類屬性都改變了。

3.7 類屬性和例項實型名字衝突怎麼辦

修改類屬性會導致所有例項訪問到的類屬性全部都受影響,但是,如果在例項變數上修改類屬性會發生什麼問題呢?

class Person(object):
    address = 'Earth'
    def __init__(self, name):
        self.name = name

p1 = Person('Bob')
p2 = Person('Alice')

print 'Person.address = ' + Person.address

p1.address = 'China'
print 'p1.address = ' + p1.address

print 'Person.address = ' + Person.address
print 'p2.address = ' + p2.address

結果如下:

Person.address = Earth
p1.address = China
Person.address = Earth
p2.address = Earth

我們發現,在設定了 p1.address = 'China' 後,p1訪問 address 確實變成了 'China',但是,Person.address和p2.address仍然是'Earch',怎麼回事?
原因是 p1.address = 'China'並沒有改變 Person 的 address,而是給 p1這個例項繫結了例項屬性address ,對p1來說,它有一個例項屬性address(值是'China'),而它所屬的類Person也有一個類屬性address,所以:
訪問 p1.address 時,優先查詢例項屬性,返回'China'。
訪問 p2.address 時,p2沒有例項屬性address,但是有類屬性address,因此返回'Earth'。
可見,當例項屬性和類屬性重名時,例項屬性優先順序高,它將遮蔽掉對類屬性的訪問。
當我們把 p1 的 address 例項屬性刪除後,訪問 p1.address 就又返回類屬性的值 'Earth'了:

del p1.address
print p1.address
# => Earth

可見,千萬不要在例項上修改類屬性,它實際上並沒有修改類屬性,而是給例項繫結了一個例項屬性。

3.8 定義例項方法

一個例項的私有屬性就是以__開頭的屬性,無法被外部訪問,那這些屬性定義有什麼用?
雖然私有屬性無法從外部訪問,但是,從類的內部是可以訪問的。除了可以定義例項的屬性外,還可以定義例項的方法。
例項的方法就是在類中定義的函式,它的第一個引數永遠是 self,指向呼叫該方法的例項本身,其他引數和一個普通函式是完全一樣的:

class Person(object):

    def __init__(self, name):
        self.__name = name

    def get_name(self):
        return self.__name

get_name(self) 就是一個例項方法,它的第一個引數是self。init(self, name)其實也可看做是一個特殊的例項方法。
呼叫例項方法必須在例項上呼叫:

p1 = Person('Bob')
print p1.get_name()  # self不需要顯式傳入
# => Bob

在例項方法內部,可以訪問所有例項屬性,這樣,如果外部需要訪問私有屬性,可以通過方法呼叫獲得,這種資料封裝的形式除了能保護內部資料一致性外,還可以簡化外部呼叫的難度。

3.9 方法也是屬性

我們在 class 中定義的例項方法其實也是屬性,它實際上是一個函式物件:

class Person(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score
    def get_grade(self):
        return 'A'

p1 = Person('Bob', 90)
print p1.get_grade
# => <bound method Person.get_grade of <__main__.Person object at 0x109e58510>>
print p1.get_grade()
# => A

也就是說,p1.get_grade 返回的是一個函式物件,但這個函式是一個繫結到例項的函式,p1.get_grade() 才是方法呼叫。
因為方法也是一個屬性,所以,它也可以動態地新增到例項上,只是需要用 types.MethodType() 把一個函式變為一個方法:

import types
def fn_get_grade(self):
    if self.score >= 80:
        return 'A'
    if self.score >= 60:
        return 'B'
    return 'C'

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

p1 = Person('Bob', 90)
p1.get_grade = types.MethodType(fn_get_grade, p1, Person)
print p1.get_grade()
# => A
p2 = Person('Alice', 65)
print p2.get_grade()
# ERROR: AttributeError: 'Person' object has no attribute 'get_grade'
# 因為p2例項並沒有繫結get_grade
給一個例項動態新增方法並不常見,直接在class中定義要更直觀。

3.10 定義類方法

和屬性類似,方法也分例項方法和類方法。
在class中定義的全部是例項方法,例項方法第一個引數 self 是例項本身。
要在class中定義類方法,需要這麼寫:

class Person(object):
    count = 0
    @classmethod
    def how_many(cls):
        return cls.count
    def __init__(self, name):
        self.name = name
        Person.count = Person.count + 1

print Person.how_many()
p1 = Person('Bob')
print Person.how_many()

通過標記一個 @classmethod,該方法將繫結到 Person 類上,而非類的例項。類方法的第一個引數將傳入類本身,通常將引數名命名為 cls,上面的 cls.count 實際上相當於 Person.count。
因為是在類上呼叫,而非例項上呼叫,因此類方法無法獲得任何例項變數,只能獲得類的引用。

四、類的繼承

4.1 什麼是繼承

什麼是繼承:

  • 新類不必從頭編寫
  • 新類從現有的類繼承,就自動擁有了現有類的所有功能
  • 新類只需要編寫現有類缺少的新功能

繼承的好處:

  • 複用已有程式碼
  • 自動擁有了現有類的所有功能
  • 只需要編寫缺少的新功能
    不要忘記呼叫super()init方法

4.2 繼承一個類

如果已經定義了Person類,需要定義新的Student和Teacher類時,可以直接從Person類繼承:

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

定義Student類時,只需要把額外的屬性加上,例如score:

class Student(Person):
    def __init__(self, name, gender, score):
        super(Student, self).__init__(name, gender)
        self.score = score

一定要用 super(Student, self).__init__(name, gender) 去初始化父類,否則,繼承自 Person 的 Student 將沒有 name 和 gender。
函式super(Student, self)將返回當前類繼承的父類,即 Person ,然後呼叫init()方法,注意self引數已在super()中傳入,在init()中將隱式傳遞,不需要寫出(也不能寫)。

43. 判斷型別

函式isinstance()可以判斷一個變數的型別,既可以用在Python內建的資料型別如str、list、dict,也可以用在我們自定義的類,它們本質上都是資料型別。
假設有如下的 Person、Student 和 Teacher 的定義及繼承關係如下:

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

class Student(Person):
    def __init__(self, name, gender, score):
        super(Student, self).__init__(name, gender)
        self.score = score

class Teacher(Person):
    def __init__(self, name, gender, course):
        super(Teacher, self).__init__(name, gender)
        self.course = course

p = Person('Tim', 'Male')
s = Student('Bob', 'Male', 88)
t = Teacher('Alice', 'Female', 'English')

當我們拿到變數 p、s、t 時,可以使用 isinstance 判斷型別:

>>> isinstance(p, Person)
True    # p是Person型別
>>> isinstance(p, Student)
False   # p不是Student型別
>>> isinstance(p, Teacher)
False   # p不是Teacher型別

這說明在繼承鏈上,一個父類的例項不能是子類型別,因為子類比父類多了一些屬性和方法。
我們再考察 s :

>>> isinstance(s, Person)
True    # s是Person型別
>>> isinstance(s, Student)
True    # s是Student型別
>>> isinstance(s, Teacher)
False   # s不是Teacher型別

s 是Student型別,不是Teacher型別,這很容易理解。但是,s 也是Person型別,因為Student繼承自Person,雖然它比Person多了一些屬性和方法,但是,把 s 看成Person的例項也是可以的。
這說明在一條繼承鏈上,一個例項可以看成它本身的型別,也可以看成它父類的型別。

4.4 多型

類具有繼承關係,並且子類型別可以向上轉型看做父類型別,如果我們從 Person 派生出 Student和Teacher ,並都寫了一個 whoAmI() 方法:

class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender
    def whoAmI(self):
        return 'I am a Person, my name is %s' % self.name

class Student(Person):
    def __init__(self, name, gender, score):
        super(Student, self).__init__(name, gender)
        self.score = score
    def whoAmI(self):
        return 'I am a Student, my name is %s' % self.name

class Teacher(Person):
    def __init__(self, name, gender, course):
        super(Teacher, self).__init__(name, gender)
        self.course = course
    def whoAmI(self):
        return 'I am a Teacher, my name is %s' % self.name

在一個函式中,如果我們接收一個變數 x,則無論該 x 是 Person、Student還是 Teacher,都可以正確列印出結果:

def who_am_i(x):
    print x.whoAmI()

p = Person('Tim', 'Male')
s = Student('Bob', 'Male', 88)
t = Teacher('Alice', 'Female', 'English')

who_am_i(p)
who_am_i(s)
who_am_i(t)

執行結果:

I am a Person, my name is Tim
I am a Student, my name is Bob
I am a Teacher, my name is Alice

這種行為稱為多型。也就是說,方法呼叫將作用在 x 的實際型別上。s 是Student型別,它實際上擁有自己的 whoAmI()方法以及從 Person繼承的 whoAmI方法,但呼叫 s.whoAmI()總是先查詢它自身的定義,如果沒有定義,則順著繼承鏈向上查詢,直到在某個父類中找到為止。
由於Python是動態語言,所以,傳遞給函式 who_am_i(x)的引數 x 不一定是 Person 或 Person 的子型別。任何資料型別的例項都可以,只要它有一個whoAmI()的方法即可:

class Book(object):
    def whoAmI(self):
        return 'I am a book'

這是動態語言和靜態語言(例如Java)最大的差別之一。動態語言呼叫例項方法,不檢查型別,只要方法存在,引數正確,就可以呼叫。

4.5 多重繼承

除了從一個父類繼承外,Python允許從多個父類繼承,稱為多重繼承。
多重繼承的繼承鏈就不是一棵樹了,它像這樣:

class A(object):
    def __init__(self, a):
        print 'init A...'
        self.a = a

class B(A):
    def __init__(self, a):
        super(B, self).__init__(a)
        print 'init B...'

class C(A):
    def __init__(self, a):
        super(C, self).__init__(a)
        print 'init C...'

class D(B, C):
    def __init__(self, a):
        super(D, self).__init__(a)
        print 'init D...'

像這樣,D 同時繼承自 B 和 C,也就是 D 擁有了 A、B、C 的全部功能。多重繼承通過 super()呼叫init()方法時,A 雖然被繼承了兩次,但init()只呼叫一次:

>>> d = D('d')
init A...
init C...
init B...
init D...

多重繼承的目的是從兩種繼承樹中分別選擇並繼承出子類,以便組合功能使用。
舉個例子,Python的網路伺服器有TCPServer、UDPServer、UnixStreamServer、UnixDatagramServer,而伺服器執行模式有 多程式ForkingMixin 和 多執行緒ThreadingMixin兩種。
要建立多程式模式的 TCPServer:
class MyTCPServer(TCPServer, ForkingMixin)
pass
要建立多執行緒模式的 UDPServer:
class MyUDPServer(UDPServer, ThreadingMixin):
pass
如果沒有多重繼承,要實現上述所有可能的組合需要 4x2=8 個子類。

4.6 獲取物件資訊

拿到一個變數,除了用 isinstance() 判斷它是否是某種型別的例項外,還有沒有別的方法獲取到更多的資訊呢?
例如,已有定義:

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

class Student(Person):
    def __init__(self, name, gender, score):
        super(Student, self).__init__(name, gender)
        self.score = score
    def whoAmI(self):
        return 'I am a Student, my name is %s' % self.name

首先可以用 type() 函式獲取變數的型別,它返回一個 Type 物件:

>>> type(123)
<type 'int'>
>>> s = Student('Bob', 'Male', 88)
>>> type(s)
<class '__main__.Student'>

其次,可以用 dir() 函式獲取變數的所有屬性:

>>> dir(123)   # 整數也有很多屬性...
['__abs__', '__add__', '__and__', '__class__', '__cmp__', ...]

>>> dir(s)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'gender', 'name', 'score', 'whoAmI']

對於例項變數,dir()返回所有例項屬性,包括__class__這類有特殊意義的屬性。注意到方法whoAmI也是 s 的一個屬性。
如何去掉__xxx__這類的特殊屬性,只保留我們自己定義的屬性?回顧一下filter()函式的用法。
dir()返回的屬性是字串列表,如果已知一個屬性名稱,要獲取或者設定物件的屬性,就需要用 getattr()setattr( )函式了:

>>> getattr(s, 'name')  # 獲取name屬性
'Bob'

>>> setattr(s, 'name', 'Adam')  # 設定新的name屬性

>>> s.name
'Adam'

>>> getattr(s, 'age')  # 獲取age屬性,但是屬性不存在,報錯:
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'age'

>>> getattr(s, 'age', 20)  # 獲取age屬性,如果屬性不存在,就返回預設值20:
20

五、定製類

5.1 什麼是特殊方法

  • 特殊方法定義在calss中
  • 不需要直接呼叫
  • Python的某些函式或操作符會呼叫對應的特殊方法

5.2 strrepr

如果要把一個類的例項變成 str,就需要實現特殊方法str():

class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender
    def __str__(self):
        return '(Person: %s, %s)' % (self.name, self.gender)

現在,在互動式命令列下用 print 試試:

>>> p = Person('Bob', 'male')
>>> print p
(Person: Bob, male)
但是,如果直接敲變數 p:
>>> p
<main.Person object at 0x10c941890>

似乎str() 不會被呼叫。
因為 Python 定義了str()和repr()兩種方法,str()用於顯示給使用者,而repr()用於顯示給開發人員。
有一個偷懶的定義repr的方法:

class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender
    def __str__(self):
        return '(Person: %s, %s)' % (self.name, self.gender)
    __repr__ = __str__

5.3 cmp

對 int、str 等內建資料型別排序時,Python的 sorted() 按照預設的比較函式 cmp 排序,但是,如果對一組 Student 類的例項排序時,就必須提供我們自己的特殊方法 cmp():

class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score
    def __str__(self):
        return '(%s: %s)' % (self.name, self.score)
    __repr__ = __str__

    def __cmp__(self, s):
        if self.name < s.name:
            return -1
        elif self.name > s.name:
            return 1
        else:
            return 0

上述 Student 類實現了cmp()方法,cmp用例項自身self和傳入的例項 s 進行比較,如果 self 應該排在前面,就返回 -1,如果 s 應該排在前面,就返回1,如果兩者相當,返回 0。
Student類實現了按name進行排序:

>>> L = [Student('Tim', 99), Student('Bob', 88), Student('Alice', 77)]
>>> print sorted(L)
[(Alice: 77), (Bob: 88), (Tim: 99)]

注意: 如果list不僅僅包含 Student 類,則 cmp 可能會報錯:

5.4 len

如果一個類表現得像一個list,要獲取有多少個元素,就得用 len() 函式。
要讓 len() 函式工作正常,類必須提供一個特殊方法len(),它返回元素的個數。
例如,我們寫一個 Students 類,把名字傳進去:

class Students(object):
    def __init__(self, *args):
        self.names = args
    def __len__(self):
        return len(self.names)

只要正確實現了len()方法,就可以用len()函式返回Students例項的“長度”:

>>> ss = Students('Bob', 'Alice', 'Tim')
>>> print len(ss)
3

5.5 數學運算

Python 提供的基本資料型別 int、float 可以做整數和浮點的四則運算以及乘方等運算。
但是,四則運算不侷限於int和float,還可以是有理數、矩陣等。
要表示有理數,可以用一個Rational類來表示:

class Rational(object):
    def __init__(self, p, q):
        self.p = p
        self.q = q

p、q 都是整數,表示有理數 p/q。
如果要讓Rational進行+運算,需要正確實現add

class Rational(object):
    def __init__(self, p, q):
        self.p = p
        self.q = q
    def __add__(self, r):
        return Rational(self.p * r.q + self.q * r.p, self.q * r.q)
    def __str__(self):
        return '%s/%s' % (self.p, self.q)
    __repr__ = __str__

現在可以試試有理數加法:

>>> r1 = Rational(1, 3)
>>> r2 = Rational(1, 2)
>>> print r1 + r2
5/6

5.6 型別轉換

Rational類實現了有理數運算,但是,如果要把結果轉為 int 或 float 怎麼辦?
考察整數和浮點數的轉換:

>>> int(12.34)
12
>>> float(12)
12.0

如果要把 Rational 轉為 int,應該使用:

r = Rational(12, 5)
n = int(r)

要讓int()函式正常工作,只需要實現特殊方法int():

class Rational(object):
    def __init__(self, p, q):
        self.p = p
        self.q = q
    def __int__(self):
        return self.p // self.q

結果如下:

>>> print int(Rational(7, 2))
3
>>> print int(Rational(1, 3))
0

同理,要讓float()函式正常工作,只需要實現特殊方法float()。

5.7 @property

考察 Student 類:

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

當我們想要修改一個 Student 的 scroe 屬性時,可以這麼寫:

s = Student('Bob', 59)
s.score = 60

但是也可以這麼寫:

s.score = 1000

顯然,直接給屬性賦值無法檢查分數的有效性。
如果利用兩個方法:

class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.__score = score
    def get_score(self):
        return self.__score
    def set_score(self, score):
        if score < 0 or score > 100:
            raise ValueError('invalid score')
        self.__score = score

這樣一來,s.set_score(1000) 就會報錯。
這種使用 get/set 方法來封裝對一個屬性的訪問在許多物件導向程式設計的語言中都很常見。
但是寫 s.get_score() 和 s.set_score() 沒有直接寫 s.score 來得直接。
有沒有兩全其美的方法?----有。
因為Python支援高階函式,在函數語言程式設計中我們介紹了裝飾器函式,可以用裝飾器函式把 get/set 方法“裝飾”成屬性呼叫:

class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.__score = score
    @property
    def score(self):
        return self.__score
    @score.setter
    def score(self, score):
        if score < 0 or score > 100:
            raise ValueError('invalid score')
        self.__score = score

注意: 第一個score(self)是get方法,用@property裝飾,第二個score(self, score)是set方法,用@score.setter裝飾,@score.setter是前一個@property裝飾後的副產品。
現在,就可以像使用屬性一樣設定score了:

>>> s = Student('Bob', 59)
>>> s.score = 60
>>> print s.score
60
>>> s.score = 1000
Traceback (most recent call last):
  ...
ValueError: invalid score

說明對 score 賦值實際呼叫的是 set方法。

5.8 slots

由於Python是動態語言,任何例項在執行期都可以動態地新增屬性。
如果要限制新增的屬性,例如,Student類只允許新增 name、gender和score 這3個屬性,就可以利用Python的一個特殊的slots來實現。
顧名思義,slots是指一個類允許的屬性列表:

class Student(object):
    __slots__ = ('name', 'gender', 'score')
    def __init__(self, name, gender, score):
        self.name = name
        self.gender = gender
        self.score = score

現在,對例項進行操作:

>>> s = Student('Bob', 'male', 59)
>>> s.name = 'Tim' # OK
>>> s.score = 99 # OK
>>> s.grade = 'A'
Traceback (most recent call last):
  ...
AttributeError: 'Student' object has no attribute 'grade'

slots的目的是限制當前類所能擁有的屬性,如果不需要新增任意動態的屬性,使用slots也能節省記憶體。

5.9 call

在Python中,函式其實是一個物件:

>>> f = abs
>>> f.__name__
'abs'
>>> f(-123)
123

由於 f 可以被呼叫,所以,f 被稱為可呼叫物件。
所有的函式都是可呼叫物件。
一個類例項也可以變成一個可呼叫物件,只需要實現一個特殊方法call()。
我們把 Person 類變成一個可呼叫物件:

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

    def __call__(self, friend):
        print 'My name is %s...' % self.name
        print 'My friend is %s...' % friend

現在可以對 Person 例項直接呼叫:

>>> p = Person('Bob', 'male')
>>> p('Tim')
My name is Bob...
My friend is Tim...

單看 p('Tim') 你無法確定 p 是一個函式還是一個類例項,所以,在Python中,函式也是物件,物件和函式的區別並不顯著。
希望大家喜歡,點贊哦

相關文章