週末班:Python基礎之面向對象進階

面向對象進階

類型判斷

issubclass

首先,咱們先看issubclass() 這個內置函數能夠幫咱們判斷x類是不是y類型的子類。python

class Base:
    pass


class Foo(Base):
    pass


class Bar(Foo):
    pass


print(issubclass(Bar, Foo))  # True
print(issubclass(Foo, Bar))  # False
print(issubclass(Bar, Base))  # True

type

而後咱們來看type,type在前面的學習期間已經使用過了。type(obj) 表示查看obj是由哪一個類建立的。程序員

class Foo:
    pass


obj = Foo()
print(obj, type(obj))  # 查看obj的類

 

isinstance

isinstance也能夠判斷x是y類型的數據。算法

class Base:
    pass


class Foo(Base):
    pass


class Bar(Foo):
    pass


print(isinstance(Foo(), Foo))  # True
print(isinstance(Foo(), Base))  # True
print(isinstance(Foo(), Bar))   # False

isinstance能夠判斷該對象是不是家族體系中的(只能往上判斷類)。數據庫

反射

爲何須要反射?

首先,咱們來看這樣一個需求。編程

從前有一個大牛,寫了一堆特別牛B的代碼。而後放在一個py文件(模塊)中。這個時候你想使用一下大牛寫的東西。可是呢,你首先得知道大牛寫的這些代碼都是幹什麼用的。那就須要你把大牛寫的每個函數跑一下。數組

大牛.py微信

def chi():
    print("⼤⽜一頓吃100個螃蟹")


def he():
    print("⼤牛一頓喝100瓶可樂")


def la():
    print("⼤牛不⽤拉")


def shui():
    print("⼤牛⼀次睡一年")

接下來,到你了。你要去一個一個調用。可是在你調用以前,大牛告訴你,他寫了哪些方法,那如今就能夠這麼辦了:網絡

while 1:
    print('''
        做爲大牛,我幫你寫了
        chi
        he
        la
        shui
        等功能,你本身看着辦吧...
    ''')
    func = input('請輸入你要測試的功能:').strip()
    if func == 'chi':
        daniu.chi()
    elif func == 'he':
        daniu.he()
    elif func == 'la':
        daniu.la()
    elif func == 'shui':
        daniu.shui()
    else:
        print('大牛就這幾個功能,別搞事情!')

這樣寫是寫完了,可是...數據結構

若是大牛寫了100個功能怎麼辦,你要寫100個if判斷嗎?太累了吧,現有的知識解決不了這個問題呀,那怎麼辦?框架

咱們可使用反射來完成這樣的功能,很是簡單,咱們如今能夠獲取要執行的功能,只不過咱們使用input()函數獲取的一個字符串,這個字符串和實際模塊中的函數名是同樣的。那咱們就能夠利用這一點,只要經過字符串動態的訪問模塊中的功能就能夠了,反射就是作這個事情的。

什麼是反射?

以前咱們導入模塊都是,先引入模塊,而後經過模塊去訪問否個咱們要用的功能,如今呢?咱們手動輸入要運行的功能,而後拿着這個功能去模塊裏查找,這就叫反射。

通俗點說就是經過字符串的形式操做對象相關的屬性。Python中的一切事物都是對象(均可以使用反射)

咱們首先來看一下,在Python中使用反射如何解決上面的問題吧。

import daniu
while 1:
    print('''
        做爲大牛,我幫你寫了
        chi
        he
        la
        shui
        等功能,你本身看着辦吧...
    ''')
    func_str = input('請輸入你要測試的功能:').strip()
    if hasattr(daniu, func_str):
        func = getattr(daniu, func_str)
        func()
    else:
        print('大牛就這幾個功能,別搞事情!')

上面的代碼中用到了以下兩個方法:

hasattr(對象, 字符串)是用來判斷對象是否有字符串名稱對應的這個屬性(功能)。

getattr(對象,字符串)是用來獲取對象中字符串名稱對應的屬性(功能)。

由於Python中一切皆對象,因此反射這個特性很是的有用,不少框架中都會用到此特性。

反射應用

接下來,咱們先看個簡單的例子:

