【廖雪峰python進階筆記】定製類

Datawhale發表於2018-07-10

1. __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__

2. __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__ 可能會報錯:

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

請思考如何解決。

例項
請修改 Student 的 __cmp__方法,讓它按照分數從高到底排序,分數相同的按名字排序。
分析
需要先比較 score,在 score 相等的情況下,再比較 name。

參考程式碼:

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.score > s.score:
            return -1
        elif self.score == s.score:
            if self.name > s.name:
                return 1
            elif self.name == s.name:
                return 0
            else:
                return -1
        else:
            return 1

L = [Student('Tim', 99), Student('Bob', 88), Student('Alice', 99)]
print sorted(L)
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.score == s.score:
            return cmp(self.name, s.name)
        return -cmp(self.score, s.score)

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

3. __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

例項
斐波那契數列是由 0, 1, 1, 2, 3, 5, 8…構成。

請編寫一個Fib類,Fib(10)表示數列的前10個元素,print Fib(10) 可以列印出數列的前 10 個元素,len(Fib(10))可以正確返回數列的個數10。
分析
需要根據num計算出斐波那契數列的前N個元素。

參考程式碼:

class Fib(object):
    def __init__(self, num):
        a, b, L = 0, 1, []
        for n in range(num):
            L.append(a)
            a, b = b, a + b
        self.numbers = L

    def __str__(self):
        return str(self.numbers)

    __repr__ = __str__

    def __len__(self):
        return len(self.numbers)

f = Fib(10)
print f
print len(f)

4. 數學運算

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類雖然可以做加法,但無法做減法、乘方和除法,請繼續完善Rational類,實現四則運算。

提示:
減法運算:__sub__
乘法運算:__mul__
除法運算:__div__
如果運算結果是 6/8,在顯示的時候需要歸約到最簡形式3/4。

參考程式碼:

def gcd(a, b):
    if b == 0:
        return a
    return gcd(b, a % b)

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 __sub__(self, r):
        return Rational(self.p * r.q - self.q * r.p, self.q * r.q)
    def __mul__(self, r):
        return Rational(self.p * r.p, self.q * r.q)
    def __div__(self, r):
        return Rational(self.p * r.q, self.q * r.p)
    def __str__(self):
        g = gcd(self.p, self.q)
        return '%s/%s' % (self.p / g, self.q / g)
    __repr__ = __str__

r1 = Rational(1, 2)
r2 = Rational(1, 4)
print r1 + r2
print r1 - r2
print r1 * r2
print r1 / r2

5. 型別轉換

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

def __float__(self):
    return float(self.p) / self.q
#或者
def __float__(self):
    return 1.0 * self.p / self.q

6. @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方法,就不能對“屬性”賦值,這時,就可以建立一個只讀“屬性”。

請給Student類加一個grade屬性,根據 score 計算 A(>=80)、B、C(<60)。
分析
用 @property 修飾 grade 的 get 方法即可實現只讀屬性。

參考程式碼:

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
    @property
    def grade(self):
        if self.score < 60:
        #self.__score < 60 結果相同
            return 'C'
        if self.score < 80:
            return 'B'
        return 'A'
s = Student('Bob', 59)
print s.grade
s.score = 60
print s.grade
s.score = 99
print s.grade

7. __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__也能節省記憶體。

8. __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中,函式也是物件,物件和函式的區別並不顯著

相關文章