Python學習之旅—面向對象進階知識:類的命名空間,類的組合與繼承
前言
上篇博客筆者帶領大家初步梳理了Python面向對象的基礎知識,本篇博客將專註於解決三個知識點:類的命名空間,類的組合以及面向對象的三大特性之一繼承,一起跟隨筆者老看看今天的內容吧。
1.類的命名空間
在上一篇博客中,我們提到過對象可以動態添加屬性,一起來回憶下昨天的知識點,看如下的代碼:
class A:
pass
a = A()
a.name = ‘alex‘
print(a.name)
這裏我們手動為a對象添加了一個屬性name,然後直接打印可以得到a對象的名稱。通過這個例子,我們可以引出類的命名空間。在Python中,創建一個類就會創建一個類的名稱空間,用來存儲類中定義的所有名字,這些名字稱為類的屬性。通常類中的屬性有兩種:靜態屬性和動態屬性。如下:
靜態屬性就是直接在類中定義的變量,例如我們上一篇博客中的role = ‘person‘,這裏role就是一個靜態屬性,我們也稱為類變量,該變量可直接通過類名調用。
動態屬性就是定義在類中的方法,例如我們前面定義的attack方法,該方法表示所有的Person類都具有的動作。
其中類的靜態屬性是共享給所有的對象的,而類的動態屬性是綁定給所有對象的。我們還是來看看下面的例子,然後再來理解這句話:
class Person:
role = ‘person‘
def __init__(self, name, sex, aggressive=200):
self.name = name
self.sex = sex
self.aggr = aggressive
self.blood = 2000
def attack(self, dog):
print(‘%s attack %s‘ % (self.name, dog.name))
dog.blood -= self.aggr
alex = Person(‘alex‘, ‘male‘, 250)
print(alex.role) # 打印 person
print(id(alex.role)) # 打印18076480
carson = Person(‘carson‘, ‘male‘, 800)
print(carson.role) # 打印 person
print(id(alex.role)) # 打印 18076480
通過上面的例子我們可知,靜態屬性可以被alex和carson兩個對象共同使用,因為它們的內存地址值是一樣的。但同時這又有一個問題,如果這個靜態屬性是一個計算器或者其他類型的共享變量,那麽很有可能會導致線程安全問題,關於這點我們後面會討論。在這裏我們必須要明白所有對象共享類的靜態屬性。
而類的動態屬性是綁定到所有對象的,即在每個對象的內存空間中都會存在一份類的動態屬性的地址。其實也很好理解,因為不同對象調用同一個方法傳入的參數不同,肯定會執行不同的結果,因此類的動態屬性,即類裏面定義的方法肯定不會是共享的,而是綁定到不同的對象上。
因此我們可以作如下的總結:
對於類的靜態屬性,如果使用類名.屬性的方式調用,那麽調用的就是類中的屬性;如果使用對象.屬性的方式調用,Python會先從對象自己的內存空間中尋找是否存在該變量,如果存在,則使用自己的,如果沒有,就是用類中定義的靜態變量。
而對於類的動態屬性(也即類中定義的方法),如果這個方法本身就存在於類中,那麽在對象的內存空間中是不會再存儲一份的;但是該方法在類中的地址是會存儲一份到對象的內存空間中,以方便對象通過該地址去類中尋找需要調用的方法。
我們再來看如下的代碼:
class A:
country = "中國"
def show_name(self):
print(self.name)
a = A()
a.name = ‘alex‘
a.show_name() # 打印alex
a.show_name = ‘egon‘
print(a.show_name) # 打印egon
同樣,按照我們之前的分析,a.name=‘alex‘表示我們為a對象動態添加了一個屬性name,並賦予了值alex。調用a.show_name()會打印出alex,因此此時對象a已經擁有一個name屬性。緊接著,我們定義了一個a.show_name = ‘egon‘,註意這裏依然表示為a對象動態添加一個屬性,只不過這個屬性名稱和類中的方法名稱show_name一樣,因此打印a.show_name,會直接打印出egon,原因是show_name是對象a的一個屬性,這裏大家千萬不要搞混了。接下來,我們來看看如果在上述代碼的最後再調用print(a.show_name())會發生什麽?
class A:
country = "中國"
def show_name(self):
print(self.name)
a = A()
a.name = ‘alex‘
a.show_name()
a.show_name = ‘egon‘
print(a.show_name)
print(a.show_name()) # 會報錯:TypeError: ‘str‘ object is not callable
我們可以看到當我們調用和對象屬性同名的方法時報錯。這是因為找名字和方法都會先從自己的內存空間中找,而名字在面向對象中只能代表一個東西,要麽是方法名,要麽是屬性名,當對象找到了屬性名,即使再調用方法,也會報錯,因為此時我們已經將該名稱看作是一個字符串,因此會報如上的錯誤。如果還不明白,我們再來看下面的一個小例子:
def a():
print(‘aaa‘)
a = 20
a() # 報錯:TypeError: ‘int‘ object is not callable
這裏的報錯其實和上面是同樣的道理,a = 20,此時a已經是一個變量,即使我們在下面繼續調用上面定義好的a函數,python依然會認為a是一個變量,而一個整型變量是不能被調用的,所以會報錯:整型對象不能被調用。
最後我們來通過一個簡單的例子來熟悉下類的命名空間,一起看看如下的代碼:
class A:
country = "中國"
def show_name(self):
print(self.name)
a = A()
b = A()
print(A.country)
print(a.country)
print(b.country)
a.country = ‘英國‘
print(A.country)
print(a.country) # 打印英國
print(b.country)
除了倒數第二個print語句打印的是英國外,其余打印的都是中國。在上面的程序中,我們發現使用對象a調用了靜態屬性country,並賦值為英國,但是這並沒有改變靜態變量a的值,因為我們打印b.country看到的依然是中國。
2.組合
再梳理完類的命名空間後,我們再來看下一個知識點:類的組合。類的組合主要用來解決代碼的重復問題,從而降低代碼的冗余,組合和繼承的概念比較類似,關於組合我們會在下一個知識點討論。組合表示的是包含的意思,是一種什麽有什麽的關系。我們一起來看看下面的2個例子。
現在我們有這樣一個需求:我們想計算一個圓環的面積和周長。怎麽做呢?圓環是由兩個圓組成的,圓環的面積是外面圓的面積減去內部圓的面積。圓環的周長是內部圓的周長加上外部圓的周長。
此時,我們首先實現一個圓形類,計算一個圓的周長和面積。按照上面圓環面積和周長的計算方法,我們只需要在"環形類"中組合圓形的實例作為環形類的屬性即可,說明白點,就是我們讓圓形對象作為環形類的一個屬性即可。一起來看看下面的例子就明白了:
from math import pi
class Circle:
‘‘‘
定義了一個圓形類;
提供計算面積(area)和周長(perimeter)的方法
‘‘‘
def __init__(self,radius):
self.radius = radius
def area(self):
return pi * self.radius * self.radius
def perimeter(self):
return 2 * pi *self.radius
circle = Circle(10) #實例化一個圓
area1 = circle.area() #計算圓面積
per1 = circle.perimeter() #計算圓周長
print(area1,per1) #打印圓面積和周長
class Ring:
‘‘‘
定義了一個圓環類
提供圓環的面積和周長的方法
‘‘‘
def __init__(self,radius_outside,radius_inside):
self.outsid_circle = Circle(radius_outside) # 大圓對象Circle(radius_outside)為圓環類的一個屬性
self.inside_circle = Circle(radius_inside) # 小圓對象Circle(radius_inside)為圓環類的一個屬性
def area(self): # 直接調用大圓對象的面積-小圓的面積即可得到圓環的面積
return self.outsid_circle.area() - self.inside_circle.area()
def perimeter(self): # 直接調用大圓的周長+小圓的周長即可得到圓環的周長
return self.outsid_circle.perimeter() + self.inside_circle.perimeter()
ring = Ring(10,5) #實例化一個環形
print(ring.perimeter()) #計算環形的周長
print(ring.area()) #計算環形的面積
通過上面的例子可知,我們使用類的組合概念計算出了圓環的面積和周長,這裏包含的的關系是圓環中有大圓和小圓,因此我們考慮使用圓的周長和面積來計算圓環的面積和周長。一句話總結:一個類的對象作為另一個類的屬性,這就是組合。為了加深對類的組合關系的理解,我們再來看下面的例子。
現在有這樣的需求,我想實現一個選課系統,具體需求筆者會單獨開通一篇博客進行說明。通過分析,我們知道,選課系統涉及到4個角色,分別為學生,講師,班級和管理員。在做關聯時,我們需要為學生關聯課程。對於學生類而言,課程僅僅是它的一個屬性;但是對於課程而言,它又是一個單獨存在的類。既然課程是學生類的一個屬性,換句話說即學生類裏面有課程,因此這裏我們就用到了組合的概念。一起來看看下面的代碼:
class BirthDate:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
class Couse:
def __init__(self, name, price, period):
self.name = name
self.price = price
self.period = period
class Student:
def __init__(self, name, gender, birth, course):
self.name = name
self.gender = gender
self.birth = birth # birth對象作為學生類的一個屬性存在,表示為學生的生日
self.course = course # 課程對象作為學生類的一個屬性存在,表示學生上的什麽課
def teach(self):
print(‘teaching‘)
stu = Student(‘carson‘, ‘male‘,
BirthDate(‘1992‘, ‘11‘, ‘13‘),
Couse(‘python‘, ‘19800‘, ‘4 months‘)
) # 從這裏可以清楚地看到BirthDate(‘1992‘, ‘11‘, ‘13‘)和Couse(‘python‘, ‘19800‘, ‘4 months‘)
兩個對象作為類Student的屬性來初始化一個實例對象stu.
print(stu.birth.year, stu.birth.month, stu.birth.day) # stu.birth相當於上面的生日對象BirthDate
print(stu.course.name, stu.course.price, stu.course.period) # stu.course相當於上面的課程對象course
通過上面的例子可知,類的組合關系表示的是什麽有什麽的關系,即當類之間有顯著不同,並且較小的類是較大的類所需要的組件時,用組合比較好。我們可以用一句話來總結下類的組合關系:只要涉及到誰裏面有誰,而且可以抽象成類,那麽就考慮使用組合。我們最後來看一個組合的例子,這裏要舉的一個例子是人狗大戰的遊戲,即人可以攻擊狗,狗可以咬人,而且人可以有武器,武器可以抽象為一個類,因此這裏可以使用組合的概念。來看如下的代碼:
3.類的繼承
3.1 繼承知識點入門
繼承也是為了解決代碼的重用問題,從而達到減少代碼冗余的目的,它和類的組合類似,但是不同點在於,繼承是一種什麽是什麽的關系,例如老師類是人類,香蕉類是水果類。在Python3中,表示兩個類之間的繼承關系很簡單,例如要表示B類要繼承A類,我們直接使用如下的表達式即可:class B(A)即可。這是在Python3中的新式類寫法,關於新式類與經典類筆者將在下一篇博客中做一個系統的梳理和說明,本次將專註於類的繼承初級知識。一起來看看如下的一個簡單的例子:
class People:
pass
class Animal:
pass
class Student(People, Animal): # People、Animal稱為基類或父類,Student繼承了People和Animal的所有屬性
pass
print(Student.__bases__) # __bases__方法用來查看子類繼承的所有父類,從做到右分別打印繼承的父類
print(People.__bases__)
print(Animal.__bases__)
上面三個print語句的打印結果如下:
(<class ‘__main__.People‘>, <class ‘__main__.Animal‘>)
(<class ‘object‘>,)
(<class ‘object‘
上面的例子是一個多繼承例子,Python中也有一個單繼承的問題;來看如下的例子:
class Animal: pass class Dog(Animal): pass print(Dog.__bases__) # 打印:(<class ‘__main__.Animal‘>,) 結果是一個元組
由此可知,Python支持多繼承和單繼承,但對於多繼承而言,並沒有太大的意義,所以在實際開發中,我們推薦使用單繼承。
3.2 繼承的重用性
我們來看看繼承是如何解決代碼的冗余的。試想這樣一個生活場景:貓和狗都具有吃飯,睡覺,喝水的功能,按照普通的類的定義方式,我們可以寫出如下的代碼:
class Dog:
def __init__(self, name, food):
self.name = name
self.food = food
def eat(self):
print(‘%s eating %s‘ % (self.name, self.food))
def drink(self):
print(‘drinking‘)
def sleep(self):
print(‘sleeping‘)
class Cat:
def __init__(self, name, food):
self.name = name
self.food = food
def eat(self):
print(‘%s eating %s‘ % (self.name, self.food))
def drink(self):
print(‘drinking‘)
def sleep(self):
print(‘sleeping‘)
dog = Dog(‘泰迪‘, ‘肉包子‘)
dog.eat()
cat = Cat(‘加菲貓‘, ‘鯽魚‘)
cat.eat()
從上面代碼不難看出,狗和貓都具有相同的功能,但是我們分別定義了兩個狗和貓兩個類,並實現了兩遍相同的方法。這無疑增加了代碼的冗余度,事實上按照繼承的思想,我們可以將這些相同的功能抽象出來,並且抽象出一個共同類:動物類。代碼如下:
class Animal:
def __init__(self, name, food):
self.name = name
self.food = food
def eat(self):
print(‘%s eating %s‘ % (self.name, self.food))
def drink(self):
print(‘drinking‘)
def sleep(self):
print(‘sleeping‘)
class Dog(Animal):
def __init__(self, name, food):
super(Dog, self).__init__(name, food)
def say(self):
print("汪汪汪")
class Cat(Animal):
def __init__(self, name, food):
super(Cat, self).__init__(name, food)
def say(self):
print(‘喵喵喵‘)
dog = Dog(‘泰迪‘, ‘肉包子‘)
dog.eat()
cat = Cat(‘加菲貓‘, ‘鯽魚‘)
cat.eat()
可以看到再使用完繼承的概念後,代碼的冗余度和可讀性都增強了。由上面的代碼可知,子類會繼承父類所有的方法和屬性。同時我們發現在子類中,我們使用了super關鍵字來初始化對象:super(Cat, self).__init__(name, food)。這裏我們手動地調用了父類中的init方法。
很多同學對super關鍵字不熟悉,這裏筆者來為大家做個小總結:
1. super裏面必須傳入兩個參數,第一個參數代表本類,第二個參數代表本類的對象。如果直接在子類裏面使用super關鍵字調用父類方法,super關鍵字可以不用傳遞參數,即我想調用父類的init方法來進行初始化,可以寫成這樣:super(Cat, self).__init__(name, food)。
2.什麽時候使用super?如果子類和父類有同名的方法,此時還需要調用父類的方法,那麽就應該使用super關鍵字。例如在上面的代碼中,子類和父類都有init方法,此時我還想調用父類的init方法來初始化一個子類對象,所以我們用到了super關鍵字。
3.在使用super關鍵字時,如果是在類外面想調用父類的方法,那我們必須要傳遞兩個參數,第一個參數是子類名,第二個參數是子類對象;如果在類裏面調用父類的方法,傳入參數時,第一個參數必須是子類名,第二個參數為self,代表的是子類的對象。
4.在實際開發中,我們使用的是在類裏面使用super關鍵字來調用父類的方法,這樣就做到了我調用子類的一個方法,又做到調用了父類的方法,一舉兩得。
3.2 派生屬性和派生方法
在前面我們說過,子類繼承父類時,會繼承父類所有的屬性和方法。但是子類有時會有一些獨有的屬性和方法是父類所不具備的,我們還是通過實際的案例來說明該知識點,代碼如下:
class Animal:
def __init__(self, name, blood, aggr):
self.name = name
self.blood = blood
self.aggr = aggr
class Person(Animal):
def __init__(self, name, blood, aggr, money):
super(Person, self).__init__(name, blood, aggr)
self.money = money
def attack(self, dog):
dog.blood -= self.aggr
class Dog(Animal):
def __init__(self, name, blood, aggr, breed):
super(Dog, self).__init__(name, blood, aggr)
self.breed = breed # 派生屬性 :在父類屬性的基礎上,之類特有的屬性
def bite(self, person): # 派生方法:子類獨有的方法
person.blood -= self.aggr
dog = Dog("泰迪", 1000, 500, 1000000)
alex = Person("Alex", 2000, 50, "金毛")
dog.bite(alex)
print(dog.blood) # 1000
print(dog.breed) # 1000000
alex.attack(dog)
print(alex.money) # 金毛
還是人狗大戰的例子,人和狗都是動物類,很自然,兩者都繼承於動物類,但是兩者都有一些獨有的屬性和方法。例如對於人來說,人具有錢money這個屬性,人的獨有方法是攻擊方法,它可以攻擊任何對象;對於狗來說,它的獨有屬性是品種breed,它的獨有方法是bite方法—咬人。像money,breed這些是子類獨有的屬性,我們稱之為類的派生屬性;而像attack(),bite()方法稱之為類的派生方法。
結語:
本篇博客主要專註於解決面向對象的進階知識:類的命名空間,類的組合與繼承。下一篇筆者將代理大家仔細梳理下繼承的進階知識和多態相關知識。
Python學習之旅—面向對象進階知識:類的命名空間,類的組合與繼承