class Person:
    country = "China"

    def eat(self):
        pass


# 類中的內容能夠這樣動態的進⾏獲取
print(getattr(Person, "country"))
print(getattr(Person, "eat"))  # 至關於Foo.func 函數
# 對象⼀樣能夠
obj = Person()
print(getattr(obj, "country"))
print(getattr(obj, "eat"))  # 至關於obj.func ⽅法

getattr能夠從模塊中獲取內容,也能夠從類中獲取內容,也能夠從對象中獲取內容。又由於在Python中⼀切皆爲對象,因此把反射理解爲從對象中動態的獲取成員。

反射的四個函數

關於反射, 其實⼀共有4個函數:

  1. hasattr(obj, str) 判斷obj中是否包含str成員
  2. getattr(obj,str) 從obj中獲取str成員。
  3. setattr(obj, str, value) 把obj中的str成員設置成value。這⾥的value能夠是值,也能夠是函數或者⽅法。
  4. delattr(obj, str) 把obj中的str成員刪除掉。
class Foo:
    pass


f = Foo()
print(hasattr(f, 'eat'))  # False
setattr(f, 'eat', "123")
print(f.eat)  # 被添加了⼀個屬性信息

setattr(f, "eat", lambda x: x + 1)
print(f.eat(3))  # 4

print(f.eat)  # 此時的eat既不是靜態⽅法, 也不是實例⽅法, 更不是類⽅法. 就至關於你在類中寫了個self.chi = lambda 是⼀樣的
print(f.__dict__)  # {'eat': <function <lambda> at 0x1015a2048>}
delattr(f, "eat")
print(hasattr(f, "eat"))  # False

注意:以上操做都是在內存中進⾏的,並不會影響你的源代碼。

補充importlib

importlib是一個能夠根據字符串的模塊名實現動態導入模塊的庫。

舉個例子:

目錄結構:

├── aaa.py
├── bbb.py
└── mypackage
    ├── __init__.py
    └── xxx.py

使用importlib動態導入模塊:

bbb.py

import importlib

func = importlib.import_module('aaa')
print(func)
func.f1()


m = importlib.import_module('mypackage.xxx')
print(m.age)

類的其餘成員

列舉類中的其餘常見成員。

 __str__

改變對象的字符串顯示。能夠理解爲使用print函數打印一個對象時,會自動調用對象的__str__方法。

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
    # 定義對象的字符串表示
    def __str__(self):
        return self.name
    
    
s1 = Student('張三', 24)
print(s1)  # 會調用s1的__str__方法

__repr__

在python解釋器環境下,會默認顯示對象的repr表示。

>>> class Student:
...     def __init__(self, name, age):
...         self.name = name
...         self.age = age
...     def __repr__(self):
...         return self.name
... 
>>> s1 = Student('張三', 24)
>>> s1
張三

總結:

 

str函數或者print函數調用的是obj.__str__()
repr函數或者交互式解釋器調用的是obj.__repr__()

注意:
若是__str__沒有被定義,那麼就會使用__repr__來代替輸出。
__str__和__repr__方法的返回值都必須是字符串。

 

__format__

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # 定義對象的字符串表示
    def __str__(self):
        return self.name

    def __repr__(self):
        return self.name

    __format_dict = {
        'n-a': '{obj.name}-{obj.age}',  # 姓名-年齡
        'n:a': '{obj.name}:{obj.age}',  # 姓名:年齡
        'n/a': '{obj.name}/{obj.age}',  # 姓名/年齡
    }

    def __format__(self, format_spec):
        """
        :param format_spec: n-a,n:a,n/a
        :return:
        """
        if not format_spec or format_spec not in self.__format_dict:
            format_spec = 'n-a'
        fmt = self.__format_dict[format_spec]
        return fmt.format(obj=self)


s1 = Student('張三', 24)
ret = format(s1, 'n/a')
print(ret)  # 張三/24
__format__示例

 

 __del__

析構方法,當對象在內存中被釋放時,自動觸發執行。

注:此方法通常無須定義,由於Python是一門高級語言,程序員在使用時無需關心內存的分配和釋放,由於此工做都是交給Python解釋器來執行,因此析構函數的調用是由解釋器在進行垃圾回收時自動觸發執行的。

