1. 程式人生 > >週末班:Python基礎之面向物件進階

週末班:Python基礎之面向物件進階

面向物件進階

型別判斷

issubclass

首先,我們先看issubclass() 這個內建函式可以幫我們判斷x類是否是y型別的子類。

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)  # 此時的chi既不是靜態⽅法, 也不是例項⽅法, 更不是類⽅法. 就相當於你在類中寫了個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__和__solt__

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__就會為例項使用一種更加緊湊的內部表示。例項通過一個很小的固定大小的陣列來構建而不是為每個例項定義一個字典,這跟元組或列表很類似。在__slots__中列出的屬性名在內部被對映到這個陣列的指定小標上。

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__方法,我們可以藉此自定義建立物件的行為。

舉個例子:

單例模式

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)

 

元類