Python -- 進階

函數式編程

f = abs
f(-1)

把函數作爲參數

def t(a,b,f)
    return f(a)+f(b)

t(1,2,math.sqrt)

map()函數

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

注意:map()函數不改變原有的 list,而是返回一個新的 list。

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。

reduce()還可以接收第3個可選參數,作爲計算的初始值。如果把初始值設爲100,計算:reduce(f, [1, 3, 5, 7, 9], 100)結果將變爲125,因爲第一輪計算是:計算初始值和第一個元素:f(100, 1),結果爲101。

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'

a='\t\t123\r\n'
a.strip()
#'123'

自定義排序函數

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]

返回函數

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 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

請編寫一個函數calc_prod(lst),它接收一個list,返回一個函數,返回函數可以計算參數的乘積。

先定義能計算乘積的函數,再將此函數返回。
參考代碼:

  def calc_prod(lst):
            def lazy_prod():
                def f(x, y):
                    return x * y
                return reduce(f, lst, 1)
            return lazy_prod
f = calc_prod([1, 2, 3, 4])
print f()

閉包

# 在函數內部定義的函數和外部定義的函數是一樣的,只是他們無法被外部訪問:
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()

print f1()
print f2()
print f3()

# 9
# 9
# 9

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

原因就是當count()函數返回了3個函數時,這3個函數所引用的變量 i 的值已經變成了3。由於f1、f2、f3並沒有被調用,所以,此時他們並未計算 i*i,當 f1 被調用時:因此,返回函數不要引用任何循環變量,或者後續會發生變化的變量。

返回閉包不能引用循環變量,請改寫count()函數,讓它正確返回能計算1x1、2x2、3x3的函數。

在count函數的循環內部,如果藉助f函數,就可以避免引用循環變量i。
參考代碼:

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()
# 考察下面的函數 f:
def f(j):
    def g():
        return j*j
    return g

它可以正確地返回一個閉包g,g所引用的變量j不是循環變量,因此將正常執行。

匿名函數

高階函數可以接收函數做參數,有些時候,我們不需要顯式地定義函數,直接傳入匿名函數更方便。

在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,返回值就是該表達式的結果。

返回函數的時候,也可以返回匿名函數:

myabs = lambda x: -x if x < 0 else x 
myabs(-1)
# 1
myabs(1)
# 1

裝飾器

不改動函數代碼,而是動態修改函數功能

  • 方法一: 修改函數本身
  • 方法二:高階函數
def f1(x):
    return x*2

def new_fn(f):
    def fn(x):
        print 'call' + f._name_ + '()'
        return f(x)

    return fn

#調用
g = new_fn(f)
g(4)

f1 = new_fn(f1)
print f1(5)
# @ 簡化
@new_fn
def f1(x):
    return x*2

# 相當於
def f1(x)
    return x*2
f1 = new_fn(f1)

裝飾器作用 – 避免重複代碼

  • 打印日誌
  • 檢測性能
  • 數據庫事務
  • URL路由

不帶參數的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 都能正常工作。

編寫帶參數decorator

考察上一節的 @log 裝飾器

發現對於被裝飾的函數,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

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

拆開以後會發現,調用會失敗,因爲在3層嵌套的decorator定義中,最內層的wrapper引用了最外層的參數prefix,所以,把一個閉包拆成普通的函數調用會比較困難。不支持閉包的編程語言要實現同樣的功能就需要更多的代碼。

完善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’。

偏函數

當一個函數有很多參數時,調用者就需要提供多個參數。如果減少參數個數,就可以簡化調用者的負擔。

比如,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可以把一個參數多的函數變成一個參數少的新函數,少的參數需要在創建時指定默認值,這樣,新函數調用的難度就降低了。

模塊和包

導入系統自帶的模塊 math

import math
math.pow(2, 0.5) # pow是函數
math.pi # pi是變量

如果我們只希望導入用到的math模塊的某幾個函數,而不是所有函數,可以用下面的語句:

from math import pow, sin, log
pow(2, 10)
sin(3.14)

如果使用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

動態導入模塊
如果導入的模塊不存在,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 語句。

使用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中,以便舊的代碼能在舊版本中測試新特性。

面向對象

先了解一下創建類和屬性已經其類方法:

# coding=utf-8

# 初始化實例屬性  __init__()
# 當創建實例時,__init__()方法被自動調用,我們就能在此爲每個實例都統一加上以下屬性:
# __init__() 方法的第一個參數必須是 self(也可以用別的名字,但建議使用習慣用法),後續參數則可以自由指定,和定義函數沒有任何區別。

# 訪問限制 Python對屬性權限的控制是通過屬性名來實現的,如果一個屬性由雙下劃線開頭(__),該屬性就無法被外部訪問。
# 但是,如果一個屬性以"__xxx__"的形式定義,那它又可以被外部訪問了,以"__xxx__"定義的屬性在Python的類中被稱爲特殊屬性,有很多預定義的特殊屬性可以使用,通常我們不要把普通屬性用"__xxx__"定義。
# 以單下劃線開頭的屬性"_xxx"也可以被外部訪問