class A:
    def __del__(self):
        print('刪除了...')


a = A()
print(a)  # <__main__.A object at 0x10164fb00>
del a  # 刪除了...
print(a)  # NameError: name 'a' is not defined
__del__簡單示例

 

__dict__和__slots__

Python中的類,都會從object裏繼承一個__dict__屬性,這個屬性中存放着類的屬性和方法對應的鍵值對。一個類實例化以後,這個類的實例也具備這麼一個__dict__屬性。可是兩者並不相同。 

class A:
    some = 1

    def __init__(self, num):
        self.num = num


a = A(10)
print(a.__dict__)  # {'num': 10}

a.age = 10
print(a.__dict__)  # {'num': 10, 'age': 10}

從上面的例子能夠看出來,實例只保存實例的屬性和方法,類的屬性和方法它是不保存的。正是因爲類和實例有__dict__屬性,因此類和實例能夠在運行過程動態添加屬性和方法。

可是因爲每實例化一個類都要分配一個__dict__變量,容易浪費內存。所以在Python中有一個內置的__slots__屬性。當一個類設置了__slots__屬性後,這個類的__dict__屬性就不存在了(同理,該類的實例也不存在__dict__屬性),如此一來,設置了__slots__屬性的類的屬性,只能是預先設定好的。

當你定義__slots__後,__slots__就會爲實例使用一種更加緊湊的內部表示。實例經過一個很小的固定大小的小型數組來構建的,而不是爲每一個實例都定義一個__dict__字典,在__slots__中列出的屬性名在內部被映射到這個數組的特定索引上。使用__slots__帶來的反作用是咱們沒有辦法給實例添加任何新的屬性了。

注意:儘管__slots__看起來是個很是有用的特性,可是除非你十分確切的知道要使用它,不然儘可能不要使用它。好比定義了__slots__屬性的類就不支持多繼承。__slots__一般都是做爲一種優化工具來使用。--摘自《Python Cookbook》8.4

 

class A:
    __slots__ = ['name', 'age']


a1 = A()
# print(a1.__dict__)  # AttributeError: 'A' object has no attribute '__dict__'
a1.name = '張三'
a1.age = 24
# a1.hobby = '吹牛逼'  # AttributeError: 'A' object has no attribute 'hobby'
print(a1.__slots__)

 

注意事項:
__slots__的不少特性都依賴於普通的基於字典的實現。
另外,定義了__slots__後的類再也不 支持一些普通類特性了,好比多繼承。大多數狀況下,你應該只在那些常常被使用到的用做數據結構的類上定義__slots__,好比在程序中須要建立某個類的幾百萬個實例對象 。
關於__slots__的一個常見誤區是它能夠做爲一個封裝工具來防止用戶給實例增長新的屬性。儘管使用__slots__能夠達到這樣的目的,可是這個並非它的初衷。它更多的是用來做爲一個內存優化工具。

__item__系列

class Foo:
    def __init__(self, name):
        self.name = name

    def __getitem__(self, item):
        print(self.__dict__[item])

    def __setitem__(self, key, value):
        self.__dict__[key] = value

    def __delitem__(self, key):
        print('del obj[key]時,執行我')
        self.__dict__.pop(key)

    def __delattr__(self, item):
        print('del obj.key時,執行我')
        self.__dict__.pop(item)


f1 = Foo('sb')
print(f1.__dict__)
f1['age'] = 18
f1.hobby = '吹牛逼'
del f1.hobby
del f1['age']
f1['name'] = 'alex'
print(f1.__dict__)
__item__系列

__init__

使用Python寫面向對象的代碼的時候咱們都會習慣性寫一個 __init__ 方法,__init__ 方法一般用在初始化一個類實例的時候。例如:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return '<Person: {}({})>'.format(self.name, self.age)


p1 = Person('張三', 24)
print(p1)

上面是__init__最普通的用法了。可是__init__其實不是實例化一個類的時候第一個被調用的方法。當使用 Persion(name, age) 來實例化一個類時,最早被調用的方法實際上是 __new__ 方法。

__new__

