python面向?qū)ο缶幊淘O(shè)計原則之單一職責(zé)原則詳解
一,封裝
封裝是面向?qū)ο缶幊趟枷氲闹匾卣髦弧?/p>
(一)什么是封裝
封裝是一個抽象對象的過程,它容納了對象的屬性和行為實(shí)現(xiàn)細(xì)節(jié),并以此對外提供公共訪問。
這樣做有幾個好處:
- 分離使用與實(shí)現(xiàn)??芍苯邮褂霉步涌?,但不需要考慮它內(nèi)部具體怎么實(shí)現(xiàn)。
- 擁有內(nèi)部狀態(tài)隱藏機(jī)制,可實(shí)現(xiàn)信息/狀態(tài)隱藏。
(二)封裝與訪問
就面向?qū)ο缶幊虂碚f,類就是實(shí)現(xiàn)對象抽象的手段,封裝的實(shí)現(xiàn),就是將對象的屬性與行為抽象為類中屬性與方法。
舉個例子:
對象 AudioFile ,需要有文件名,還需要能播放與停止播放。用類封裝的話,就類似于下面這個實(shí)現(xiàn):
class AudioFil:
def __init__(self, filename):
self.filename = filename
def play(self):
print("playing...")
def stop(self):
print("stop playing...")
self參數(shù)必須是傳入類方法的第一個(最左側(cè))參數(shù);Python 會通過這個參數(shù)自動填入實(shí)例對象(也就是調(diào)用這個方法的主體)。這個參數(shù)不必叫self,其位置才是重點(diǎn)(C++或Java程序員可能更喜歡把它稱作this,因?yàn)樵谶@些語言中,該名稱反應(yīng)的是相同的概念。在Python中,這個參數(shù)總是需要明確的)。
封裝之后,能輕松實(shí)現(xiàn)訪問:
if __name__ == "__main__":
file_name = "金剛葫蘆娃.mp3"
current_file = AudioFil(filename=file_name)
print(current_file.filename)
current_file.play()
current_file.stop()
>>>
金剛葫蘆娃.mp3
playing 金剛葫蘆娃.mp3...
stop playing 金剛葫蘆娃.mp3...
同時能在外部修改內(nèi)部的屬性:
if __name__ == "__main__":
file_name = "金剛葫蘆娃.mp3"
current_file = AudioFil(filename=file_name)
print(current_file.filename)
current_file.play()
current_file.stop()
current_file.filename = "舒克與貝塔.ogg"
print(current_file.filename)
current_file.play()
current_file.stop()
>>>
金剛葫蘆娃.mp3
playing 金剛葫蘆娃.mp3...
stop playing 金剛葫蘆娃.mp3...
舒克與貝塔.ogg
playing 舒克與貝塔.ogg...
stop playing 舒克與貝塔.ogg...
(三)私有化與訪問控制
盡管能通過外部修改內(nèi)部的屬性或狀態(tài),但有時出于安全考慮,需要限制外部對內(nèi)部某些屬性或者方法的訪問。
一些語言能顯式地指定內(nèi)部屬性或方法的有效訪問范圍。比如在 Java 中明確地有 public、private 等關(guān)鍵字提供對內(nèi)部屬性與方法的訪問限制,但 python 并提供另一種方式將它們的訪問范圍控制在類的內(nèi)部:
- 用
_或__來修飾屬性與方法,使之成為內(nèi)部屬性或方法。 - 用
__method-name__來實(shí)現(xiàn)方法重載。
1,屬性與方法的私有化
舉個例子:
class AudioFil:
def __init__(self, filename):
self._filename = filename
def play(self):
print(f"playing {self._filename}...")
def stop(self):
print(f"stop playing {self._filename}...")
if __name__ == "__main__":
file_name = "金剛葫蘆娃.mp3"
current_file = AudioFil(filename=file_name)
print(current_file._filename)
current_file.play()
current_file.stop()

