天天看點

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

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

總覽

《Python進階程式設計》學習心得——第四章 深入類和對象《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: 菱形繼承

《Python進階程式設計》學習心得——第四章 深入類和對象《Python進階程式設計》學習心得——第四章 深入類和對象
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: 樹形繼承

《Python進階程式設計》學習心得——第四章 深入類和對象《Python進階程式設計》學習心得——第四章 深入類和對象
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()