《Python進階程式設計》學習心得——第四章 深入類和對象
總覽

鴨子類型和多态
Java中多态是通過繼承實作的,子類繼承父類(或接口),重寫父類(或接口)的方法,進而實作多态。而在Python中,一個對象從本質上來說是一個字典的封裝,在該字典中,每個key是一個屬性(或方法),每個value是屬性的值或者方法的實作。是以,在Python中,一個對象的類型本質上是由其屬性和方法決定的,比如一個類如果實作了__next__方法和__iter__方法,那該類的對象屬于疊代器類型。
下面用一個例子展示Java和Python實作多态方式的不同:
Java多态
public class Main
{
interface Animal {
public void run();
}
class Dog implements Animal {
@Override
public void run()
{
System.out.println("A dog is running");
}
}
class Cat implements Animal {
@Override
public void run()
{
System.out.println("A cat is running");
}
}
public void outerMethod()
{
Animal[] animals = new Animal[2];
animals[0] = new Dog();
animals[1] = new Cat();
for (Animal animal : animals)
{
animal.run();
}
}
public static void main(String[] args)
{
Main main = new Main();
main.outerMethod();
}
}
執行結果
A dog is running
A cat is running
Python多态
class Dog:
def run(self):
print ("A dog is running")
class Cat:
def run(self):
print ("A cat is running")
if __name__ == '__main__':
animals = (Dog(), Cat())
for animal in animals:
animal.run()
執行結果
A dog is running
A cat is running
抽象基類(abc子產品)
Python的抽象基類類似于Java的接口interface的作用,但與interface不同的是,Python的抽象基類可以定義非抽象的方法。類似類方法(@classmethod)和靜态方法(@staticmethod),Python中抽象方法也是用裝飾器修飾定義。
視訊中老師說為了避免過度設計,Python中不推薦使用抽象基類。但是通過閱讀源碼,我們可以發現Python源代碼中使用到了抽象基類,了解抽象基類有助于我們閱讀、學習Python源碼。
下面用一個例子來看抽象基類的定義和使用。
import abc
class Animal(metaclass=abc.ABCMeta):
@abc.abstractmethod
def run(self):
pass
class Dog(Animal):
"""
If class Dog does not implements abstract method run, TypeError will be raised
"""
pass
if __name__ == '__main__':
dog = Dog()
執行結果
Traceback (most recent call last):
File "AbstractClass.py", line 15, in <module>
dog = Dog()
TypeError: Can't instantiate abstract class Dog with abstract methods run
但其實用NotImplementedError也可以限制Dog類必須實作run方法,隻不過Dog類仍能執行個體化,報錯會發生在Dog類執行個體調用run方法時。
class Animal:
def run(self):
raise NotImplementedError
class Dog(Animal):
pass
if __name__ == '__main__':
dog = Dog()
dog.run()
執行結果
Traceback (most recent call last):
File "AbstractClass.py", line 27, in <module>
dog.run()
File "AbstractClass.py", line 18, in run
raise NotImplementedError
NotImplementedError
使用isinstance而不是type
- isinstance: 子類對象仍是父類的執行個體
- type: 子類對象的類型僅僅是子類,而不能是父類
class Animal:
def run(self):
raise NotImplementedError
class Dog(Animal):
pass
if __name__ == '__main__':
dog = Dog()
print (isinstance(dog, Animal))
print (type(dog) is Animal)
執行結果
True
False
類屬性和對象屬性及其查找順序
類屬性和對象屬性及其覆寫問題比較簡單,僅用下面一個例子說明:
class A:
aa = 1 # class property
def __init__(self, x, y):
self.x = x # object property
self.y = y # object property
if __name__ == '__main__':
a = A(2,3)
b = A(4,5)
a.aa = 11
print(a.aa)
print(b.aa)
print(A.aa)
執行結果
11
1
1
在多繼承時,屬性的查找順序遵循MRO(method resolution order). 在Python2中有經典類和新式類之分,而在Python3中,不管類定義時是否顯式地繼承了object, 都是新式類。新式類的MRO算法是C3算法,具體算法比較複雜,可以簡單了解C3算法既不同于BFS,也不同于DFS,總的原則是優先左邊的父類和優先淺層的父類. 作為屬性/方法的一個特例,super關鍵字在調用父類的__init__方法時也遵循MRO的順序。實際上,super不是調用父類的__init__方法,而是調用__mro__中下一個類的__init__方法。下面用兩個例子來說明Python3的MRO:
Case1: 菱形繼承
class A:
def __init__(self):
print('A')
class B(A):
def __init__(self):
super().__init__()
print('B')
class C(A):
def __init__(self):
super().__init__()
print('C')
class D(B, C):
def __init__(self):
super().__init__()
print('D')
if __name__ == '__main__':
d = D()
print(D.__mro__)
執行結果
A
C
B
D
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
解釋:MRO為D->B->C->A->object. D的super調用B的__init__, B的super調用C的__init__, C的super調用A的__init__.
Case2: 樹形繼承
class A:
def __init__(self):
print('A')
class C(A):
def __init__(self):
super().__init__()
print('C')
class B:
def __init__(self):
print('B')
class D(B):
def __init__(self):
super().__init__()
print('D')
class E(C, D):
def __init__(self):
super().__init__()
print('E')
if __name__ == '__main__':
e = E()
print(E.__mro__)
執行結果
A
C
E
(<class '__main__.E'>, <class '__main__.C'>, <class '__main__.A'>, <class '__main__.D'>, <class '__main__.B'>, <class 'object'>)
解釋:MRO為E->C->A->D->B->object. E的super調用C的__init__, C的super調用A的__init__.
如果想要在子類E的__init__中同時調用所有父類的__init__,則可以改寫class E為:
class E(C, D):
def __init__(self):
C.__init__(self)
D.__init__(self)
# super().__init__()
print('E')
執行結果
A
C
B
D
E
(<class '__main__.E'>, <class '__main__.C'>, <class '__main__.A'>, <class '__main__.D'>, <class '__main__.B'>, <class 'object'>)
靜态方法,類方法及對象方法
這部分筆者已經比較熟悉了,就簡單說一下。靜态方法、類方法及對象方法不太好拿Java類比,可能和C++做比較牽強的類比(并不完全相等,在命名空間等方面有所不同):
C++ | Python |
---|---|
全局函數 | 靜态方法(@staticmethod修飾) |
類方法(static關鍵字修飾) | 類方法(@classmethod修飾) |
執行個體方法 | 對象方法 |
寫一個Python的類方法的常見使用場景。衆所周知,Python是不支援函數重載的(為什麼Python不支援函數重載,又可以引出一個話題,在此不表)。是以,一種支援多個構造函數的迂回方案就可以利用類方法實作:
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
def __str__(self):
return str(self.year) + '-' + str(self.month) + '-' + str(self.day)
@classmethod
def from_str(cls, date_str):
year, month, day = date_str.split('-')
return cls(int(year), int(month), int(day))
if __name__ == '__main__':
day1 = Date(2019, 3, 14)
print(day1)
day2 = Date.from_str('2019-03-14')
print(day2)
注:cls和self一樣,是類方法中大家約定俗成的第一個參數,但不是Python關鍵字
執行結果
2019-3-14
2019-3-14
資料封裝和私有屬性
Python沒有資料通路控制權限關鍵字,Python類的所有屬性和方法預設都是public權限,約定俗成地,用_(一條下劃線)開頭表示protected, 用__(兩條下劃線)開頭表示private.
class A:
def __init__(self):
self.__a = 1
self._b = 2
def getA(self):
return self.__a
def getB(self):
return self._b
class B(A):
def __init__(self):
super().__init__()
self._b = 3
if __name__ == '__main__':
b = B()
print('a: {}, b: {}'.format(b.getA(), b.getB()))
可以正常執行,而直接通路私有屬性
則會報錯:
Traceback (most recent call last):
File "private.py", line 22, in <module>
print(b.__a)
AttributeError: 'B' object has no attribute '__a'
實際上,私有屬性是可以通過"_classname__attribute"的方式直接通路的
print(b._A__a)
>> 1
由此可見,Python的通路權限控制并不嚴格,隻是在編譯的時候用了一個屬性重命名的小技巧。
Python對象的自省機制
用"對象.__dict__"或"dir(對象)"可以得到對象/類的屬性/方法清單。其中"對象.__dict__"傳回對象相對于父類新增的(說法不嚴謹,望大神指正)屬性名和屬性值的字典,"dir(對象)"傳回對象的所有屬性清單(比__dict__更全,但沒有值).
>>> class A:
... def __init__(self, x):
... self.x = x
...
>>>
>>> a = A(1)
>>> dir(a)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'x']
>>> dir(A)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> a.__dict__
{'x': 1}
>>> A.__dict__
mappingproxy({'__module__': '__main__', '__init__': <function A.__init__ at 0x000001DC7FA29620>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None})
with語句和上下文管理
首先視訊講了try-except-finally三部曲的一些文法規定,這裡Python的try-except-finally的文法和Java的try-catch-finally一樣,這裡就不再贅述了。
with關鍵字是對try-except-finally三部曲的改進。在申請和釋放資源(打開/關閉檔案、資料庫、socket連接配接)時,常用with關鍵字。
進一步地,with+一個符合上下文管理協定的對象+as+對象别名,構成上下文管理器的正确文法。實作了__enter__和__exit__的類是符合上下文管理協定的類。以我們常用的with關鍵字操作檔案為例,用open建立了檔案對象file後,dir(file)可以看到,file是實作了"__enter__“和”__exit__"方法的,是以file是一個上下文管理器。
>>> file = open('Main.java', 'r')
>>> dir(file)
['_CHUNK_SIZE', '__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_checkClosed', '_checkReadable', '_checkSeekable', '_checkWritable', '_finalizing', 'buffer', 'close', 'closed', 'detach', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'line_buffering', 'mode', 'name', 'newlines', 'read', 'readable', 'readline', 'readlines', 'seek', 'seekable', 'tell', 'truncate', 'writable', 'write', 'writelines']
>>> file.close()