其實__init__是在類實例被建立以後調用的,它完成的是類實例的初始化操做,而 __new__方法正是建立這個類實例的方法。

class Person:

    def __new__(cls, *args, **kwargs):
        print('調用__new__,建立類實例')
        return super().__new__(Person)

    def __init__(self, name, age):
        print('調用__init__,初始化實例')
        self.name = name
        self.age = age

    def __str__(self):
        return '<Person: {}({})>'.format(self.name, self.age)


p1 = Person('張三', 24)
print(p1)

輸出:

調用__new__,建立類實例
調用__init__,初始化實例
<Person: 張三(24)>

__new__方法在類定義中不是必須寫的,若是沒定義的話默認會調用object.__new__去建立一個對象(由於建立類的時候默認繼承的就是object)。

若是咱們在類中定義了__new__方法,就是重寫了默認的__new__方法,咱們能夠藉此自定義建立對象的行爲。

舉個例子:

重寫類的__new__方法來實現單例模式。

class Singleton:
    # 重寫__new__方法,實現每一次實例化的時候,返回同一個instance對象
    def __new__(cls, *args, **kw):
        if not hasattr(cls, '_instance'):
            cls._instance = super().__new__(Singleton)
        return cls._instance

    def __init__(self, name, age):
        self.name = name
        self.age = age


s1 = Singleton('張三', 24)
s2 = Singleton('李四', 20)
print(s1, s2)  # 這兩實例都同樣
print(s1.name, s2.name)

 

__call__

__call__ 方法的執行是由對象後加括號觸發的,即:對象()。擁有此方法的對象能夠像函數同樣被調用。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __call__(self, *args, **kwargs):
        print('調用對象的__call__方法')


a = Person('張三', 24)  # 類Person可調用
a()  # 對象a能夠調用

注意: 

__new__、__init__、__call__等方法都不是必須寫的。

__doc__

定義類的描述信息。注意該信息沒法被繼承。

class A:
    """我是A類的描述信息"""
    pass

print(A.__doc__)

__iter__和__next__

以前的課程中講過,若是一個對象擁有了__iter__和__next__方法,那這個對象就是可迭代對象。

class A:
    def __init__(self, start, stop=None):
        if not stop:
            start, stop = 0, start
        self.start = start
        self.stop = stop

    def __iter__(self):
        return self

    def __next__(self):
        if self.start >= self.stop:
            raise StopIteration
        n = self.start
        self.start += 1
        return n


a = A(1, 5)
from collections import Iterator
print(isinstance(a, Iterator))

for i in A(1, 5):
    print(i)

for i in A(5):
    print(i)

__enter__和__exit__

一個對象若是實現了__enter__和___exit__方法,那麼這個對象就支持上下文管理協議,即with語句。

class A:

    def __enter__(self):
        print('進入with語句塊時執行此方法,此方法若是有返回值會賦值給as聲明的變量')

    def __exit__(self, exc_type, exc_val, exc_tb):
        """
        :param exc_type: 異常類型
        :param exc_val: 異常值
        :param exc_tb: 追溯信息
        :return:
        """
        print('退出with代碼塊時執行此方法')
        print('1', exc_type)
        print('2', exc_val)
        print('3', exc_tb)


with A() as f:
    print('進入with語句塊')
    # with語句中代碼塊出現異常,則with後的代碼都沒法執行。
    # raise AttributeError('sb')
print('嘿嘿嘿')

上下文管理協議適用於那些進入和退出以後自動執行一些代碼的場景,好比文件、網絡鏈接、數據庫鏈接或使用鎖的編碼場景等。

 

__len__

擁有__len__方法的對象支持len(obj)操做。

class A:
    def __init__(self):
        self.x = 1
        self.y = 2

    def __len__(self):
        return len(self.__dict__)


a = A()
print(len(a))

__hash__

擁有__hash__方法的對象支持hash(obj)操做。

class A:
    def __init__(self):
        self.x = 1
        self.x = 2

    def __hash__(self):
        return hash(str(self.x) + str(self.x))


a = A()
print(hash(a))

__eq__

擁有__eq__方法的對象支持相等的比較操做。

