1.引子
面向對象編程有三大特性:封裝、繼承、多態,其中最重要的一個特性就是封裝。封裝指的就是把數據與功能都整合到一起,聽起來是不是很熟悉,沒錯,我們之前所說的”整合“二字其實就是封裝的通俗說法。除此之外,針對封裝到對象或者類中的屬性,我們還可以嚴格控制對它們的訪問,分兩步實現:隱藏與開放接口
2.隱藏屬性
類中存放的有數據屬性和函數屬性 , 所謂的隱藏屬性 , 就是隱藏這兩種,隱藏字面意思 , 就是別人看不到,那么在類中我們如何隱藏呢?
2.1隱藏數據屬性
Python的Class機制采用雙下划線開頭的方式將屬性隱藏起來(設置成私有的),但其實這僅僅只是一種變形操作,類中所有雙下滑線開頭的屬性都會在類定義階段、檢測語法時自動變成“_類名__屬性名”的形式:
class Foo:
__N=0 # 變形為_Foo__N
def __init__(self): # 定義函數時,會檢測函數語法,所以__開頭的屬性也會變形
self.__x=10 # 變形為self._Foo__x
print(Foo.__N) # 報錯AttributeError:類Foo沒有屬性__N
obj = Foo()
print(obbj.__x) # 報錯AttributeError:對象obj沒有屬性__x
當然有人可能疑問 , 我Foo._Foo__N不就可以訪問了嗎?確實可以訪問,注意我們隱藏的目的就是不能讓外面直接訪問,如果你想訪問,那就不要隱藏。隱藏屬性本質上是一種變形方法,目的就是為了不能在方面直接訪問,但是可以在類內直接訪問
class Foo:
__N=0 # 變形為_Foo__N
def __init__(self): # 定義函數時,會檢測函數語法,所以__開頭的屬性也會變形
self.__x=10 # 變形為self._Foo__x
print(Foo.__N)
obj = Foo() # 0
注意: 這種變形操作只在類體代碼定義的時候變形 , 在類定義之后的賦值操作,不會變形。
>>> Foo.__M=100
>>> Foo.__dict__
mappingproxy({..., '__M': 100,...})
>>> Foo.__M
100
>>> obj.__y=20
>>> obj.__dict__
{'__y': 20, '_Foo__x': 10}
>>> obj.__y
20
2.2隱藏函數屬性
目的的是為了隔離復雜度,例如ATM程序的取款功能,該功能有很多其他功能組成,比如插卡、身份認證、輸入金額、打印小票、取錢等,而對使用者來說 , 只需要開發取款這個功能接口即可,其余功能我們都可以隱藏起來
class ATM:
def __card(self): # 插卡
print('插卡')
def __auth(self): # 身份認證
print('用戶認證')
def __input(self): # 輸入金額
print('輸入取款金額')
def __print_bill(self): # 打印小票
print('打印賬單')
def __take_money(self): # 取錢
print('取款')
def withdraw(self): # 取款功能
self.__card()
self.__auth()
self.__input()
self.__print_bill()
self.__take_money()
obj = ATM()
obj.withdraw()
3.開放接口
定義屬性就是為了使用,所以隱藏並不是目的
接口意義:
作為類的設計者可以在接口上附加任意邏輯,來嚴格控制使用者對屬性的操作(刪除,修改,查看)
比如我想修改學生對象的屬性中的學校名字,必須是字符串
class Student:
__school = "北大"
def __init__(self, name, age):
self.name = name
self.age = age
def set_school_name(self, school_name):
if type(school_name) is str:
Student.__school = school_name
else:
print("輸入錯誤: sbsbsbsbsb")
obj = Student('小張',20)
obj.set_school_name(986567)
4.小總結
隱藏屬性並不是不給外部用 , 而是想讓外部間接的使用 , 用接口--->內部功能的高度封裝
總結隱藏屬性與開放接口,本質就是為了明確地區分內外,類內部可以修改封裝內的東西而不影響外部調用者的代碼;而類外部只需拿到一個接口,只要接口名、參數不變,則無論設計者如何改變內部實現代碼,使用者均無需改變代碼。這就提供一個良好的合作基礎,只要接口這個基礎約定不變,則代碼的修改不足為慮。
5.property
property是用類實現的一種裝飾器 , 簡單回顧一下裝飾器
裝飾器是在不修改被裝飾對象源代碼以及調用方式的前提下為被裝飾對象添加新功能的可調用對象
property是一個類實現的裝飾器,是用來干什么???
一般動態生成的數據屬性我們可以用property偽裝其是功能的調用
案例1:
# 案例1
class People:
def __init__(self, height, weight):
self.height = height
self.weight = weight
"""
從bmi的公式上看,bmi應該是觸發功能計算得到的
bmi是隨着身高、體重的變化而動態變化的,不是一個固定的值
說白了,每次都是需要臨時計算得到的
但是bmi聽起來更像是一個數據屬性,而非功能,最后調用的時候是直接調用屬性的那種
property剛好可以偽裝綁定方法為一個屬性
"""
@property
def bmi(self):
return self.weight / (self.height ** 2)
obj = People(1.83, 70)
print(obj.bmi) # 20.1778676
property和隱藏屬性相結合 , 同時使用property有效地保證了屬性訪問的一致性。另外property還提供設置和刪除屬性的功能,如下
"""需求:比如我有一個name屬性想要隱藏但是我還要想訪問他(改 刪 查),調接口但是外部我想更符合邏輯,畢竟是屬性,我想直接點調用"""
class People: def __init__(self): self.__name = "張三" def get_name(self): return self.__name def set_name(self, val): if not type(val) is str: print("輸入字符串") return self.__name = val def del_name(self): print('就是不給刪除') # 這種做法實際上是老版本的操作 name = property(get_name, set_name, del_name)obj = People()# 獲取屬性# print(obj.get_name())print(obj.name)# 修改屬性obj.name = '李四'# 刪除屬性del obj.name
案例2的改良版本
class People: def __init__(self): self.__name = "張三" @property def name(self): # obj.name return self.__name @name.setter def name(self, val): # obj.name="李四" if not type(val) is str: print("輸入字符串") return self.__name = val @name.deleter def name(self): # del obj.name print('就是不給刪除')obj = People()# 獲取屬性# print(obj.get_name())print(obj.name)# 修改屬性obj.name = '李四'print(obj.name) # 李四# 刪除屬性del obj.name# 注意:函數名必須是你要調用的屬性的名字
5.總結
面向對象雖然有三大特征 , 封裝 , 繼承 , 多態 , 但是我覺得封裝才是面向對象的核心 , 白話講就是整合 , 把數據和功能整合到一個小容器里面 , 這個容器就是對象 , 你在一個文件中拿到這個對象 , 就拿到了他所有的數據和功能 , 對於代碼的設計和編寫就比較費頭腦設計了 , 不過好的一點就是 , 代碼的擴展性很強 , 你可以開始想到的只是一個基礎版本的封裝的數據和功能 , 但是你后期想到了再加進來 , 仍然不影響整個代碼的邏輯和調用 , 還有一點就是面向對象只是提高了代碼的擴展性 , 你不用面向對象寫代碼並不代表着你的代碼垃圾 , 你用面向對象寫代碼 , 也不一定非要用到所有的特征 , 至於寫出來的代碼符不符合面向對象的代碼 , 主要還是看你的代碼規范 , 就比如定義私有屬性最好在__init__里面 , 如果初始化沒有值 , 可以先定義為None , 然后常見的搭配使用就ok了 , 這也是他們為什么說你寫多了就會了, 確實是這樣的 , 面向對象只是一個編程范式 , 寫代碼比較頭疼的就是設計類 , 根據需求來設計 , 再加上自己實際生活的一些歸類去處理
