面向對象編程——Object Oriented Programming,簡稱OOP,是一種程序設計思想。OOP把對象作為程序的基本單元,一個對象包含了數據和操作數據的函數。
面向過程的程序設計把計算機程序視為一系列的命令集合,即一組函數的順序執行。為了簡化程序設計,面向過程把函數繼續切分為子函數,即把大塊函數通過切割成小塊函數來降低系統的復雜度。
而面向對象的程序設計把計算機程序視為一組對象的集合,而每個對象都可以接收其他對象發過來的消息,並處理這些消息,計算機程序的執行就是一系列消息在各個對象之間傳遞。
面向對象最重要的概念就是類和實例。
在Python中,定義類是通過class
關鍵字:
class Student(object):
pass
class
後面緊接著是類名,即Student
,類名通常是大寫開頭的單詞,緊接著是(object)
,表示該類是從哪個類繼承下來的,繼承的概念我們後面再講,通常,如果沒有合適的繼承類,就使用object
類,這是所有類最終都會繼承的類。
創建實例是通過類名+()實現:
huffie = Student()
變量bart
指向的就是一個Student
的實例
(1)__init__方法
通過定義一個特殊的__init__
方法,在創建實例的時候,就把name
,score
等屬性綁上去:
class Student(object):
def __init__(self, name, score):
self.name = name
self.age = age
__init__
方法的第一個參數永遠是self
,表示創建的實例本身,因此,在__init__
方法內部,就可以把各種屬性綁定到self
,因為self
就指向創建的實例本身。
有了__init__
方法,在創建實例的時候,就不能傳入空的參數了,必須傳入與__init__
方法匹配的參數,但self
不需要傳,Python解釋器自己會把實例變量傳進去:
huffie = Student('huffie', 22)
(2)類的方法
Student
實例本身就擁有這些數據,要訪問這些數據,就沒有必要從外面的函數去訪問,可以直接在類的內部定義訪問數據的函數,這樣,就把“數據”給封裝起來了。這些封裝數據的函數是和Student
類本身是關聯起來的,我們稱之為類的方法。
要定義一個方法,除了第一個參數是self
外,其他和普通函數一樣。
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))
要調用一個方法,只需要在實例變量上直接調用,除了self
不用傳遞,其他參數正常傳入:
bart.print_score()
(3)訪問限制
類的內部可以有屬性和方法,外部代碼通過實例變量的方法來操作數據,這樣就隱藏了內部的復雜邏輯。
但外部代碼還是可以自由的修改實例的屬性。
要讓內部屬性不被外部訪問,可以把屬性的名稱前加上兩個下劃線__
。在Python中,實例的變量名如果以__
開頭,就變成了一個私有變量(private),只有內部可以訪問,外部不能訪問
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))
這樣就確保了外部代碼不能隨意修改對象的屬性,使代碼更穩定可靠。
如果外部代碼要獲取name和score,或者要修改score,可以給Student類增加get_name
和get_score
這樣的方法:
class Student(object):
...
def get_name(self):
return self.__name
def get_score(self):
return self.__score
def set_score(self, score):
self.__score = score
(4)類屬性和實例屬性
給實例綁定屬性的方法是通過實例變量,或者通過self
變量:
class Student(object):
def __init__(self, name):
self.name = name
s = Student('Bob')
s.score = 90
給類綁定屬性可以直接在class中定義屬性。
class Student(object):
name = 'Student'
類屬性的特性參考以下測試:
>>> class Student(object):
... name = 'Student'
...
>>> s = Student() # 創建實例s
>>> print(s.name) # 打印name屬性,因為實例並沒有name屬性,所以會繼續查找class的name屬性
Student
>>> print(Student.name) # 打印類的name屬性
Student
>>> s.name = 'Michael' # 給實例綁定name屬性
>>> print(s.name) # 由於實例屬性優先級比類屬性高,因此,它會屏蔽掉類的name屬性
Michael
>>> print(Student.name) # 但是類屬性並未消失,用Student.name仍然可以訪問
Student
>>> del s.name # 如果刪除實例的name屬性
>>> print(s.name) # 再次調用s.name,由於實例的name屬性沒有找到,類的name屬性就顯示出來了
Student
從上面的例子可以看出,在編寫程序的時候,千萬不要對實例屬性和類屬性使用相同的名字,因為相同名稱的實例屬性將屏蔽掉類屬性,但是當你刪除實例屬性後,再使用相同的名稱,訪問到的將是類屬性。
定義一個class的時候,可以從某個現有的class繼承,新的class稱為子類(Subclass),而被繼承的class稱為基類、父類或超類(Base class、Super class)。
(1)繼承的優點1:子類擁有父類的屬性和方法
繼承最大的好處是子類獲得了父類的全部功能。即子類自動擁有父類的所有共有屬性和方法。
舉例如下,對於Dog
來說,Animal
就是它的父類,對於Animal
來說,Dog
就是它的子類。Cat
和Dog
類似。
class Animal(object):
def run(self):
print('Animal is running...')
class Dog(Animal):
pass
class Cat(Animal):
pass
(2)繼承的優點2:子類可以修改父類的方法
當子類和父類都存在相同的run()
方法時,我們說,子類的run()
覆蓋了父類的run()
,在代碼運行的時候,總是會調用子類的run()
。這樣,我們就獲得了繼承的另一個好處:多態。
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')
首先說明,當我們定義一個class的時候,我們實際上就定義了一種數據類型。我們定義的數據類型和Python自帶的數據類型,比如str、list、dict沒什麼兩樣。
在繼承關系中,如果一個實例的數據類型是某個子類,那它的數據類型也可以被看做是父類。
(1)多態的優點
當新增子類時,任何依賴父類作為參數的函數或方法都可以不加修改的正常運行。即
父類
的子類;父類
類型的函數。(例如def func(父類)
)(2)動態語言
定義一個Animal類:
class Animal(object):
def run(self):
print('Animal is running...')
參考如下依賴Animal
類的函數,其中調用了Animal
類的方法run
:
def run_twice(animal):
animal.run()
animal.run()
對於Python這樣的動態語言來說,則不一定需要傳入Animal
類型。我們只需要保證傳入的對象有一個run()
方法就可以了:
class Timer(object):
def run(self):
print('Start...')
即使Timer
類和Animal
類毫無關系,但只要他們都有run
方法就可以使用依賴Animal
的函數。
這就是動態語言的“鴨子類型”,它並不要求嚴格的繼承體系,一個對象只要“看起來像鴨子,走起路來像鴨子”,那它就可以被看做是鴨子。
animal = Animal()
timer = Timer()
run_twice(animal)
run_twice(timer)
前面介紹了set
和get
方法用來保證代碼的穩定性,但這種調用方式過於復雜。因此…對於追求完美的Python程序員來說,參數的使用必須要簡單!
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
就可以了。
把一個setter方法變成屬性,只需要加上@func_name.setter
就可以了
只定義getter方法,不定義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!
(1)slot
定義類:
class Student(object):
pass
為實例綁定屬性:
>>> s = Student()
>>> s.name = 'Michael' # 動態給實例綁定一個屬性
為實例綁定方法:
>>> def set_age(self, age): # 定義一個函數作為實例方法
... self.age = age
...
>>> from types import MethodType
>>> s.set_age = MethodType(set_age, s) # 給實例綁定一個方法
為類綁定方法,所有實例均可調用:
>>> def set_score(self, score):
... self.score = score
...
>>> Student.set_score = set_score
上面可以看到,在定義了實例後可以任意添加實例的屬性,但如果我們想要限制實例的屬性怎麼辦。
Python允許在定義class的時候,定義一個特殊的__slots__
變量,來限制該class實例能添加的屬性:
class Student(object):
__slots__ = ('name', 'age') # 用tuple定義允許綁定的屬性名稱
測試:
>>> 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'
使用__slots__
要注意,__slots__
定義的屬性僅對當前類實例起作用,對繼承的子類是不起作用的。
(2)str
我們先定義一個Student
類,打印一個實例:
>>> class Student(object):
... def __init__(self, name):
... self.name = name
...
>>> s = Student('Michael')
>>> print(s)
<__main__.Student object at 0x109afb190>
>>> s
<__main__.Student object at 0x109afb190>
打印出一堆<__main__.Student object at 0x109afb190>
,不好看。
怎麼才能打印得好看呢?只需要定義好__str__()
方法和__repr__
方法,返回一個好看的字符串就可以了。
>>> class Student(object):
... def __init__(self, name):
... self.name = name
... def __str__(self):
... return 'Student object (name: %s) from __str__' % self.name
... def __repr__(self):
... return 'Student object (name: %s) from __repr__' % self.name
...
>>> s = Student('Michael')
>>> print(s)
Student object (name: Michael) from __str__
>>> s
Student object (name: Michael) from __repr__
這樣打印出來的實例,不但好看,而且容易看出實例內部重要的數據。
簡便寫法:__repr__ = __str__
(3)getattr
正常情況下,當我們調用類的方法或屬性時,如果不存在,就會報錯。
要避免這個錯誤,除了可以加上一個score
屬性外,Python還有另一個機制,那就是寫一個__getattr__()
方法,動態返回一個屬性。修改如下:
class Student(object):
def __init__(self):
self.name = 'Michael'
def __getattr__(self, attr):
if attr=='score':
return 99
if attr=='age':
return lambda: 25
raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)
當調用不存在的屬性或方法時,就會有返回:
>>> s = Student()
>>> s.name
'Michael'
>>> s.score
99
>>> s.age()
25
(4)call
任何類,只需要定義一個__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.
__call__()
還可以定義參數。對實例進行直接調用就好比對一個函數進行調用一樣,所以你完全可以把對象看成函數,把函數看成對象,因為這兩者之間本來就沒啥根本的區別。
當我們需要定義常量時,一個辦法是用大寫變量通過整數來定義,好處是簡單,缺點是類型是int
,並且仍然是變量。
更好的方法是為這樣的枚舉類型定義一個class類型,然後,每個常量都是class的一個唯一實例。Python提供了Enum
類來實現這個功能:
from enum import Enum
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
這樣我們就獲得了Month
類型的枚舉類,可以直接使用Month.Jan
來引用一個常量。
for name, member in Month.__members__.items():
print(name, '=>', member, ',', member.value)
value
屬性則是自動賦給成員的int
常量,默認從1
開始計數。
如果需要更精確地控制枚舉類型,可以從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