重拾Python學習(六)----------面向物件高階程式設計
使用__slots__
果我們想要限制例項的屬性,比如,只允許對Student
例項新增name和age屬性。
class Student(object):
__slots__ = ('name', 'age') # 用tuple定義允許繫結的屬性名稱
定義一個特殊的__slots__
變數,來限制該class例項能新增的屬性:
>>> s = Student() # 建立新的例項
>>> s.name = 'Michael' # 繫結屬性'name'
>>> s.age = 25 # 繫結屬性'age'
>>> s.score = 99 # 繫結屬性'score'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'
對繼承的子類是不起作用的:
>>> class GraduateStudent(Student):
... pass
...
>>> g = GraduateStudent()
>>> g.score = 9999
使用@property
裝飾器(decorator)可以給函式動態加上功能,對於類的方法,裝飾器一樣起作用。Python內建的@property
裝飾器就是負責把一個方法變成屬性呼叫的:
class Student(object):
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value
把一個getter方法變成屬性,只需要加上@property
就可以了,此時,@property
本身又建立了另一個裝飾器@score.setter
,負責把一個setter方法變成屬性賦值
>>> s = Student()
>>> s.score = 60 # OK,實際轉化為s.set_score(60)
>>> s.score # OK,實際轉化為s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!
多重繼承
在設計類的繼承關係時,通常,主線都是單一繼承下來的,但是,如果需要“混入”額外的功能,通過多重繼承就可以實現,這種設計通常稱之為MixIn。
class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
pass
舉個例子,Python自帶了TCPServer
和UDPServer
這兩類網路服務,而要同時服務多個使用者就必須使用多程序或多執行緒模型,這兩種模型由ForkingMixIn
和ThreadingMixIn
提供
class MyTCPServer(TCPServer, ForkingMixIn):
pass
class MyUDPServer(UDPServer, ThreadingMixIn):
pass
定製類
__slots__
我們已經知道怎麼用了,__len__()
方法我們也知道是為了能讓class作用於len()
函式。
Python的class中還有許多這樣有特殊用途的函式,可以幫助我們定製類。
__str__
列印一個例項:
>>> class Student(object):
... def __init__(self, name):
... self.name = name
... def __str__(self):
... return 'Student object (name: %s)' % self.name
...
>>> print(Student('Michael'))
Student object (name: Michael)
【注】不用print
,打印出來的例項:
>>> s = Student('Michael')
>>> s
<__main__.Student object at 0x109afb310>
這是因為直接顯示變數呼叫的不是__str__()
,而是__repr__()
,兩者的區別是__str__()
返回使用者看到的字串,而__repr__()
返回程式開發者看到的字串,也就是說,__repr__()
是為除錯服務的。(解決: __repr__ = __str__
)
__iter__
一個類想被用於for ... in
迴圈,類似list或tuple那樣,就必須實現一個__iter__()
方法,該方法返回一個迭代物件。Python的for迴圈就會不斷呼叫該迭代物件的__next__()
方法拿到迴圈的下一個值,直到遇到StopIteration錯誤時退出迴圈。
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1 # 初始化兩個計數器a,b
def __iter__(self):
return self # 例項本身就是迭代物件,故返回自己
def __next__(self):
self.a, self.b = self.b, self.a + self.b # 計算下一個值
if self.a > 100000: # 退出迴圈的條件
raise StopIteration()
return self.a # 返回下一個值
>>> for n in Fib():
... print(n)
...
1
1
2
3
5
...
46368
75025
__getitem__
要表現得像list那樣按照下標取出元素,需要實現__getitem__()
方法:
class Fib(object):
def __getitem__(self, n):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
>>> f = Fib()
>>> f[0]
1
>>> f[1]
1
>>> f[2]
2
>>> f[3]
3
>>> f[10]
89
>>> f[100]
573147844013817084101
新增切片方法:
class Fib(object):
def __getitem__(self, n):
if isinstance(n, int): # n是索引
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
if isinstance(n, slice): # n是切片
start = n.start
stop = n.stop
if start is None:
start = 0
a, b = 1, 1
L = []
for x in range(stop):
if x >= start:
L.append(a)
a, b = b, a + b
return L
>>> f = Fib()
>>> f[0:5]
[1, 1, 2, 3, 5]
>>> f[:10]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
沒有對step引數作處理,也沒有對負數作處理,所以,要正確實現一個__getitem__()
還是有很多工作要做的
__getattr__
正常情況下,當我們呼叫類的方法或屬性時,如果不存在,就會報錯,要避免這個錯誤,除了可以加上這個屬性外,寫一個__getattr__()
方法,動態返回一個屬性
class Student(object):
def __init__(self):
self.name = 'Michael'
def __getattr__(self, attr):
if attr=='score':
return 99
當呼叫不存在的屬性時,比如score,Python直譯器會試圖呼叫__getattr__(self, 'score')
來嘗試獲得屬性
>>> s = Student()
>>> s.name
'Michael'
>>> s.score
99
只有在沒有找到屬性的情況下,才呼叫__getattr__
,已有的屬性,比如name,不會在__getattr__
中查詢。
這實際上可以把一個類的所有屬性和方法呼叫全部動態化處理了,不需要任何特殊手段。這種完全動態呼叫的特性有什麼實際作用呢?作用就是,可以針對完全動態的情況作呼叫。
利用完全動態的__getattr__,我們可以寫出一個鏈式呼叫:
class Chain(object):
def __init__(self, path=''):
self._path = path
def __getattr__(self, path):
return Chain('%s/%s' % (self._path, path))
def __str__(self):
return self._path
__repr__ = __str__
>>> Chain().status.user.timeline.list
'/status/user/timeline/list'
如果要寫SDK,給每個URL對應的API都寫一個方法,那得累死,而且,API一旦改動,SDK也要改。這樣,無論API怎麼變,SDK都可以根據URL實現完全動態的呼叫,而且,不隨API的增加而改變!
__call__
當我們呼叫例項方法時,我們用instance.method()
來呼叫,只需要定義一個__call__()
方法,就可以直接對例項進行呼叫
class Student(object):
def __init__(self, name):
self.name = name
def __call__(self):
print('My name is %s.' % self.name)
呼叫方式如下:
>>> s = Student('Michael')
>>> s() # self引數不要傳入
My name is Michael.
使用列舉類
Python提供了Enum類來實現每個常量都是class的一個唯一例項
from enum import Enum
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
更精確地控制列舉型別,可以從Enum派生出自定義類:
from enum import Enum, unique
@unique
class Weekday(Enum):
Sun = 0 # Sun的value被設定為0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6
@unique
裝飾器可以幫助我們檢查保證沒有重複值。
>>> day1 = Weekday.Mon
>>> print(day1)
Weekday.Mon
>>> print(Weekday.Tue)
Weekday.Tue
>>> print(Weekday['Tue'])
Weekday.Tue
>>> print(Weekday.Tue.value)
2
>>> print(day1 == Weekday.Mon)
True
>>> print(day1 == Weekday.Tue)
False
>>> print(Weekday(1))
Weekday.Mon
>>> print(day1 == Weekday(1))
True