面向對象三大特性——封裝(含property)
封裝是面向對象的特征之一,是對象和類概念的主要特性。
封裝概念
封裝就是把客觀事物封裝成抽象的類,並且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏。
隱藏屬性 |
在python中用雙下劃線開頭的方式將屬性隱藏起來(設置成私有的)
其實這僅僅這是一種變形操作,類中所有雙下劃線開頭的名稱如__x都會自動變形成:_類名__x的形式。
class A: __x = 1 # _A__x = 1 def __init__(self, name): self.__name = name # self._A__name=‘egon‘def __foo(self): # _A__foo print(‘%s foo run‘ % self.__name) def bar(self): self.__foo() # self._A__foo() print(‘from bar‘) # 無法找到類的屬性和函數: # print(A.__x) # print(A.__foo) print(A.__dict__) # 可以查看到_A__foo;bar這兩個函數 # 輸出:{‘__module__‘: ‘__main__‘, ‘_A__x‘: 1, ‘__init__‘: <function A.__init__ at 0x101f211e0>, ‘_A__foo‘: <function A.__foo at 0x101f21378>, ‘bar‘: <function A.bar at 0x101f212f0>, ‘__dict__‘: <attribute ‘__dict__‘ of ‘A‘ objects>, ‘__weakref__‘: <attribute ‘__weakref__‘ of ‘A‘ objects>, ‘__doc__‘: None}a = A(‘egon‘) a._A__foo() # 通過這種方式可以訪問類隱藏函數 # 輸出:egon foo run a.bar() """ egon foo run from bar """
可以看到類的屬性和函數在前面加‘__‘,在類定義階段就發生了變形,變形後在外部就無法通過.__x或.__func來調用。
這種自動變形的特點
1、類中定義的__x只能在內部使用,如self.__x,引用的就是變形的結果。
2、這種變形其實正是針對外部的變形,在外部是無法通過__x這個名字訪問到的。
3、在子類定義的__x不會覆蓋在父類定義的__x,因為子類中變形成了:_子類名__x,而父類中變形成了:_父類名__x,即雙下滑線開頭的屬性在繼承給子類時,子類是無法覆蓋的。
class Foo: def __func(self): # _Foo_func print(‘from foo‘) class Bar(Foo): def __func(self): # _Bar__func print(‘from bar‘) b = Bar() # b.func() # AttributeError:沒有這個屬性 b._Bar__func() # 輸出:from bar
變形需要註意的問題
1、知道了類名和屬性名就可以拼出名字:_類名__屬性,然後就可以訪問了
class B: __x = 1 def __init__(self, name): self.__name = name print(B._B__x) """ 1 """
2、變形的過程只在類的定義時發生一次,定義後的賦值操作,不會變形
>>> class A: ... def __init__(self): ... self.__X=10 ... >>> a=A() >>> a.__dict__ {‘_A__X‘: 10} >>> a.__Y=2131 >>> a.__dict__ {‘_A__X‘: 10, ‘__Y‘: 2131} # __Y沒有變形
3、在繼承中,父類如果不想讓子類覆蓋自己的方法,可以將方法定義為私有的
class A: def __foo(self): # _A__foo print(‘A foo‘) def bar(self): print(‘A.bar‘) self.__foo() # self._A__foo() class B(A): def __foo(self): # _B__foo print(‘B.foo‘) b = B() b.bar() """ A.bar A foo # 只在自己類找方法不去其他類查找,子類不覆蓋父類方法 """
#正常情況 >>> class A: ... def fa(self): ... print(‘from A‘) ... def test(self): ... self.fa() ... >>> class B(A): ... def fa(self): ... print(‘from B‘) ... >>> b=B() >>> b.test() from B #把fa定義成私有的,即__fa >>> class A: ... def __fa(self): #在定義時就變形為_A__fa ... print(‘from A‘) ... def test(self): ... self.__fa() #只會與自己所在的類為準,即調用_A__fa ... >>> class B(A): ... def __fa(self): ... print(‘from B‘) ... >>> b=B() >>> b.test() from A方法私有的情況
封裝的意義 |
封裝不是單純意義的隱藏
一、封裝數據屬性
將數據隱藏起來這不是目的。隱藏起來然後對外提供操作該數據的接口,然後我們可以在接口附加上對該數據操作的限制,以此完成對數據屬性操作的嚴格控制。
提示:在編程語言裏,對外提供的接口(接口可理解為了一個入口),可以是函數,稱為接口函數,這與接口的概念還不一樣,接口代表一組接口函數的集合體。
class People: def __init__(self, name, age): self.__name = name self.__age = age def tell_info(self): print(‘Name:<%s> Age:<%s>‘ % (self.__name, self.__age)) def set_info(self, name, age): if not isinstance(name, str): print(‘名字必須是字符串類型‘) return if not isinstance(age, int): print(‘年齡必須是數字類型‘) return self.__name = name self.__age = age p = People(‘egon‘, 18) # p.tell_info() """ Name:<egon> Age:<18> # 封裝數據,開放接口給外部訪問 """ # p.set_info(‘Egon‘, 38) # 修改數據只能通過接口來完成,可以通過接口完成各種限制 # p.tell_info() """ Name:<Egon> Age:<38> """ # p.set_info(123, 38) """ 名字必須是字符串類型 """ p.set_info(‘egon‘, ‘38‘) p.tell_info() """ 年齡必須是數字類型 Name:<egon> Age:<18> """封裝數據屬性
二、封裝方法
隔離復雜度。
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() a = ATM() a.withdraw() """ 插卡 輸入取款金額 輸入取款金額 打印賬單 取款 """封裝方法
由上例可以看到,取款是功能,而這個功能有很多功能組成:插卡、密碼認證、輸入金額、打印賬單、取錢;對使用者來說,只需要知道取款這個功能即可,其余功能我們都可以隱藏起來,很明顯這麽做隔離了復雜度,同時也提升了安全性
封裝和擴展性 |
封裝在於明確區分內外,使得類實現者可以修改封裝內的東西而不影響外部調用者的代碼;而外部使用用者只知道一個接口(函數),只要接口(函數)名、參數不變,使用者的代碼永遠無需改變。
class Room: def __init__(self, name, owner, weight, length, height): self.name = name self.owner = owner self.__weight = weight self.__length = length self.__height = height def tell_area(self): return self.__weight * self.__length r = Room(‘衛生間‘, ‘alex‘, 10, 10, 10) print(r.tell_area()) # 不管是求面積還是體積,用戶調用的方式不變
由上例可以看出,只要接口這個基礎約定不變,就不用擔心代碼的改動。
#類的設計者 class Room: def __init__(self,name,owner,width,length,high): self.name=name self.owner=owner self.__width=width self.__length=length self.__high=high def tell_area(self): #對外提供的接口,隱藏了內部的實現細節,此時我們想求的是面積 return self.__width * self.__length #使用者 >>> r1=Room(‘臥室‘,‘egon‘,20,20,20) >>> r1.tell_area() #使用者調用接口tell_area #類的設計者,輕松的擴展了功能,而類的使用者完全不需要改變自己的代碼 class Room: def __init__(self,name,owner,width,length,high): self.name=name self.owner=owner self.__width=width self.__length=length self.__high=high def tell_area(self): #對外提供的接口,隱藏內部實現,此時我們想求的是體積,內部邏輯變了,只需求修該下列一行就可以很簡答的實現,而且外部調用感知不到,仍然使用該方法,但是功能已經變了 return self.__width * self.__length * self.__high #對於仍然在使用tell_area接口的人來說,根本無需改動自己的代碼,就可以用上新功能 >>> r1.tell_area()封裝與擴展
特性(property) |
property概念
property是一種特殊的屬性,訪問它時會執行一段功能(函數)然後返回值。
下面以計算BMI指數為例:(bmi是計算而來的,但很明顯它聽起來像是一個屬性而非方法,如果我們將其做成一個屬性,更便於理解)
BMI指數:(BMI是計算而來的,很明顯它聽起來像一個屬性而非方法,如果我們將其作為一個屬性,更便於理解)
成人的BMI數值:
過輕:低於18.5
正常:18.5-23.9
過重:24-27
肥胖:28-32
非常肥胖:高於32
體質指數(BMI)= 體重(KG)/ 身高^2(M)
EX:70KG / (1.75*1.75) = 22.86
方法一:普通解決辦法
class People: def __init__(self, name, weight, height): self.name = name self.weight = weight self.height = height p = People(‘jack‘, 48, 1.65) p.bmi = p.weight / (p.height ** 2) print(p.bmi) """ 17.63085399449036 egon dragon 名字必須是字符串類型 dragon 不允許刪除 """
方法二:添加函數改寫
class People: def __init__(self, name, weight, height): self.name = name self.weight = weight self.height = height def bmi(self): print(‘===>‘) return self.weight / (self.height ** 2) p = People(‘SH‘, 53, 1.70) print(p.bmi()) # bmi是一個名詞,卻使用bmi(),容易誤解為一個動作
方法三:添加property裝飾器
class People: def __init__(self, name, weight, height): self.name = name self.weight = weight self.height = height @property # 應用場景:有一個值是通過計算得來的,首選定義方法,運用property讓使用者感知不到 def bmi(self): print(‘===>‘) return self.weight / (self.height ** 2) # 必須有返回值 p = People(‘SH‘, 53, 1.70) print(p.bmi) # 使用者像訪問數據屬性一樣訪問bmi,方法被偽裝 """ ===> 18.339100346020764 """ p.height = 1.82 print(p.bmi) """ ===> 16.000483033450067 """ p.bmi = 333 # 報錯,看起來像數據屬性,其實還是一個方法,不能賦值
property好處
將一個類的函數定義成特性以後,對象再去使用的時候obj.name,根本無法察覺自己的name是執行了一個函數然後計算出來的,這種特性的使用方式遵循了統一訪問的原則。總結來說:把一些計算得到的屬性偽裝得像數據屬性一樣被用戶訪問。
property其他用法(set\get\delete方法) |
在C++裏一般會將所有的所有的數據都設置為私有的,然後提供set和get方法(接口)。Python中通過property來實現這個功能:
class People: def __init__(self, name): self.__name = name def get_name(self): return self.__name p = People(‘egon‘) print(p.get_name()) # 輸出:egon
在上面的代碼中,People的name屬性被封裝,不能直接訪問,因此給類添加了一個get_name()方法來查看內部已經封裝的屬性。
但是這種這種情況下,用戶的屬性調用方式發生了改變。可以通過@property解決該問題。
class People: def __init__(self, name): self.__name = name @property def name(self): return self.__name p = People(‘egon‘) print(p.name) # 輸出:egon
調用方式已經和普通屬性相同,但是在上面bmi的代碼中可以看到,這種情況下是不能對隱藏屬性賦值的。要實現賦值,需要運用@name.setter裝飾器(name被property裝飾後才可用)進行如下修改:
class People: def __init__(self, name): self.__name = name @property def name(self): # print(‘getter‘) return self.__name @name.setter def name(self, val): # print(‘setter‘, val) if not isinstance(val, str): print(‘名字必須是字符串類型‘) return self.__name=val @name.deleter def name(self): # print(‘deleter‘) print(‘不允許刪除‘) p = People(‘egon‘) print(p.name) # 輸出:egon p.name = ‘dragon‘ print(p.name) # 輸出:dragon # name修改成功 p.name = 123 # 輸出:名字必須是字符串類型(報錯) print(p.name) # 輸出:dragon del p.name # 輸出:不允許刪除(報錯)
如代碼所示,實現了name屬性的修改和刪除,@name.deleter和@name.setter都是基於name被@property裝飾才可用的。
面向對象三大特性——封裝(含property)