注意 _filename 的格式,單下劃線開頭表明這是一個類的內(nèi)部變量,它提醒程序員不要在外部隨意訪問這個變量,盡管是能夠訪問的。
更加嚴(yán)格的形式是使用雙下劃線:
class AudioFil:
def __init__(self, filename):
self.__filename = filename
def play(self):
print(f"playing {self.__filename}...")
def stop(self):
print(f"stop playing {self.__filename}...")
if __name__ == "__main__":
file_name = "金剛葫蘆娃.mp3"
current_file = AudioFil(filename=file_name)
print(current_file.__filename) #AttributeError: 'AudioFil' object has no attribute '__filename'
current_file.play()
current_file.stop()
注意 __filename 的格式,雙下劃線開頭表明這是一個類的內(nèi)部變量,它會給出更加嚴(yán)格的外部訪問限制,但還是能夠通過特殊手段實(shí)現(xiàn)外部訪問:
# print(current_file.__filename)
print(current_file._AudioFil__filename)
_ClassName__attributename
總之,這種私有化的手段“防君子不防小人”,更何況這并非是真的私有化——偽私有化。有一個更加準(zhǔn)確的概念來描述這種機(jī)制:變量名壓縮。
2,變量名壓縮
Python 支持變量名壓縮(mangling,起到擴(kuò)展作用)的概念——讓類內(nèi)某些變量局部化。
壓縮后的變量名通常會被誤認(rèn)為是私有屬性,但這其實(shí)只是一種把類所創(chuàng)建的變量名局部化的方式而已:名稱壓縮并無法阻止類外代碼對它的讀取。
這種機(jī)制主要是為了避免實(shí)例內(nèi)的命名空間的沖突,而不是限制變量名的訪問。因此,壓縮過的變量名最好稱為“偽私有”,而不是“私有”。
類內(nèi)部以 _ 或 __ 開頭進(jìn)行命名的操作只是一個非正式的慣例,目的是讓程序員知道這是一個不應(yīng)該修改的名字(它對Python自身來說沒有什么意義)。
3,方法重載
python 內(nèi)置的數(shù)據(jù)類型自動地支持有些運(yùn)算操作,比如 + 運(yùn)算、索引、切片等,它們都是通過對應(yīng)對象的類的內(nèi)部的以 __method-name__ 格式命名的方法來實(shí)現(xiàn)的。
方法重載可用于實(shí)現(xiàn)模擬內(nèi)置類型的對象(例如,序列或像矩陣這樣的數(shù)值對象),以及模擬代碼中所預(yù)期的內(nèi)置類型接口。
最常用的重載方法是__init__構(gòu)造方法,幾乎每個類都使用這個方法為實(shí)例屬性進(jìn)行初始化或執(zhí)行其他的啟動任務(wù)。
方法中特殊的self參數(shù)和__init__構(gòu)造方法是 Python OOP的兩個基石。
舉個例子:
class AudioFil:
def __init__(self, filename):
self.__filename = filename
def __str__(self):
return f"我是《{self.__filename}》"
def play(self):
print(f"playing {self.__filename}...")
def stop(self):
print(f"stop playing {self.__filename}...")
if __name__ == "__main__":
file_name = "金剛葫蘆娃.mp3"
current_file = AudioFil(filename=file_name)
print(current_file) #>>> 我是《金剛葫蘆娃.mp3》
(四)屬性引用:getter、setter 與 property
一些語言使用私有屬性的方式是通過 getter 與 setter 來實(shí)現(xiàn)內(nèi)部屬性的獲取與設(shè)置。python 提供 property 類來達(dá)到同樣的目的。舉個例子:
class C:
def __init__(self):
self._x = None
def getx(self) -> str:
return self._x
def setx(self, value):
self._x = value
def delx(self):
del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")
if __name__ == '__main__':
c = C()
c.x = "ccc" # 調(diào)用setx
print(c.x) # 調(diào)用getx
del c.x # 調(diào)用delx
property的存在讓對屬性的獲取、設(shè)置、刪除操作自動內(nèi)置化。
更加優(yōu)雅的方式是使用@property裝飾器。舉個例子:
class C:
def __init__(self):
self._x = None
@property
def x(self):
"""I'm the 'x' property."""
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
if __name__ == '__main__':
c = C()
c.x = "ccc"
print(c.x)
del c.x
二,單一職責(zé)原則
(一)一個不滿足單一職責(zé)原則的例子
現(xiàn)在需要處理一些音頻文件,除了一些描述性的屬性之外,還擁有播放、停止播放和信息存儲這三項(xiàng)行為:
class AudioFile:
def __init__(self, filename, author):
self.__filename = filename
self.__author = author
self.__type = self.__filename.split(".")[-1]
def __str__(self):
return f"我是《{self.__filename}》"
def play(self):
print(f"playing {self.__filename}...")
def stop(self):
print(f"stop playing {self.__filename}...")
def save(self, filename):
content = {}
for item in self.__dict__:
key = item.split("__")[-1]
value = self.__dict__[item]
content[key] = value
with open(filename+".txt", "a") as file:
file.writelines(str(content)+'\n')
if __name__ == '__main__':
file_name = "金剛葫蘆娃.mp3"
author_name = "姚禮忠、吳應(yīng)炬"
current_file = AudioFile(filename=file_name,author=author_name)
current_file.save(filename="info_list")
這個類能夠正常工作。
注意觀察 save 方法,在保存文件信息之前,它做了一些格式化的工作。顯然后面的工作是“臨時添加”的且在別的文件類型中可能也會用到。
隨著項(xiàng)目需求的變更或者其他原因,經(jīng)常會在方法內(nèi)部出現(xiàn)這種處理邏輯的擴(kuò)散現(xiàn)象,即完成一個功能,需要新的功能作為前提保障。
從最簡單的代碼可重用性的角度來說,應(yīng)該將方法內(nèi)可重用的工作單獨(dú)提出來:
至于公共功能放在哪個層次,請具體分析。
def info_format(obj):
content = {}
for item in obj.__dict__:
key = item.split("__")[-1]
value = obj.__dict__[item]
content[key] = value
return content
class AudioFile:
...
def save(self, filename):
content = info_format(self)
with open(filename+".txt", "a") as file:
file.writelines(str(content)+'\n')
但是,給改進(jìn)后的代碼在遇到功能變更時,任然需要花費(fèi)大力氣在原有基礎(chǔ)上進(jìn)行修改。比如需要提供信息搜索功能,就可能出現(xiàn)這種代碼:
class AudioFile:
...
def save(self, filename):
...
def search(self, filename, key=None):
...
如果后期搜索條件發(fā)生變更、或者再新增功能,都會導(dǎo)致類內(nèi)部出現(xiàn)功能擴(kuò)散,將進(jìn)一步增加原有代碼的復(fù)雜性,可讀性逐漸變差,尤其不利于維護(hù)與測試。
(二)單一職責(zé)原則
單一職責(zé)原則(Single-Responsibility Principle,SRP)由羅伯特·C.馬丁于《敏捷軟件開發(fā):原則、模式和實(shí)踐》一書中提出。這里的職責(zé)是指類發(fā)生變化的原因,單一職責(zé)原則規(guī)定一個類應(yīng)該有且僅有一個引起它變化的原因,否則類應(yīng)該被拆分。
該原則提出對象不應(yīng)該承擔(dān)太多職責(zé),如果一個對象承擔(dān)了太多的職責(zé),至少存在以下兩個缺點(diǎn):
- 一個職責(zé)的變化可能會削弱或者抑制這個類實(shí)現(xiàn)其他職責(zé)的能力;
- 當(dāng)客戶端需要該對象的某一個職責(zé)時,不得不將其他不需要的職責(zé)全都包含進(jìn)來,從而造成冗余代碼或代碼的浪費(fèi)。
舉個例子:一個編譯和打印報告的模塊。想象這樣一個模塊可以出于兩個原因進(jìn)行更改。
首先,報告的內(nèi)容可能會發(fā)生變化。其次,報告的格式可能會發(fā)生變化。這兩件事因不同的原因而變化。單一職責(zé)原則說問題的這兩個方面實(shí)際上是兩個獨(dú)立的職責(zé),因此應(yīng)該在不同的類或模塊中。
總之,單一職責(zé)原則認(rèn)為將在不同時間因不同原因而改變的兩件事情結(jié)合起來是一個糟糕的設(shè)計。
看一下修改后的代碼:
class AudioFile:
def __init__(self, filename, author):
self.__filename = filename
self.__author = author
self.__type = self.__filename.split(".")[-1]
def __str__(self):
return f"我是《{self.__filename}》"
def play(self):
print(f"playing {self.__filename}...")
def stop(self):
print(f"stop playing {self.__filename}...")
class AudioFileDataPersistence:
def save(self, obj, filename):
...
class AudioFileDataSearch:
def search(self, key, filename):
...
if __name__ == '__main__':
file_name = "金剛葫蘆娃.mp3"
author_name = "姚禮忠、吳應(yīng)炬"
current_file = AudioFile(filename=file_name, author=author_name)
data_persistence = AudioFileDataPersistence()
data_persistence.save(current_file, filename="info_list")
data_search = AudioFileDataSearch()
data_search.search(file_name, filename="info_list")
但這樣將拆分代碼,是不是合理的選擇?
三,封裝與單一職責(zé)原則
從封裝的角度看來說,它的目的就是在對外提供接口的同時,提高代碼的內(nèi)聚性和可重用性,但功能大而全的封裝更加的不安全。
單一職責(zé)原則通過拆分代碼實(shí)現(xiàn)更低的耦合性和更高的可重用性,但過度拆分會增加對象間交互的復(fù)雜性。
關(guān)于兩這的結(jié)合,有一些問題需要事先注意:
- 需求的粒度是多大?
- 維護(hù)的成本有多高?
作為面向?qū)ο缶幊痰幕A(chǔ)概念與實(shí)踐原則,二者實(shí)際上是因果關(guān)系——如果一個類是有凝聚力的,如果有一個更高層次的目的,如果它的職責(zé)符合它的名字,那么 SRP 就會自然而然地出現(xiàn)。SRP 只是代碼優(yōu)化后的實(shí)際的結(jié)果,它本身并不是一個目標(biāo)。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
Python通過paramiko庫實(shí)現(xiàn)遠(yuǎn)程執(zhí)行l(wèi)inux命令的方法
這篇文章主要介紹了Python通過paramiko庫實(shí)現(xiàn)遠(yuǎn)程執(zhí)行l(wèi)inux命令,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-03-03
pycharm運(yùn)行出現(xiàn)ImportError:No module named的解決方法
今天小編就為大家分享一篇pycharm運(yùn)行出現(xiàn)ImportError:No module named的解決方法。具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-10-10
Python+matplotlib實(shí)現(xiàn)簡單曲線的繪制
Matplotlib是Python的繪圖庫,它能讓使用者很輕松地將數(shù)據(jù)圖形化,并且提供多樣化的輸出格式。本文將利用matplotlib繪制簡單的曲線圖,感興趣的朋友可以學(xué)習(xí)一下2022-04-04
Django中使用haystack+whoosh實(shí)現(xiàn)搜索功能
這篇文章主要介紹了Django之使用haystack+whoosh實(shí)現(xiàn)搜索功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2019-10-10
在Python中操作字符串之startswith()方法的使用
這篇文章主要介紹了在Python中操作字符串之startswith()方法的使用,是Python入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下2015-05-05
Pandas中Concat與Append的實(shí)現(xiàn)與區(qū)別小結(jié)
本文主要介紹了Pandas中Concat與Append的實(shí)現(xiàn)與區(qū)別小結(jié),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-11-11