# 創建類屬性(靜態成員變量)要用Person.count調用

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

# 定義類方法
# 通過標記一個 @classmethod,該方法將綁定到 Person 類上,而非類的實例。類方法的第一個參數將傳入類本身,通常將參數名命名爲 cls,
# 上面的 cls.count 實際上相當於 Person.count。
# 因爲是在類上調用,而非實例上調用,因此類方法無法獲得任何實例變量,只能獲得類的引用。


class Person(object):
    address = 'Earth' #類屬性,要用Person.count調用
    def __init__(self, name, gender, birth,job):    #初始化實例屬性
        self.name = name
        self.gender = gender
        self.birth = birth
        self.__job = job   # 訪問限制

    def getJob(self):
        return self.__job

    @classmethod
    def this_is_a_classmethod(cls):
        print 'this is a class method'


# 相應地,創建實例時,就必須要提供除 self 以外的參數:
p = Person('Xiao Ming', 'Male', '1991-1-1','Student')


# 可見,只有以雙下劃線開頭的"__job"不能直接被外部訪問。
# print p.__job

# 調用實例方法
print p.getJob()

# 方法也是一種屬性
print p.getJob
f = p.getJob
print f()

p.this_is_a_classmethod()
Person.this_is_a_classmethod()

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

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

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

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

方法也是屬性

我們在 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中定義要更直觀

繼承

import Person
# 繼承
# 一定要用 super(Student, self).__init__(name, gender) 去初始化父類,否則,繼承自 Person 的 Student 將沒有 name 和 gender。
# self參數已在super()中傳入,在__init__()中將隱式傳遞,不需要寫出(也不能寫)。

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

多態


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

# 這種行爲稱爲多態。也就是說,方法調用將作用在 x 的實際類型上。s 是Student類型,
# 它實際上擁有自己的 whoAmI()方法以及從 Person繼承的 whoAmI方法,但調用 s.whoAmI()總是先查找它自身的定義,
# 如果沒有定義,則順着繼承鏈向上查找,直到在某個父類中找到爲止。

# 由於Python是動態語言,所以,傳遞給函數 who_am_i(x)的參數 x 不一定是 Person 或 Person 的子類型。任何數據類型的實例都可以,只要它有一個whoAmI()的方法即可:
# 這是動態語言和靜態語言(例如Java)最大的差別之一。動態語言調用實例方法,不檢查類型,只要方法存在,參數正確,就可以調用。



class Person(object):
    address = 'Earth' #類屬性,要用Person.count調用
    def __init__(self, name, gender, birth, job):    #初始化實例屬性
        self.name = name
        self.gender = gender
        self.birth = birth
        self.__job = job   # 訪問限制

    def getJob(self):
        return 'Person'

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

    def getJob(self):
        return 'Student'


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

    def getJob(self):
        return 'Teacher'


def who_is_my_job(x):
    print x.getJob()

p = Person('Tim', 'Male', '99', 'kk')
s = Student('Bob', 'Male', '88', '990', 'kls')
t = Teacher('Alice', 'Female', 'English', 'Teacher', 'sdf')

who_is_my_job(p)
who_is_my_job(s)
who_is_my_job(t)


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

b = Book()
who_is_my_job(b)

多重繼承


# 多重繼承
# 除了從一個父類繼承外,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 個子類。

類型判斷


# 判斷類型
# 函數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類型

獲取對象信息

有type()、dir()、getattr()、setattr()。


# 獲取對象信息
#
# 拿到一個變量,除了用 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']


# 如何去掉`__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

特殊方法

類似Java的toString()、equals()等

__str__和__repr__

如果要把一個類的實例變成 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__

__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。

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

L = [Student('Tim', 99), Student('Bob', 88), 100, 'Hello']
print sorted(L)

__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

數學運算

加法運算:__add__
減法運算:__sub__
乘法運算:__mul__
除法運算:__div__

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

類型轉換

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__()

@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方法。

如果沒有定義set方法,就不能對“屬性”賦值,這時,就可以創建一個只讀“屬性”。

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_也能節省內存。

假設Person類通過__slots__定義了name和gender,請在派生類Student中通過__slots__繼續添加score的定義,使Student類可以實現name、gender和score 3個屬性。

Student類的__slots__只需要包含Person類不包含的score屬性即可。

參考代碼:

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

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

s = Student('Bob', 'male', 59)
s.name = 'Tim'
s.score = 99
print s.score

__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中,函數也是對象,對象和函數的區別並不顯著。

本篇博客是繼上一篇博客內容,好了,接下來寫可以去寫數據結構了 T^T

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章