面向對象編程
面向對象編程的三個基礎概念是數據封裝(類)、繼承和多態,在這三個記住概念的基礎上,進一步擴展出更加高級的多重繼承、定製類和元類等概念,這些概念和相關的功能使得python稱爲一種強大的面向對象的高級動態語言。
數據封裝
面向對象編程中最重要的概念就是類和實例,類是對象的抽象的概念,而實例是對象的具象的結果。python中通過class關鍵字定義一個類,
class person(object):
def __init__(self, name, age):
self.name = name
self.age = age
def show(self):
print("name: %s, age: %s" % (self.name, self.age))
laowang = person("laowang", 20)
laowang .show()
___________
輸出:
name: laowang , age: 20
在類中爲了代碼安全性的要求,有些屬性和方法不應該被外部直接獲取到,需要對這些屬性或方法設置訪問限制,這一點和其他面向對象編程語言是一樣的,即屬性和方法有private
和public
的區分。在python不使用這兩個關鍵字來設置訪問權限,而是直接在屬性名或函數名之前添加兩個下劃線__
來表示這個屬性是私有的屬性,不允許外部直接修改,如果需要修改該屬性,可以增加一個專門的函數用來修改該屬性。
class person(object):
def __init__(self, name, age):
self.__name = name
self.__age = age
def show(self):
print("name: %s, age: %s" % (self.__name, self.__age))
def set_age(self, age):
self.__age = age
qq = person("laowang", 20)
qq.show()
qq.set_age(90)
qq.show()
____________
輸出:
name: laowang, age: 20
name: laowang, age: 90
設置的訪問限制的屬性無法通過類似qq.__age
的形式直接訪問,因爲在解釋器中它的名稱已經被改變爲qq._person__age
,通過這一變量可以訪問到該屬性,但是並不建議這麼做,因爲不同版本的解釋器的修改方法可能不一致,這會導致代碼的不兼容,同時這麼做也是不安全的。
如果在設置的訪問限制之後,再直接使用__age
強制的修改屬性的內容,此時其實是新建了一個類的屬性,原先類中的__age
其實並未改變。
class person(object):
def __init__(self, name, age):
self.__name = name
self.__age = age
def show(self):
print("name: %s, age: %s" % (self.__name, self.__age))
def set_age(self, age):
self.__age = age
qq = person("laowang", 20)
qq.show()
qq.__age = 50
qq.__age
qq.show()
qq.set_age(90)
qq.show()
————————————————————
輸出:
name: laowang, age: 20
name: laowang, age: 20
name: laowang, age: 90
繼承和多態
當我們新建一個類時,可以從一個現有的類上繼承,這個新建的類被稱爲子類,這個被繼承的類被稱爲子類的基類、父類或超類。
繼承可以獲得父類的全部功能,包括屬性和方法,同時可以在此基礎上進行添加新的屬性和方法,或者重寫原有的方法。同時,由於繼承存在層層傳遞的關係,子類的實例,也是父類的實例,也是父類的父類的實例,以此類推,直到最上層的那個類,這也是爲什麼子類可以使用父類以及父類的父類中所定義的屬性和方法,這也是由繼承所導致的多態性,一個類可以具有包括其自身及所有父類的。
同時在使用子類,一定是先使用子類所定義的方法和屬性,如果沒有,纔會去依次向上追溯尋找可用的屬性或方法,如果直到最終的根類object
都沒有,纔會爆錯。
#!/usr/bin/python
# -*- encoding:utf-8 -*-
class person(object):
def __init__(self, name, age):
self.__name = name
self.__age = age
def show(self):
print("name: %s, age: %s" % (self.__name, self.__age))
def set_age(self, age):
self.__age = age
class doctor(person):
def set_major(self, major):
self.__major = major
def show_major(self):
print("major : %s" % self.__major)
laowang = doctor("laowang", 20)
laowang.show()
laowang.set_major("Pediatrics")
laowang.show_major()
print(isinstance(laowang, doctor))
print(isinstance(laowang, person))
_________________________
輸出:
name: laowang, age: 20
major : Pediatrics
True
True
多態的好處在於,如果一個函數的輸入可以是多種子類,但如果這些子類具有一個公共的父類,那麼就可以在定義是時將輸入的參數定義爲該父類,在使用時可以輸入其各自子類,而不用關注它具體的類型(注意在函數內不應使用只有某一個子類才具有的屬性或函數),在他具體執行的時候,纔會根據它具體的類型,執行對應的函數。
#!/usr/bin/python
# -*- encoding:utf-8 -*-
class person(object):
def show(self):
print("this is person speaking")
class doctor(person):
def show(self):
print("this is doctor speaking")
def show(person):
person.show()
show(person())
show(doctor())
——————————————————————————
輸出:
this is person speaking
this is doctor speaking
動態語言特性
對於上述情況,甚至我們不需要嚴格要求輸入的類是定義時要求的類的子類,只需要傳入的類具有show
方法即可,這和JAVA
這種靜態語言有很大不同,JAVA
要求定義時如果是要求傳入一個類,那麼在使用時比如傳入該種類或其子類,而python作爲一門動態語言可以使用上述這種被稱爲鴨子模型。比如,很多對象只要有read
方法,都可以被視爲file-like object
,而很多函數的傳入參數就是file-like object
類型,此時你可以不必傳入文件對象,而是直接傳入實現了read
方法的對象即可。
作爲動態語言,python具有另一個特性,就是可以直接給實例綁定新特性,新特性如果和類已有的特性重名,由於實例特性的優先級較高,在直接訪問時會覆蓋掉類的特性,但是類的特性依然存在。所以,在給實例添加新特性時,最好不要使用和已有屬性相同的名字,不然會導致調用的混亂。
>>> class Chinese(object):
... country = "China"
...
>>> laowang = Chinese()
>>> print(laowang.country)
China
>>> laowang.country = 'PRC' #修改實例的country特性
>>> print(laowang.country) #實例的country被修改
PRC
>>> print(Chinese.country) #但是類的country值沒有被修改
China
>>> del laowang.country #刪除實例的country特性
>>> print(laowang.country) #類的country特性仍保留
China
>>> laowang.hometown = "taiwan" #添加新的實例特性
>>> print(laowang.hometown) #新的實例特性可以正常使用
taiwan
同樣的,我們可以給一個實例綁定一個新的方法,這裏需要使用types
模塊中的MethodType
方法,
>>> def set_sex(self, sex):
... self.sex = sex
...
>>> from types import MethodType
>>> laowang.set_sex = MethodType(set_sex, laowang)
>>> laowang.set_sex("male")
>>> laowang.sex
'male'
如果想給所有的實例都綁定新方法,則需要直接給類綁定新方法
>>> Chinese.set_sex = set_sex
但是如果這樣隨意給實例或類添加新的特性,代碼的安全性可能得不到保證,所以python提供了__slots__
變量,在定義函數的時候通過它來限定實例能添加的屬性。
>>> class Chinese(object):
... __slots__ = ('country', 'hometown')
...
>>> laowang = Chinese()
>>> laowang.country = 'China'
>>> laowang.hometown = 'taiwan'
>>> laowang.sex = 'male'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Chinese' object has no attribute 'sex'
Error in sys.excepthook:
..........
Original exception was:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Chinese' object has no attribute 'sex'
>>>
同時,該限制只在該類中起作用,在其子類中並不起作用,子類但如果子類也使用了__slots__
變量,那麼就將接收父類__slots__
的限制,此時子類實例可以添加的屬性就是子類和父類__slots__
指定內容的並集。
>>> class taiwanren(Chinese):
... pass
...
>>> jojo = taiwanren()
>>> jojo.sex = 'female'
>>> class taiwanren(Chinese):
... __slots__ = ()
...
>>> jj = taiwanren()
>>> jj.sex = 'male'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'taiwanren' object has no attribute 'sex'
Error in sys.excepthook:
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/apport_python_hook.py", line 53, in apport_excepthook
if not enabled():
File "/usr/lib/python3/dist-packages/apport_python_hook.py", line 28, in enabled
return re.search('^\s*enabled\s*=\s*0\s*$', conf, re.M) is None
AttributeError: module 're' has no attribute 'search'
Original exception was:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'taiwanren' object has no attribute 'sex'
@property
在自定義類時,如果我們想給一個屬性賦值,可以直接調用class.age = 20
,但這樣會直接把age
屬性暴露給外部,而且如果想要在賦值的同時對傳入的參數進行檢查,就必須使用set_age
這樣的方法,但是這樣的方法使用起來比較麻煩,python提供了一種在直接給屬性賦值的同時進行參數檢查的方法,即@property
,如下代碼所示的age
屬性。
#!/usr/bin/python
# -*- encoding:utf-8 -*-
class person(object):
@property
def age(self):
return self.__age
@age.setter
def age(self, age):
if not isinstance(age, int):
raise ValueError('age must be an integer')
if age < 0 or age > 120:
raise ValueError('invalid age, beyond [0, 120]')
self.__age = age
pp = person()
pp.age = 30
print(pp.age)
pp.age = 190
——————————
輸出:
30
Traceback (most recent call last):
File "proper.py", line 21, in <module>
pp.age = 190
File "proper.py", line 14, in age
raise ValueError('invalid age, beyond [0, 120]')
ValueError: invalid age, beyond [0, 120]
同時,如果想要給一個屬性設置爲只讀的,那麼只要在類中只給它創建getter
的方法,即直接的@property
方法,而不創建setter
的方法,如下所示的sex
屬性。
#!/usr/bin/python
# -*- encoding:utf-8 -*-
class person(object):
def __init__(self, sex):
self.__sex = sex
@property
def age(self):
return self.__age
@age.setter
def age(self, age):
if not isinstance(age, int):
raise ValueError('age must be an integer')
if age < 0 or age > 120:
raise ValueError('invalid age, beyond [0, 120]')
self.__age = age
@property
def sex(self):
return self.__sex
pp = person('female')
pp.age = 30
print('person age:%d, sex:%s' % (pp.age, pp.sex))
pp.sex = 'male'
——————————————
輸出:
person age:30, sex:female
Traceback (most recent call last):
File "proper.py", line 28, in <module>
pp.sex = 'male'
AttributeError: can't set attribute
多重繼承
繼承可以使子類直接獲得父類的所有功能,並且在此基礎上擴展父類的功能。但是正如類的概念來自真實世界中的對象,每個對象都可以有不同的分類,比如cat
,dog
、bat
和parrot
,既可以按照能飛不能飛來分類,也可以按照哺乳動物和非哺乳動物來分類,如果按照簡單的單一繼承關係,我們需要很複雜的層次關係來實現這種邏輯,所以可以使用多重繼承,來讓一個子類獲得其所有相關父類的繼承關係,這樣在我們設計類時,只需要按照大類設計所有的基本的類,隨後讓子類直接多重繼承其所有所歸屬的大類。爲了
#!/usr/bin/python
# -*- coding: utf-8 -*-
class animal(object):
pass
class mammal(animal):
pass
class unmammal(animal):
pass
class runnable(animal):
pass
class flyable(animal):
pass
class dog(mammal, runnable):
print("I am a dog")
定製類
在上文中我們看到,使用__slots__
可以限定類可以添加的屬性額範圍,這一形如__xxxx__
的變量或函數名在定義類時一般也是有特殊的,比如__len()__
方法使得類可以作用於len()
函數,__str()__
函數讓類在被print
直接打印時,輸出函數制定的字符串,__repr()__
函數的作用和__str__
類似,但是他會打印一些用於調試服務的信息。
枚舉類
在python中,每一組數據結構都被抽象成了一個類,因此自定義的數據可以在這些基本數據結構的基礎上繼承並使用其提供的豐富的接口,一個典型的應用就是在創建枚舉類時。
使用python自帶的枚舉類型,首先要引入枚舉模塊,然後就可以直接創建自己的枚舉類型,隨後可以直接通過week.Mon
直接引用一個常量,或者使用for
循環枚舉它所有成員。
>>> from enum import Enum
>>> week = Enum('week', ('Mon', 'Tues', 'Wed', 'Thur', 'Fri', 'Sat', 'Sun'))
>>> week.Tues
<week.Tues: 2>
>>> week['Mon']
<week.Mon: 1>
>>> for name,member in week.__members__.items():
... print(name, '-->', member, ',', member.value)
...
('Mon', '-->', <week.Mon: 1>, ',', 1)
('Tues', '-->', <week.Tues: 2>, ',', 2)
('Wed', '-->', <week.Wed: 3>, ',', 3)
('Thur', '-->', <week.Thur: 4>, ',', 4)
('Fri', '-->', <week.Fri: 5>, ',', 5)
('Sat', '-->', <week.Sat: 6>, ',', 6)
('Sun', '-->', <week.Sun: 7>, ',', 7)
value是自動分配給成員的int
性常量,默認從1
開始,如果想要自定義更復雜的枚舉類型,可以從Enum
派生自定義的類,在類的前面加上@unique
裝飾器用於檢查成員沒有重複。
元類
在上文中我們說到,python是一種動態語言,而動態語言和靜態語言最大的不同之處在於,函數和類的定義不是在編譯時定義的,而是在運行時動態創建的。type
函數可以查看一個類型或變量的類型,如果輸入一個class
,那麼他的類型就是type
,如果輸入一個類型的實例,即一個變量,那麼他的類型就是該class
。
但是type()
還可以在運行時動態的創建class
,而無需在編譯之前通過class
來定義。
>>> def foo(self, name='laowang'):
... print('hello, i am %s.' % name)
...
>>> person = type('person', (object,), dict(hello=foo))
>>> lw = person()
>>> lw.hello()
hello, i am laowang.
>>> print(type(person))
<type 'type'>
>>> print(type(lw))
<class '__main__.person'>
注意到,使用type()
創建一個新的class,要依次傳入3個參數:
class
的名稱- 繼承的父類集合,注意如果要繼承多個父類,要使用
tuple
的寫法,如果是繼承單個父類,要注意單個tuple
的寫法(object,)
class
的方法名稱和已有的函數的綁定,使用dict
的寫法,左側是要綁定的類的方法名稱,右側是已有的函數名稱