1. 程式人生 > >Python - 進階篇

Python - 進階篇

一、課程介紹

二、函數語言程式設計

  1. Python中函數語言程式設計簡介

  2. Python中高階函式

  3. Python中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可以處理這種資料型別。

  4. Python中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

  5. Python中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'

    任務

    請利用filter()過濾出1~100中平方根是整數的數,即結果應該是:

    [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

    import math
    
    def is_sqr(x):
        return math.sqrt(x)%1==0
    
    print filter(is_sqr, range(1, 101))

     

  6. Python中自定義排序函式

    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'小。

  7. Python中返回函式

    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

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

    任務

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

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

     

  8. Python中閉包

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

    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

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

    任務

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

    def count():
        fs = []
        for i in range(1, 4):
            def f(i=i): # default
                return i*i
            fs.append(f)
        return fs
    
    f1, f2, f3 = count()
    
    print f1(), f2(), f3() # 因為上面屬於default,所以可以省略

     

  9. Python中匿名函式

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

    在Python中,對匿名函式提供了有限支援。還是以map()函式為例,計算 f(x)=x時,除了定義一個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

     

  10. 待更新...

三、裝飾器

  1. Python中decorator裝飾器

  2. Python中編寫無引數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 都能正常工作。

    Ps:以*開頭開頭表示可以傳遞一個元元組進去,類似於(1,2,3);**開頭是表示可以接受一個字典,類似於{'a'=1,'b'=2}。

    任務

    請編寫一個@performance,它可以打印出函式呼叫的時間。

    import time
    
    def performance(f):
        def fun(x):
            print 'call',f.__name__+'()','in',time.strftime('%Y-%m-%d',time.localtime(time.time()))
            return f(x)
        return fun
    
    @performance
    def factorial(n):
        return reduce(lambda x,y: x*y, range(1, n+1))
    
    print factorial(10)

     

  3. Python中編寫帶引數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,所以,把一個閉包拆成普通的函式呼叫會比較困難。不支援閉包的程式語言要實現同樣的功能就需要更多的程式碼。

    Ps:帶引數decorator,也就是在無參decorator的最外層再加上一層def即可。

    任務

    上一節的@performance只能列印秒,請給 @performace 增加一個引數,允許傳入's'或'ms'

    @performance('ms')
    def factorial(n):
        return reduce(lambda x,y: x*y, range(1, n+1))
    import time
    
    def performance(unit):
        def perf_decorator(f):
            def wrapper(*args,**kw):
                t1=time.time()
                fun=f(*args,**kw)
                t2=time.time()
                ts=(t2-t1)*1000 if unit=='ms' else (t2-t1)
                print 'call %s() in %d %s' %(f.__name__,ts,unit)
                return fun
            return wrapper
        return perf_decorator
    
    @performance('ms')
    def factorial(n):
        return reduce(lambda x,y: x*y, range(1, n+1))
    
    print factorial(10)

     

  4. Python中完善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'

    任務

    請思考帶引數的@decorator,@functools.wraps應該放置在哪:

    def performance(unit):
        def perf_decorator(f):
            def wrapper(*args, **kw):
                ???
            return wrapper
        return perf_decorator

     

  5. import time, functools
    
    def performance(unit):
        def perf_decorator(f):
            @functools.wraps(f)
            def wrapper(*args,**kw):
                return f(*args,**kw)
            return wrapper
        return perf_decorator
    
    @performance('ms')
    def factorial(n):
        return reduce(lambda x,y: x*y, range(1, n+1))
    
    print factorial.__name__

    Python中偏函式

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

    比如,int()函式可以把字串轉換為整數,當僅傳入字串時,int()函式預設按十進位制轉換:

    >>> int('12345')
    12345

    但int()函式還提供額外的base引數,預設值為10。如果傳入base引數,就可以做 進位制的轉換:

    >>> 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可以把一個引數多的函式變成一個引數少的新函式,少的引數需要在建立時指定預設值,這樣,新函式呼叫的難度就降低了。

    任務

    在第7節中,我們在sorted這個高階函式中傳入自定義排序函式就可以實現忽略大小寫排序。請用functools.partial把這個複雜呼叫變成一個簡單的函式:

    sorted_ignore_case(iterable)
    import functools
    
    sorted_ignore_case = functools.partial(sorted, key=str.lower)
    
    print sorted_ignore_case(['bob', 'about', 'Zoo', 'Credit'])

     

  6. 待更新...

四、模組

  1. 簡介

  2. Python之匯入模組

    要使用一個模組,我們必須首先匯入該模組。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

     

  3. Python中動態匯入模組

    如果匯入的模組不存在,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 部分函式是 寫的,因此 cStringIO 執行速度更快。

    利用ImportError錯誤,我們經常在Python中動態匯入模組:

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

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

    try 的作用是捕獲錯誤,並在捕獲到指定錯誤時執行 except 語句。

  4. Python之使用__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__中,以便舊的程式碼能在舊版本中測試新特性。

  5. Python之安裝第三方模組

  6. 待更新...

五、面向物件程式設計基礎

  1. 簡介

  2. Python之定義類並建立例項

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

    class Person(object):
        pass

    按照 Python 的程式設計習慣,類名以大寫字母開頭,緊接著是(object),表示該類是從哪個類繼承下來的。類的繼承將在後面的章節講解,現在我們只需要簡單地從object類繼承。

    有了Person類的定義,就可以創建出具體的xiaoming、xiaohong等例項。建立例項使用 類名+(),類似函式呼叫的形式建立:

    xiaoming = Person()
    xiaohong = Person()

     

  3. Python中建立例項屬性

    雖然可以通過Person類創建出xiaoming、xiaohong等例項,但是這些例項看上除了地址不同外,沒有什麼其他不同。在現實世界中,區分xiaoming、xiaohong要依靠他們各自的名字、性別、生日等屬性。

    如何讓每個例項擁有各自不同的屬性?由於Python是動態語言,對每一個例項,都可以直接給他們的屬性賦值,例如,給xiaoming這個例項加上name、genderbirth屬性:

    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

    任務

    請建立包含兩個 Person 類的例項的 list,並給兩個例項的 name 賦值,然後按照 name 進行排序。

    class Person(object):
        pass
    
    p1 = Person()
    p1.name = 'Bart'
    
    p2 = Person()
    p2.name = 'Adam'
    
    p3 = Person()
    p3.name = 'Lisa'
    
    L1 = [p1, p2, p3]
    L2 = sorted(L1,key=lambda x: x.name)
    
    print L2[0].name
    print L2[1].name
    print L2[2].name

     

  4. Python中初始化例項屬性

    雖然我們可以自由地給一個例項繫結各種屬性,但是,現實世界中,一種型別的例項應該擁有相同名字的屬性。例如,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直譯器傳入了例項的引用,從而導致整個方法的呼叫引數位置全部沒有對上。

  5. Python中訪問限制

    我們可以給一個例項繫結很多屬性,如果有些屬性不希望被外部訪問到怎麼辦?

    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"雖然也可以被外部訪問,但是,按照習慣,他們不應該被外部訪問。

  6. Python中建立類屬性

    類是模板,而例項則是根據類建立的物件。

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

    定義類屬性可以直接在 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改變時,所有例項訪問到的類屬性都改變了。

    任務

    請給 Person 類新增一個類屬性 count,每建立一個例項,count 屬性就加 1,這樣就可以統計出一共建立了多少個 Person 的例項。

    class Person(object):
        count=0
        def __init__(self, name):
            self.name=name
            Person.count=1+Person.count  
    
    p1 = Person('Bob')
    print Person.count
    
    p2 = Person('Alice')
    print Person.count
    
    p3 = Person('Tim')
    print Person.count

     

  7. Python中類屬性和例項屬性名字衝突怎麼辦

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

    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

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

  8. Python中定義例項方法

    一個例項的私有屬性就是以__開頭的屬性,無法被外部訪問,那這些屬性定義有什麼用?

    雖然私有屬性無法從外部訪問,但是,從類的內部是可以訪問的。除了可以定義例項的屬性外,還可以定義例項的方法。

    例項的方法就是在類中定義的函式,它的第一個引數永遠是 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

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

  9. Python