# 面向對象編程
面向對象編程是一種程序設計思想,它把對象作為程序的基本單元。
何為對象?對象指的的是數據和操作數據的函數的集合。
面向對象的程序設計把程序視為一組對象的集合,每個對象可以接收其他對象來的消息,并且處理這些消息。
計算機程序就是一系列的消息在各個對象之間的傳遞。
在Python中,所有數據類型都可以視為對象
我們以一個例子來說明面向過程和面向對象在程序流程上的不同之處。
假設我們要處理學生的成績表,為了表示一個學生的成績,面向過程的程序可以用一個 dict 表示:
```python
std1 = { 'name': 'Michael', 'score': 98 }
std2 = { 'name': 'Bob', 'score': 81 }
```
然后處理學生成績可以通過函數實現
```python
def print_score(std):
print('%s: %s' % (std['name'], std['score']))
```
而采用面向對象的程序設計思想,我們首先思考的不是程序的執行流程,而是構建一個Student對象,這個對象擁有 name和score兩個屬性(Property)。
如果要打印一個學生的程序,首先需要創建這個學生的對象,然后給對象發一個print_score的信息,讓對象自己把自己的數據打印出來。
```python
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def print_score(self):
print('%s: %s' % (self.name, self.score))
bart = Student('Bart Simpson', 59)
lisa = Student('Lisa Simpson', 87)
bart.print_score()
lisa.print_score()
```
面向對象的設計思想是從自然界中來的,因為在自然界中,類(Class)和實例(Instance)的概念是很自然的。
Class 是一種抽象概念,比如我們定義的 Class——Student,是指學生這個概念,而實例(Instance)則是一個個具體的 Student,比如,Bart Simpson 和 Lisa Simpson 是兩個具體的 Student。
所以,面向對象的設計思想是抽象出 Class,根據 Class 創建 Instance。
面向對象的抽象程度又比函數要高,因為一個 Class 既包含數據,又包含操作數據的方法。
## 類和實例
類是抽象的模板,而實例是根據類創建的一個具體的對象。
每個對象擁有相同的方法
```python
class Student(object):
pass
>>> bart = Student()
>>> bart
<__main__.Student object at 0x10a67a590>
>>> Student
<class '__main__.Student'>
```
class 后面緊接著是類名,即 Student,類名通常是大寫開頭的單詞
緊接著是 (object),表示該類是從哪個類繼承下來的,
通常,如果沒有合適的繼承類,就使用 object 類,這是所有類最終都會繼承的類
可以自由地給一個實例變量綁定屬性,比如,給實例 bart 綁定一個 name 屬性:
```python
>>> bart.name = 'Bart Simpson'
```
由于類可以起到模板的作用,因此,可以在創建實例的時候,把一些我們認為必須綁定的屬性強制填寫進去。
通過定義一個特殊的__init__方法,在創建實例的時候,就把 name,score 等屬性綁上去:
```python
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
```
注意特殊的 `__init__`方法前后分別有兩個下劃線
注意到__init__方法的第一個參數永遠是 self,表示創建的實例本身,因此,在__init__方法內部,就可以把各種屬性綁定到 self,因為 self 就指向創建的實例本身。
有了__init__方法,在創建實例的時候,就不能傳入空的參數了,必須傳入與__init__方法匹配的參數,
```python
>>> bart = Student('Bart Simpson', 59)
```
方法就是與實例綁定的函數,和普通函數不同,方法可以直接訪問實例的數據;
通過在實例上調用方法,我們就直接操作了對象內部的數據,但無需知道方法內部的實現細節。
和靜態語言不同,Python 允許對實例變量綁定任何數據,也就是說,對于兩個實例變量,雖然它們都是同一個類的不同實例,但擁有的變量名稱都可能不同
## 訪問限制
外部代碼可以自由地修改一個實例的 name、score 屬性:
如果要讓內部屬性不被外部訪問,可以把屬性的名稱前加上兩個下劃線__,在 Python 中,實例的變量名如果以__開頭,就變成了一個私有變量(private),只有內部可以訪問,外部不能訪問
但是如果外部代碼要獲取 name 和 score 怎么辦?
可以給 Student 類增加 get_name 和 get_score
如果又要允許外部代碼修改 score 怎么辦?可以再給 Student 類增加 set_score 方法,為什么要這么費勁呢?因為在方法中,可以對參數做檢查,避免傳入無效的參數
需要注意的是,在 Python 中,變量名類似__xxx__的,也就是以雙下劃線開頭,并且以雙下劃線結尾的,是特殊變量,特殊變量是可以直接訪問的,不是 private 變量,所以,不能用__name__、__score__這樣的變量名。
## 繼承和多態
在 OOP 程序設計中,當我們定義一個 class 的時候,可以從某個現有的 class 繼承,新的 class 稱為子類(Subclass),
而被繼承的 class 稱為基類、父類或超類(Base class、Super class)。
比如說直接從 Animal 類繼承:
```python
class Animal(object):
def run(self):
print('Animal is running...')
class Dog(Animal):
pass
class Cat(Animal):
pass
```
繼承有什么好處?最大的好處是子類獲得了父類的全部功能。
由于 Animial 實現了 run() 方法,因此,Dog 和 Cat 作為它的子類,什么事也沒干,就自動擁有了 run() 方法:
也可以對子類增加一些方法
繼承的第二個好處是我們可以覆蓋父類的 `run()`,在代碼運行的時候總會調用子類的 `run()`,這就是多態
```python
a = list() # a是list類型
b = Animal() # b是Animal類型
c = Dog() # c是Dog類型
```
比如說c 不僅僅是 Dog,c 還是 Animal!
為 Dog 是從 Animal 繼承下來的,當我們創建了一個 Dog 的實例 c 時,c同時是Dog和Animal
> 在繼承關系中,如果一個實例的數據類型是某個子類,那么它的數據類型可以被看做是父類
> 反過來不行。
Dog 可以看成 Animal,但 Animal 不可以看成 Dog。
為了理解多態的好處,我們可以編寫一個函數
```python
def run_twice(animal):
animal.run()
animal.run()
```
這個函數接收一個 `Animal` 類型的變量
當我們傳入 Animal 的實例時,run_twice() 就打印出:
```python
>>> run_twice(Animal())
Animal is running...
Animal is running...
```
當我們傳入 Dog 的實例時,run_twice() 就打印出:
```python
>>> run_twice(Dog())
Dog is running...
Dog is running...
```
新增一個 Animal 的子類,不必對 run_twice() 做任何修改,實際上,任何依賴 Animal 作為參數的函數或者方法都可以不加修改地正常運行
所以多態的好處在于,由于Animal類型有 run()方法,因此我們定義方法的時候,只需要接收 Animal類型就可以了,然后按照Animal類型進行操作,比如說傳入Animal類或者子類,就可以自動調用實際類型的 run()的方法。
所以,調用方只管調用,不管細節,而當我們新增一種 Animal 的子類時,只要確保 run() 方法編寫正確,不用管原來的代碼是如何調用的。這就是著名的 “開閉” 原則:
- 對擴展開放:允許新增 Animal 子類;
- 對修改封閉:不需要修改依賴 Animal 類型的 run_twice() 等函數。
```python
┌───────────────┐
│ object │
└───────────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Animal │ │ Plant │
└─────────────┘ └─────────────┘
│ │
┌─────┴──────┐ ┌─────┴──────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Dog │ │ Cat │ │ Tree │ │ Flower │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
```
## 靜態語言 vs 動態
對于靜態語言(例如 Java)來說,如果需要傳入 Animal 類型,則傳入的對象必須是 Animal 類型或者它的子類,否則,將無法調用 run() 方法。
對于 Python 這樣的動態語言來說,則不一定需要傳入 Animal 類型。我們只需要保證傳入的對象有一個 run() 方法就可以了:
這就是動態語言的 “鴨子類型”,它并不要求嚴格的繼承體系,一個對象只要 “看起來像鴨子,走起路來像鴨子”,那它就可以被看做是鴨子。
## 獲取對象信息
當我們拿到一個對象,如果想知道對象是什么類型,有哪些方法呢?
### 使用type()
可以使用 `type()` 來判斷對象類型。
基本類型都可以使用 `type()`來判斷
```python
>>> type(123)
<class 'int'>
>>> type('str')
<class 'str'>
>>> type(None)
<type(None) 'NoneType'>
```
如果一個變量指向函數或者類,也可以使用 `type()`
type() 返回的是變量對應的Class類型。
可以使用 `type('abc')==str` 這種格式來比較兩個變量的type類型是否相同。
如果要判斷一個對象是否是函數怎么辦?
可以使用 types 模塊中定義的常量
```python
>>> import types
>>> def fn():
... pass
...
>>> type(fn)==types.FunctionType
True
>>> type(abs)==types.BuiltinFunctionType
True
>>> type(lambda x: x)==types.LambdaType
True
>>> type((x for x in range(10)))==types.GeneratorType
True
```
### 使用 isinstance()
如果使用 `type()`來判斷class的繼承關系,所以我們可以使用 `isinstance()`
比如說,繼承關系為:
```python
object -> Animal -> Dog -> Husky
```
先創建3種類型
```python
>>> a = Animal()
>>> d = Dog()
>>> h = Husky()
```
然后可以判斷
```python
>>> isinstance(h, Dog)
True
```
h 雖然自身是 Husky 類型,但由于 Husky 是從 Dog 繼承下來的,所以,h 也還是 Dog 類型。換句話說,isinstance() 判斷的是一個對象是否是該類型本身,或者位于該類型的父繼承鏈上。
能用 type() 判斷的基本類型也可以用 isinstance() 判斷:
```python
>>> isinstance(b'a', bytes)
True
```
并且還可以判斷一個變量是否是某些類型中的一種,比如
```python
>>> isinstance([1, 2, 3], (list, tuple))
True
```
### 使用dir()
我們可以使用 `dir()`來獲得一個對象所有的屬性和方法。
```python
>>> dir('ABC')
['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']
```
類似 `__xxx__`的屬性和方法在Python中都有特殊用途。
比如,調用 len() 函數試圖獲取一個對象的長度,實際上,在 len() 函數內部,它自動去調用該對象的__len__() 方法,
然后,配合 getattr()、setattr() 以及 hasattr(),我們可以直接操作一個對象的狀態
```python
>>> hasattr(obj, 'x') # 有屬性'x'嗎?
True
>>> setattr(obj, 'y', 19) # 設置一個屬性'y'
>>> getattr(obj, 'y') # 獲取屬性'y'
19
```
如果試圖獲取不存在的屬性,會拋出 AttributeError 的錯誤:
還可以傳入一個default參數,如果屬性不存在,就返回默認值
```python
# 獲取屬性'z',如果不存在,返回默認值404
getattr(obj,'z',404)
```
同樣也可以獲取對象方法
```python
# 獲取屬性,并且賦值給fn
fn = getattr(obj , 'power')
# 直接調用
fn()
```
通過內置的一系列函數,我們可以對任意一個 Python 對象進行剖析,拿到其內部的數據。
> 要注意的是,只有在不知道對象信息的時候,我們才會去獲取對象信息。如果可以直接寫成 `sum = obj.x + obj.y` 就不要寫成 `sum = getattr(obj,'x') + getattr(obj,'y')`
我們可能會在如下場景中使用到這種特性
碧落說我們希望從文件流中讀取圖像,首先需要判斷fp對象是否存在read方法,如果存在,則說明該對象是一個流,如果不存在,則無法讀取。
```python
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None
```
Python 這種動態語言中,根據鴨子類型,有 read() 方法,不代表該 fp 對象就是一個文件流,它也可能是網絡流,也可能是內存中的一個字節流,但只要 read() 方法返回的是有效的圖像數據,就不影響讀取圖像的功能。
## 實例屬性和類屬性
由于Python是動態語言,根據類創建的實例可以任意綁定屬性。
給實例綁定屬性是通過實例變量,比如 `s.score = 90`
但是如果 `Student`這個類本身需要綁定一個屬性呢?可以直接在class里面定義
```python
class Student(object):
name = 'Student'
```
這樣,所有的類的實例都可以訪問這個屬性。
> 在編寫程序的時候,千萬不要對實例屬性和類屬性使用相同的名字,因為相同名稱的實例屬性將屏蔽掉類屬性,但是當你刪除實例屬性后,再使用相同的名稱,訪問到的將是類屬性。
小結
- 實例屬性屬于各個實例所有,互不干擾;
- 類屬性屬于類所有,所有實例共享一個屬性;
- 不要對實例屬性和類屬性使用相同的名字,否則將產生難以發現的錯誤。
## 使用__slots__
由于Python是動態語言,我們可以在創建一個class實例之后,動態的綁定任何屬性和方法。
綁定屬性
```python
>>> s = Student()
>>> s.name = 'Michael'
```
綁定方法
- 先定義一個函數作為實例的方法
```python
>>> def set_age(self, age): # 定義一個函數作為實例方法
... self.age = age
...
```
然后給實例綁定一個方法
```python
>>> from types import MethodType
>>> s.set_age = MethodType(set_age, s) # 給實例綁定一個方法
>>> s.set_age(25) # 調用實例方法
>>> s.age # 測試結果
25
```
為了給所有實例都綁定方法,可以給 class 綁定方法:
```python
>>> def set_score(self, score):
... self.score = score
...
>>> Student.set_score = set_score
```
> 動態綁定允許我們在程序運行的過程中動態給 class 加上功能,這在靜態語言中很難實現
如果我們要限制實例的屬性怎么辦,比如只允許對Student實例添加name和age屬性。
```python
class Student(object):
__slots__ = ('name', 'age') # 用tuple定義允許綁定的屬性名稱
```
使用__slots__要注意,__slots__定義的屬性僅對當前類實例起作用,對繼承的子類是不起作用的
## 使用 @property
在綁定屬性的時候,如果直接把屬性暴露出去,雖然寫起來簡單,但是沒有辦法檢查參數,而且別人容易修改屬性。
所以我們可以定義一個 `set_score`的方法來設置成績,通過一個 `get_score`來獲取成績。
但是使用這種方法略顯復雜。
我們可以使用@property這個內置的裝飾器,將一個方法變成屬性調用
```Python
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 and value > 100:
raise ValueError ('score must between 0 - 100')
self._score = value
```
把一個getter方法變成屬性,只需要加上@property 就可以了。
此時@property 本身又創建另一個裝飾器 @score.setter , 負責把一個setter方法變成屬性。
```python
>>> s = Student()
>>> s.score = 9999
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!
```
如果只需要定義只讀屬性,就可以只定義 getter方法,而不定義setter方法。
```python
class Student(object):
@property
def age(self):
return 2015 - self._birth
```
## 多重繼承
### 多重繼承
假設我們要實現4種動物。
- Dog - 狗狗;
- Bat - 蝙蝠;
- Parrot - 鸚鵡;
- Ostrich - 鴕鳥。
如果按照哺乳動物和鳥類分類:
```python
┌───────────────┐
│ Animal │
└───────────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Mammal │ │ Bird │
└─────────────┘ └─────────────┘
│ │
┌─────┴──────┐ ┌─────┴──────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Dog │ │ Bat │ │ Parrot │ │ Ostrich │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
```
如果按照“能飛”和“能跑”來歸類
```python
┌───────────────┐
│ Animal │
└───────────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Runnable │ │ Flyable │
└─────────────┘ └─────────────┘
│ │
┌─────┴──────┐ ┌─────┴──────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Dog │ │ Ostrich │ │ Parrot │ │ Bat │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
```
如果要把上面兩種分類方法都加起來的話,類的層次就復雜了:
- 哺乳類:能跑的哺乳類,能飛的哺乳類;
- 鳥類:能跑的鳥類,能飛的鳥類
```python
┌───────────────┐
│ Animal │
└───────────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Mammal │ │ Bird │
└─────────────┘ └─────────────┘
│ │
┌─────┴──────┐ ┌─────┴──────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ MRun │ │ MFly │ │ BRun │ │ BFly │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Dog │ │ Bat │ │ Ostrich │ │ Parrot │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
```
如果要再增加 “寵物類” 和 “非寵物類”,這么搞下去,類的數量會呈指數增長
所以可以采用 **多重繼承**
主要的類層次仍然按照哺乳類和鳥類設計:
```python
class Animal(object):
pass
# 大類:
class Mammal(Animal):
pass
class Bird(Animal):
pass
# 各種動物:
class Dog(Mammal):
pass
class Bat(Mammal):
pass
class Parrot(Bird):
pass
class Ostrich(Bird):
pass
```
然后為了給動物加上 `Runnable`和 `Flyable`的功能
只需要先定義好這兩個類
```python
class Runnable(object):
def run(self):
print('Running...')
class Flyable(object):
def fly(self):
print('Flying...')
```
對于需要 Runnable的動物,就多繼承一個 Runnable
```python
class Dog(Mammal, Runnable):
pass
```
通過多重繼承,一個子類就可以同時獲得多個父類的所有功能。
### Mixln
如果一個類需要多種功能,可以通過多重繼承來實現,這種設計通常稱為 Mixln
我們可以把Runnable 和 Flyable 改為 RunnableMixIn 和 FlyableMixIn。類似的,你還可以定義出肉食動物 CarnivorousMixIn 和植食動物 HerbivoresMixIn,讓某個動物同時擁有好幾個 MixIn:
```python
class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
pass
```
Mixln 的目的就是給一個類增加多個功能
Python自帶庫中,有 TCPServer 和 UDPServer 這兩類網絡服務
要同時服務多個用戶就必須使用多進程或多線程模型,這兩種模型由 ForkingMixIn 和 ThreadingMixIn 提供
```python
# 編寫一個多進程模式的 TCP 服務,定義如下:
class MyTCPServer(TCPServer, ForkingMixIn):
pass
# 編寫一個多線程模式的 UDP 服務,定義如下:
class MyUDPServer(UDPServer, ThreadingMixIn):
pass
```
如果搞一個更先進的協程模型,可以編寫一個 CoroutineMixIn:
```python
class MyTCPServer(TCPServer, CoroutineMixIn):
pass
```
### 小結
由于 Python 允許使用多重繼承,因此,MixIn 就是一種常見的設計。
只允許單一繼承的語言(如 Java)不能使用 MixIn 的設計。
## 定制類
形如__xxx__的變量或者函數名就要注意,這些在 Python 中是有特殊用途的。
### 控制打印實例時的信息
比如說我們打印一個實例
```python
>>> print(Student('Michael'))
<__main__.Student object at 0x109afb190>
```
打印出來的東西可以格式化嗎?
只需要重新定義 `__str__()` 方法
```python
>>> 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)
>>> s = Student('Michael')
>>> s
<__main__.Student object at 0x109afb310>
```
如果直接敲變量,仍然還是遠離的格式,因為直接顯示變量調用的是 `__repr__()`
```python
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Student object (name=%s)' % self.name
__repr__ = __str__
```
可以直接將 `__str__`賦到 `__repr__`里面。
### 定義可迭代的類
如果一個類想被用于 for ... in 循環,類似 list 或 tuple 那樣,就必須實現一個__iter__() 方法,該方法返回一個迭代對
for循環會不斷調用該迭代對象的 __next()__方法
```python
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 # 返回下一個值
# 把 Fib 實例作用于 for 循環:
>>> for n in Fib():
... print(n)
...
```
### 使實例可以索引、切片
修改了 `__iter__`方法之后,雖然可以作用for循環了,但是當成list來使用還是不行。
如果要使用切片、索引等功能,可以實現 `__getitem__()`
```python
class Fib(object):
def __getitem__(self, n):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
```
這樣就可以使用下標來訪問數列中的任意一項了。
但是list有切片方法,Fib仍然不具備。
因為 `__getitem__()` 傳入的參數可能是一個int,也可能是一個切片對象 slice
```python
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
```
不過沒有對step參數進行處理,也沒有對負數做處理。
### 動態返回屬性或者方法
正常情況下,當我們調用類的方法或屬性時,如果不存在,就會報錯,要避免這個問題,除了可以加上一個 score 屬性外,Python 還有另一個機制,那就是寫一個__getattr__() 方法,動態返回一個屬性。
```python
class Student(object):
def __init__(self):
self.name = 'Michael'
def __getattr__(self, attr):
if attr=='score':
return 99
```
當調用不存在的屬性時,比如 score,Python 解釋器會試圖調用__getattr__(self, 'score') 來嘗試獲得屬性,這樣,我們就有機會返回 score 的值:
```python
>>> s.score
99
```
> 注意,只有在沒有找到屬性的情況下,才調用__getattr__,已有的屬性,比如 name,不會在__getattr__中查找。
這么操作之后,任意調用都會返回 None,這是因為我們定義的__getattr__默認返回就是 None
如果要讓 class 只響應特定的幾個屬性,我們就要按照約定,拋出 AttributeError 的錯誤:
```python
class Student(object):
def __getattr__(self, attr):
if attr=='age':
return lambda: 25
raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)
```
這種完全動態調用的特性有什么實際作用呢?
作用就是,可以針對完全動態的情況作調用。
比如說現在很多網站都使用REST API
調用 API 的 URL 類似:
- http://api.server/user/friends
- http://api.server/user/timeline/list
如果要給每個 URL 對應的 API 都寫一個方法,并不便于維護
這樣的話,我們可以實現鏈式調用
```python
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'
```
### 把類看做函數
如果要調用實例方法,我們會使用 `instance.method()`來調用,能不能直接在的實例上實現調用呢?比如說
```python
>>> s = Student('Michael')
>>> s() # self參數不要傳入
```
只需要定義一個__call__() 方法,就可以直接對實例進行調用
```python
class Student(object):
def __init__(self, name):
self.name = name
def __call__(self):
print('My name is %s.' % self.name)
```
這樣,就方法對函數進行調用一樣,所以完全可以把函數看作對象
如果你把對象看成函數,那么函數本身其實也可以在運行期動態創建出來,因為類的實例都是運行期創建出來的,這么一來,我們就模糊了對象和函數的界限。
那么,怎么判斷一個變量是對象還是函數呢?其實,更多的時候,我們需要判斷一個對象是否能被調用,能被調用的對象就是一個 Callable 對象,比如函數和我們上面定義的帶有__call__() 的類實例:
```python
>>> callable(Student())
True
>>> callable([1, 2, 3])
False
```
接著上一小節的問題,現在有REST API 會把參數放到 URL 中,比如
```python
GET /users/:user/repos
```
調用時,需要把:user 替換為實際用戶名
我們希望能如此調用:
```python
Chain().users('michael').repos
```
那么我們可以如此定義
```python
class Chain(object):
def __init__(self, path=''):
self.__path = path
def __getattr__(self, path):
return Chain('%s/%s' % (self.__path, path))
def __call__(self, path):
return Chain('%s/%s' % (self.__path, path))
def __str__(self):
return self.__path
__repr__ = __str__
print(Chain().users('michael').repos) # /users/michael/repos
```
Step 1: 實例化
```python
Chain() # 實例化
```
Step 2: `Chain().users`
由于沒有給實例傳入初始化對應屬性的具體信息,從而自動調用__getattr__()函數,
```python
Chain().users = Chain('\users') # 這是重建實例
```
Step 3: `Chain().users('michael')`
這是對實例直接調用,相當于調用普通函數一樣 ,也就是Chain('\users\michael'), 這樣會重建實例,覆蓋掉Chain('\users'),
同時需要注意一旦返回了一個新的實例,就會執行__init__方法;
我們可以記 `renew = Chain('\users\michael')`, 此時新實例的屬性`renew.__path = \users\michael`
```python
Chain().users('michael') = Chain('\users')('michael')
```
Step 4: `Chain().users('michael').repos`
同樣會查詢renew實例的屬性repos,由于沒有這一屬性,就會執行__getattr__()函數,再一次返回新的實例Chain('\users\michael\repos')并且覆蓋點之前的實例,
這里記` trinew =Chain('\users\michael\repos')`
## 使用枚舉類
當我們需要定義常量的時候,一個辦法是使用大寫變量,好處是簡單,缺點是類型是int,而且同樣也是變量
其實可以為枚舉類型定義一個class類型,然后每個常量就是class 的唯一實例。
```python
from enum import Enum
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
```
如果要引用一個常量: `Month.Jan`
如果要列舉所有的成員:
```python
for name, member in Month.__members__.items():
print(name, '=>', member, ',', member.value)
```
value 屬性則是自動賦給成員的 int 常量,默認從 1 開始計數。
如果需要更精確地控制枚舉類型,可以從 Enum 派生出自定義類:
```python
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 裝飾器可以幫助我們檢查保證沒有重復值。
如果要訪問枚舉類型可以使用:
- Weekday.Mon
- Weekday['Mon']
- Weekday(1)
```python
>>> print(Weekday.Tue.value)
2
>>> for name, member in Weekday.__members__.items():
... print(name, '=>', member)
...
```
## 使用元類
### 查看變量的類型
與靜態語言不同,動態語言的函數和類的定義不是在編譯的時候指定,而是在運行的時候動態創建。
比如
```python
class Hello(object):
def hello(self, name='world'):
print('Hello, %s.' % name)
```
當 Python 解釋器載入 hello 模塊時,就會依次執行該模塊的所有語句,執行結果就是動態創建出一個 Hello 的 class 對象
```python
>>> from hello import Hello
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class 'hello.Hello'>
```
我們可以使用 `type()`來查看一個變量的類型,所以
- 因為Hello是一個class,所以他的類型就是 type
- 因為hello 是一個實例,所以它的類型就是class Hello
同時 `type()`函數還可以創建新的類型,而無需通過 `class Hello (object)`
要創建一個class對象,type()函數需要傳入三個參數
- class的名稱
- 繼承的父類集合,注意 Python 支持多重繼承,如果只有一個父類,別忘了 tuple 的單元素寫法;
- class的方法名稱與函數綁定,比如我們把函數 fn 綁定到方法名 hello 上
```python
>>> def fn(self, name='world'): # 先定義函數
... print('Hello, %s.' % name)
...
>>> Hello = type('Hello', (object,), dict(hello=fn)) # 創建Hello class
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>
```
通過 type() 函數創建的類和直接寫 class 是完全一樣的,因為 Python 解釋器遇到 class 定義時,僅僅是掃描一下 class 定義的語法,然后調用 type() 函數創建出 class。
正常情況下,我們都用 class Xxx... 來定義類,但是,type() 函數也允許我們動態創建出類來
也就是說,動態語言本身支持運行期動態創建類,這和靜態語言有非常大的不同,要在靜態語言運行期創建類,必須構造源代碼字符串再調用編譯器,或者借助一些工具生成字節碼實現,本質上都是動態編譯,會非常復雜。
### metaclass:動態創建類
除了使用 type() 動態創建類以外,要控制類的創建行為,還可以使用 metaclass。
我們如果要創建實例,需要先創建出類,如果我們想創建出類,則需要先根據metaclass(元類)創建出類。
也就是先定義 metaclass,就可以創建類,最后創建實例。
換句話說可以把類看成是 metaclass 創建出來的 “實例”。
不過正常情況下,不會碰到需要使用 metaclass 的情況
我們以給自定義的MyList增加一個add方法為例
定義 ListMetaclass,按照默認習慣,metaclass 的類名總是以 Metaclass 結尾,以便清楚地表示這是一個 metaclass:
```python
# metaclass是類的模板,所以必須從`type`類型派生:
class ListMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(cls, name, bases, attrs)
```
__new__() 方法接收到的參數依次是:
- 當前準備創建的類的對象;
- 類的名字;
- 類繼承的父類集合;
- 類的方法集合。
下面我們可以使用 ListMetaclass來創建新的類
```python
class MyList(list, metaclass=ListMetaclass):
pass
```
它指示 Python 解釋器在創建 MyList 時,要通過 ListMetaclass.__new__() 來創建
測試一下MyList是否可以調用 add() 方法
```python
>>> L = MyList()
>>> L.add(1)
>> L
[1]
```
那么動態修改到底有什么意義呢?
ORM就是一個典型的例子。
ORM 全稱 “Object Relational Mapping”,即對象 - 關系映射,就是把關系數據庫的一行映射為一個對象,也就是一個類對應一個表,這樣,寫代碼更簡單,不用直接操作 SQL 語句。
要編寫一個 ORM 框架,所有的類都只能動態定義,因為只有使用者才能根據表的結構定義出對應的類來。
在編寫底層框架之前,建議先把調用接口寫出來,比如使用者如果想使用這個ORM框架,可以這樣
```python
class User(Model):
# 定義類的屬性到列的映射
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
# 創建一個實例
u = User(id=12345,name='Michael',email='duyang@cetcbigdata.com',password='my-pwd')
u.save()
```
父類 Model 和屬性類型 StringField、IntegerField 是由 ORM 框架提供的,剩下的魔術方法比如 save() 全部由 metaclass 自動完成
可以看出ORM 的使用者用起來異常簡單。
那么如何實現該ORM呢?
首先來定義 Field 類,它負責保存數據庫表的字段名和字段類型:
```python
class Field(object):
def __init__(self, name, column_type):
self.name = name
self.column_type = column_type
def __str__(self):
return '<%s:%s>' % (self.__class__.__name__, self.name)
```
然后在此基礎上定義各種類型的 Field,比如 StringField,IntegerField 等等:
```python
class StringField(Field):
def __init__(self, name):
super(StringField, self).__init__(name, 'varchar(100)')
class IntegerField(Field):
def __init__(self, name):
super(IntegerField, self).__init__(name, 'bigint')
```
然后就要編寫ModelMetaclass了。
```python
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
# 如果當前創建對象為Model 的實例,則不用做操作,因為Model沒有屬性
if name=='Model':
return type.__new__(cls, name, bases, attrs)
print('Found model: %s' % name)
mappings = dict()
# attrs應該是存放了類的所有屬性以及方法的字典
# 這里k是屬性或方法的類型,v是屬性或方法的值
for k, v in attrs.items():
# 如果v是Field的實例對象
if isinstance(v, Field):
print('Found mapping: %s ==> %s' % (k, v))
mappings[k] = v
# 最后遍歷mappings,從attrs中pop
for k in mappings.keys():
attrs.pop(k)
# 為正在創建的對象 添加兩個屬性 __mappings__和 __table__
attrs['__mappings__'] = mappings # 保存屬性和列的映射關系
attrs['__table__'] = name # 假設表名和類名一致
return type.__new__(cls, name, bases, attrs)
```
以及基類 Model:
```python
class Model(dict, metaclass=ModelMetaclass):
def __init__(self, **kw):
super(Model, self).__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Model' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
def save(self):
fields = []
params = []
args = []
for k, v in self.__mappings__.items():
fields.append(v.name)
params.append('?')
args.append(getattr(self, k, None))
sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
print('SQL: %s' % sql)
print('ARGS: %s' % str(args))
```
當用戶定義一個 class User(Model) 時,Python 解釋器首先在當前類 User 的定義中查找 metaclass,如果沒有找到,就繼續在父類 Model 中查找 metaclass,找到了,就使用 Model 中定義的 metaclass 的 ModelMetaclass 來創建 User 類,
也就是說,**metaclass 可以隱式地繼承到子類,但子類自己卻感覺不到。**
在 ModelMetaclass 中,一共做了幾件事情:
- 排除掉對 Model 類的修改;
- 在當前類(比如 User)中查找定義的類的所有屬性,如果找到一個 Field 屬性,就把它保存到一個__mappings__的 dict 中,同時從類屬性中刪除該 Field 屬性,否則,容易造成運行時錯誤(實例的屬性會遮蓋類的同名屬性);
- 把表名保存到__table__中,這里簡化為表名默認為類名。
- 在 Model 類中,就可以定義各種操作數據庫的方法,比如 save(),delete(),find(),update 等等。
- 我們實現了 save() 方法,把一個實例保存到數據庫中。因為有表名,屬性到字段的映射和屬性值的集合,就可以構造出 INSERT 語句。
編寫代碼試試:
```python
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
u.save()
```
輸出如下:
```python
Found model: User
Found mapping: email ==> <StringField:email>
Found mapping: password ==> <StringField:password>
Found mapping: id ==> <IntegerField:uid>
Found mapping: name ==> <StringField:username>
SQL: insert into User (password,email,username,id) values (?,?,?,?)
ARGS: ['my-pwd', 'test@orm.org', 'Michael', 12345]
```