面向對象基礎
- 面向對象編程:把一組數據結構和處理它們的方法組成對象(object),把相同行為的對象歸納為類(class),通過類的封裝(encapsulation)隱藏內部細節,通過繼承(inheritance)實現類的特化(specialization)和泛化(generalization),通過多態(polymorphism)實現基于對象類型的動態分派。
- 簡單地說,類是對象的藍圖和模板,對象是類的實例。
- python中可以使用class關鍵字定義類,在類中通過函數定義方法,代碼如下。
class Student(object):
# __init__是一個特殊方法用于在創建對象時進行初始化操作
# 通過這個方法我們可以為學生對象綁定name和age兩個屬性
def __init__(self, name, age):
self.name = name
self.age = age
def study(self, course_name):
print('%s正在學習%s.' % (self.name, course_name))
# PEP 8要求標識符的名字用全小寫多個單詞用下劃線連接
# 但是部分程序員和公司更傾向于使用駝峰命名法(駝峰標識)
def watch_movie(self):
if self.age < 18:
print('%s只能觀看《熊出沒》.' % self.name)
else:
print('%s正在觀看島國愛情大電影.' % self.name)
- 當定義好類后,可以創建對象并給對象發消息。
def main():
# 創建學生對象并指定姓名和年齡
stu1 = Student('駱昊', 38)
# 給對象發study消息
stu1.study('Python程序設計')
# 給對象發watch_av消息
stu1.watch_movie()
stu2 = Student('王大錘', 15)
stu2.study('思想品德')
stu2.watch_movie()
if __name__ == '__main__':
main()
- 在python中,屬性和方法的訪問權限只有兩種:公開和私有。如果希望屬性是私有的,在給屬性命名時可以用兩個下劃線作為開頭,代碼如下。
class Test:
def __init__(self, foo):
self.__foo = foo
def __bar(self):
print(self.__foo)
print('__bar')
def main():
test = Test('hello')
# AttributeError: 'Test' object has no attribute '__bar'
test.__bar()
# AttributeError: 'Test' object has no attribute '__foo'
print(test.__foo)
if __name__ == "__main__":
main()
- 但事實上,python并未從語法上嚴格保證其私有性,而只是給私有的屬性和方法換了個名字妨礙訪問,如果了解更換名字的規則就可以訪問到,代碼如下。
class Test:
def __init__(self, foo):
self.__foo = foo
def __bar(self):
print(self.__foo)
print('__bar')
def main():
test = Test('hello')
test._Test__bar()
print(test._Test__foo)
if __name__ == "__main__":
main()
面向對象進階
-
在實際開發中,并不建議將屬性設為私有,這會導致子類無法訪問。但如果直接將屬性暴露給外界也是有問題的,建議是將屬性名以單下劃線開頭,這樣既不會設為私有,又能夠表示其是受保護的,在訪問該屬性時就要保持慎重。
如果想訪問該類屬性,可以通過屬性的getter(訪問器)和setter(修改器)方法進行相應的操作。
可以使用@property包裝器來包裝getter和setter方法,使得對屬性的訪問既安全又方便。
class Person(object):
def __init__(self, name, age):
self._name = name
self._age = age
# 訪問器 - getter方法
@property
def name(self):
return self._name
# 訪問器 - getter方法
@property
def age(self):
return self._age
# 修改器 - setter方法
@age.setter
def age(self, age):
self._age = age
def play(self):
if self._age <= 16:
print('%s正在玩飛行棋.' % self._name)
else:
print('%s正在玩斗地主.' % self._name)
def main():
person = Person('王大錘', 12)
person.play()
person.age = 22
person.play()
# person.name = '白元芳' # AttributeError: can't set attribute
if __name__ == '__main__':
main()
- python是一門動態語言,允許在程序運行時給對象綁定新的屬性或方法,也可對已綁定的進行解綁。但如果需要限定自定義類型的對象只能綁定某些屬性,可以通過在類中定義_slots_變量來完成,_slots_的限定只對當前類的對象生效,對子類不起作用。
class Person(object):
# 限定Person對象只能綁定_name, _age和_gender屬性
__slots__ = ('_name', '_age', '_gender')
def main():
person = Person('王大錘', 22)
person._gender = '男'
# AttributeError: 'Person' object has no attribute '_is_gay'
# person._is_gay = True
- 在類中定義的方法包括對象方法、靜態方法和類方法等。
- 對象方法是發送給對象的消息,而靜態方法只要定義了類,不必建立類的實例就可使用,屬于類本身而非類的某個對象。
from math import sqrt
class Triangle(object):
def __init__(self, a, b, c):
self._a = a
self._b = b
self._c = c
@staticmethod # 靜態方法
def is_valid(a, b, c):
return a + b > c and b + c > a and a + c > b
def perimeter(self):
return self._a + self._b + self._c
def area(self):
half = self.perimeter() / 2
return sqrt(half * (half - self._a) *
(half - self._b) * (half - self._c))
def main():
a, b, c = 3, 4, 5
# 靜態方法和類方法都是通過給類發消息來調用的
if Triangle.is_valid(a, b, c):
t = Triangle(a, b, c)
print(t.perimeter())
# 也可以通過給類發消息來調用對象方法但是要傳入接收消息的對象作為參數
# print(Triangle.perimeter(t))
print(t.area())
# print(Triangle.area(t))
else:
print('無法構成三角形.')
if __name__ == '__main__':
main()
- 類方法第一個參數約定名為cls,它代表當前類相關信息的對象(類本身也是一個對象,也稱為類的元數據對象),通過該參數可以獲取和類相關的信息且創建類的對象。
from time import time, localtime, sleep
class Clock(object):
"""數字時鐘"""
def __init__(self, hour=0, minute=0, second=0):
self._hour = hour
self._minute = minute
self._second = second
@classmethod # 類方法
def now(cls):
ctime = localtime(time())
return cls(ctime.tm_hour, ctime.tm_min, ctime.tm_sec)
def run(self):
"""走字"""
self._second += 1
if self._second == 60:
self._second = 0
self._minute += 1
if self._minute == 60:
self._minute = 0
self._hour += 1
if self._hour == 24:
self._hour = 0
def show(self):
"""顯示時間"""
return '%02d:%02d:%02d' % \
(self._hour, self._minute, self._second)
def main():
# 通過類方法創建對象并獲取系統時間
clock = Clock.now()
while True:
print(clock.show())
sleep(1)
clock.run()
if __name__ == '__main__':
main()
-
類與類之間有三種關系:is-a、has-a和use-a。
is-a關系即繼承或泛化,如學生-人。
has-a關系即關聯,如部門-員工。如果是整體和部分的關聯,即聚合關系;如果整體進一步負責了部分的生命周期(整體和部分是不可分割的,同時同在也同時消亡),即為合成關系,屬于最強的關聯關系。
use-a關系即依賴,如司機的駕駛方法,其中參數用到了汽車。 -
面向對象三大特性:封裝、繼承和多態。
封裝:隱藏一切可以隱藏的實現細節,只向外界提供簡單接口。在創建對象后,只需要調用方法就可以執行,只需要知道方法的名字和傳入的參數,而不需要知道方法內部的實現細節。
繼承:讓一個類從另一個類那里將屬性和方法直接繼承下來,減少重復代碼的編寫。提供信息的為父類,又稱超類或基類;繼承信息的為子類,又稱派生或衍生類。子類不僅可以繼承父類的屬性和方法,還可以定義自己的,在實際開發中,通常會用子類對象替換父類對象,即里氏替換原則。
多態:子類對父類的方法給出新的實現版本,稱為方法重寫(override),從而讓父類的同一行為在子類中擁有不同版本。當調用這個經過子類重寫的方法時,不同子類對象會表現出不同的行為,即多態(poly-morphism)。
from abc import ABCMeta, abstractmethod
class Pet(object, metaclass=ABCMeta):
"""寵物"""
def __init__(self, nickname):
self._nickname = nickname
@abstractmethod
def make_voice(self):
"""發出聲音"""
pass
class Dog(Pet):
"""狗"""
def make_voice(self):
print('%s: 汪汪汪...' % self._nickname)
class Cat(Pet):
"""貓"""
def make_voice(self):
print('%s: 喵...喵...' % self._nickname)
def main():
pets = [Dog('旺財'), Cat('凱蒂'), Dog('大黃')]
for pet in pets:
pet.make_voice()
if __name__ == '__main__':
main()
- 上面代碼里,將Pet類處理為抽象類,即不能夠創建對象,只為讓其他類來繼承。python從語法層面沒有提供對抽象類的支持,但可以通過abc模塊的ABCMeta元類和abstractmethod包裝器來達到抽象類的效果,如果一個類中存在抽象方法它就不能實例化。Dog和Cat子類分別對Pet類中的make_voice抽象方法進行了不同地重寫,在main函數中調用該方法時,就表現出了多態行為。
整數比較
- 在python中比較兩個整數時有兩種運算符:==和is:is比較的是兩者id值是否相等,即是否指向同一個地址;==比較兩者內容是否相等,實際上調用了_eq_()方法。
- 矛盾1 代碼如下。
def main():
x = y = -1
while True:
x += 1
y += 1
if x is y:
print('%d is %d' % (x, y))
else:
print('Attention! %d is not %d' % (x, y))
break
x = y = 0
while True:
x -= 1
y -= 1
if x is y:
print('%d is %d' % (x, y))
else:
print('Attention! %d is not %d' % (x, y))
break
if __name__ == '__main__':
main()
- 矛盾1 解釋:python出于對性能的考慮,會將一些頻繁使用的整數對象緩存到一個叫small_ints的鏈表中,任何需要使用這些整數對象的時候,都不需要再重新創建,而是直接引用緩存。緩存的區間為[-5,256],當使用is進行比較時,在該范圍內的指向同一地址,而超出該范圍則為重新創建的,就會得到257 is 257結果為false的情況。
- 矛盾2 代碼如下:
import dis
a = 257
def main():
b = 257 # 第6行
c = 257 # 第7行
print(b is c) # True
print(a is b) # False
print(a is c) # False
if __name__ == "__main__":
main()
- 矛盾2 解釋:代碼塊是程序的最小執行單位,在上述代碼中,a=257和main屬于兩個代碼塊。python為進一步提高性能,規定在同一個代碼塊中創建的整數對象,即便超出[-5,256]的范圍,只要存在有值相同的整數對象,后續創建就直接引用。該規則對不在small_ints范圍內的負數和負數值浮點數并不適用,但對非負浮點數和字符串都適用。因而c引用了b的257,而a與b不在同一個代碼塊內,才會得出注釋中的執行結果。
- 導入dis模塊并在main()方法下添加“dis.dis(main)”一句,可進行反匯編,從字節碼的角度來看該代碼。運行結果中,代碼第6、7行的257,是從同一位置加載的,而第9行的a則是從不同地方加載的,因此引用的是不同的對象。
嵌套列表
- 把列表作為列表中的元素,即為嵌套列表,可以模擬現實中的表格、矩陣和棋盤等,但需謹慎使用,否則會出現問題,代碼如下。
def main():
names = ['關羽', '張飛', '趙云', '馬超', '黃忠']
subjs = ['語文', '數學', '英語']
scores = [[0] * 3] * 5
for row, name in enumerate(names):
print('請輸入%s的成績' % name)
for col, subj in enumerate(subjs):
scores[row][col] = float(input(subj + ': '))
print(scores)
if __name__ == '__main__':
main()
- 上述代碼原本打算錄入五位同學的三門成績,但最終輸出結果卻發現,每個學生三門課程的成績是一樣的,而且就是最后錄入的那個學生的成績。解決此問題,需要區分開對象和對象的引用兩個概念,這就涉及到內存中的棧和堆。
-
程序中可以使用的內存從邏輯上可以分為五個部分,按照地址從高到低依次是:棧、堆、數據段、只讀數據段和代碼段。
其中,棧用來存儲局部、臨時變量,以及函數調用時保存和恢復現場需要用到的數據,這部分內存在代碼塊執行開始時自動分配,結束時自動釋放,通常由編譯器自動管理。
堆的大小不固定,可以動態地分配和回收,如果程序中有大量數據需要處理,通常都放在堆上。如果堆空間沒有被正確釋放,會引發內存泄露的問題,像Python、Java等都使用了垃圾回收機制(自動回收不再使用的堆空間),來實現自動化的內存管理。 - 因此以如下代碼為例,變量a并不是真正的對象,而是對象的引用,相當于記錄了對象在堆空間中的地址;同理,變量b是列表容器的引用,它引用了堆空間上的列表容器,而列表容器中并沒有保存真正的對象。
a = object()
b = ['apple', 'pitaya', 'grape']
- 再看最初的程序,對列表進行[[0]*3]*5操作時,僅僅是將[0,0,0]這個列表的地址進行了復制,并沒有創建新的列表對象。所以容器中雖然有五個元素,卻都引用的是同一個列表對象,每次輸入都相當于對該列表對象進行了修改,因此最終顯示的即為最后一個學生的成績,正確的代碼如下。
def main():
names = ['關羽', '張飛', '趙云', '馬超', '黃忠']
subjs = ['語文', '數學', '英語']
scores = [[]] * 5 # 或scores = [[0] * 3 for _ in range(5)]
for row, name in enumerate(names):
print('請輸入%s的成績' % name)
scores[row] = [0] * 3
for col, subj in enumerate(subjs):
scores[row][col] = float(input(subj + ': '))
print(scores)
if __name__ == '__main__':
main()
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