class A:
    def __init__(self):
        self.x = 1
        self.y = 2

    def __eq__(self,obj):
        if self.x == obj.x and self.y == obj.y:
            return True


a = A()
b = A()
print(a == b)

 

類的繼承

抽象和繼承

抽象是指找出物體之間的共同點,把這些相同的部分抽象成一個劃分標準。

好比黑人和白人之間的共同點是都是人。

抽象最主要的做用是劃分類別(能夠隔離關注點,下降複雜度),看下圖:

繼承

繼承:是基於抽象的結果,經過編程語言去實現它,確定是先經歷抽象這個過程,才能經過繼承的方式去表達出抽象的結構。

 

 

爲何要使用繼承?

代碼重用

使用繼承的一個目的就是實現代碼的重用。

在開發程序的過程當中,若是咱們定義了一個A類,而後又想新實現另一個B類,可是B類的大部份內容與A類相同時咱們就沒有必要從頭實現一個B類了,這時就可使用類的繼承。
經過繼承的方式實現B類,讓B類繼承A類,B類就會‘遺傳’A類的全部屬性和方法(除私有成員外),從而實現代碼重用。

看代碼:

class Animal:
    """
    人和狗都是動物,因此創造一個Animal基類
    """

    def __init__(self, name, age):
        self.name = name  # 人和狗都有暱稱;
        self.age = age  # 人和狗都有年齡

    def eat(self):
        print('{}在吃東西'.format(self.name))


# 建立一個Dog類繼承Animal類
class Dog(Animal):
    pass


# 建立一個Person類繼承Animal類
class Person(Animal):
    pass


# 實例化一我的
p1 = Person('張三', 18)
# 實例化一條狗
d1 = Dog('二哈', 5)
p1.eat()  # 人能吃東西
d1.eat()  # 狗也能吃東西

固然,咱們在子類中還能夠定義本身子類獨有的一些屬性和方法。

class Animal:
    """
    人和狗都是動物,因此創造一個Animal基類
    """

    def __init__(self, name, age):
        self.name = name  # 人和狗都有暱稱;
        self.age = age  # 人和狗都有年齡

    def eat(self):
        print('{}在吃東西'.format(self.name))


# 建立一個Dog類繼承Animal類
class Dog(Animal):
    pass


# 建立一個Person類繼承Animal類
class Person(Animal):

    # Person類能夠本身定義本身獨有的方法
    def dream(self):
        print('{}在作夢...'.format(self.name))


# 實例化一我的
p1 = Person('張三', 18)
# 實例化一條狗
d1 = Dog('二哈', 5)
p1.eat()  # 人能吃東西
d1.eat()  # 狗也能吃東西

p1.dream()  # 人能作夢

固然咱們還能夠重寫父類的方法:

class Animal:
    """
    人和狗都是動物,因此創造一個Animal基類
    """

    def __init__(self, name, age):
        self.name = name  # 人和狗都有暱稱;
        self.age = age  # 人和狗都有年齡

    def eat(self):
        print('{}在吃東西'.format(self.name))


# 建立一個Dog類繼承Animal類
class Dog(Animal):
    # 重寫父類的eat方法,Dog實例之後都使用這個eat方法
    def eat(self):
        print('吃骨頭吃骨頭吃骨頭')

 

規範類的行爲

咱們能夠聲明一個類,在該類中定義了一些方法名可是並無實現具體的功能,等子類繼承我這個類的時候再去實現方法應該有的功能。

舉個例子:

咱們要作一個電商網站,在寫支付功能的時候,咱們的客戶能夠選擇支付寶支付、微信支付、銀聯支付等各類支付方式。那代碼裏應該怎麼實現這個關係呢?

def check_out(payment, money):
    """
    一個結帳業務邏輯函數,實現具體的結帳功能。
    :param payment: 具體的支付方式實例對象
    :param money: 須要支付的金額
    :return:
    """
    payment.pay(money)  # 調用具體支付方式對象的pay方法

如今咱們要動手實現支付寶支付和微信支付,兩名萌萌的程序員就開工了,寫出了下面的代碼:

程序員A:

class AliPay:
    """支付寶支付"""
    def __init__(self):
        # 配置支付寶收款帳號信息
        pass

    # 具體的支付方法
    def pay(self, money):
        print('支付寶支付了{}元'.format(money))

 

程序員B:

class WeChatPay:
    """微信支付"""
    def __init__(self):
        # 配置微信支付收款帳號信息
        pass

    # 具體的支付方法
    def zhifu(self, money):
        print('使用支付寶支付了{}元'.format(money))

此時此刻,咱們就會發現,咱們確實須要爲某些類定義一些統一的規則或約束。

# 定義一個支付的基類
class Payment:
    # 規定子類必須實現pay方法,不然調用實例的pay方法時會拋出異常
    def pay(self, *args, **kwargs):
        raise NotImplementedError


class AliPay(Payment):
    """支付寶支付"""
    def __init__(self):
        # 配置支付寶收款帳號信息
        pass

    # 實現父類中定義好的支付方法
    def pay(self, money):
        print('支付寶支付了{}元'.format(money))


class WeChatPay(Payment):
    """微信支付"""
    def __init__(self):
        # 配置微信支付收款帳號信息
        pass

    # 具體的支付方法
    def pay(self, money):
        print('使用支付寶支付了{}元'.format(money))


p = WeChatPay()
check_out(p, 200)

 

Python中爲了解決上面的問題,已經給咱們提供了一套現成的工具。

from abc import ABCMeta, abstractmethod


class Payment(metaclass=ABCMeta):

    # 子類必須實現pay方法,不然實例化子類的時候就會報錯
    @abstractmethod
    def pay(self, money):
        pass


class WeChatPay(Payment):

    def fuqian(self, money):
        print('微信支付了{}元'.format(money))


p = WeChatPay()  # 實例化時就報錯了

調用父類的方法

咱們在子類中想要調用父類的方法,可使用super()函數來完成。

class A:
    def m1(self):
        print('A.m1')


class B(A):
    def m1(self):
        print('B.m1')
        super().m1()  # 在子類中調用父類中的m1方法


b = B()
b.m1()

super()函數的一種常見用途是調用父類的__init__()方法,以確保父類被正確的初始化了。

class A:
    def __init__(self):
        self.x = 0


class B(A):
    def __init__(self):
        # 先調用父類的__init__()方法
        super().__init__()
        # 執行本身的初始化操做
        self.y = 1


b = B()
print(b.x)
print(b.y)

 

多繼承的順序

Python中支持多繼承!因此會存在一個繼承順序的問題。

首先咱們來看一個經典的鑽石繼承問題:  

class A:
    def __init__(self):
        print('進入A類')
        print('離開A類')


class B(A):
    def __init__(self):
        print('進入B類')
        super().__init__()
        print('離開B類')


class C(A):
    def __init__(self):
        print('進入C類')
        super().__init__()
        print('離開C類')


class D(B, C):
    def __init__(self):
        print('進入D類')
        super().__init__()
        print('離開D類')

其中,A是父類,B, C 繼承自A, D繼承自 B和C,它們的繼承關係以下:

咱們看一下實例化D的輸出結果:

進入D類
進入B類
進入C類
進入A類
離開A類
離開C類
離開B類
離開D類

 

你會發現,super()函數並非簡單的調用父類的方法,由於上面的結果顯示在「進入B類」以後打印的不是 「進入A類」,而是「進入C類」。

看來super()和父類沒什麼實質性的關係,那它是按照什麼規則來調用方法的呢?

MRO列表

對於咱們定義的每個類,Python會計算出一個方法解析順序(Method Resolution Order, MRO)列表,它表明了類繼承的順序,咱們可使用下面的方式得到某個類的 MRO 列表:

[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

 

那這個 MRO 列表的順序是怎麼定的呢,它是經過一個 C3 線性化算法來實現的,這裏咱們就不去深究這個算法了,簡單來講,一個類的 MRO 列表就是合併全部父類的 MRO 列表,並遵循如下三條原則:

  1. 子類永遠在父類前面
  2. 若是有多個父類,會根據它們在列表中的順序被檢查
  3. 若是對下一個類存在兩個合法的選擇,選擇第一個父類

super()函數所作的事情就是在MRO列表中找到當前類的下一個類。

相關文章
相關標籤/搜索