1. 程式人生 > >26 - 面向對象高級-上下文管理-反射

26 - 面向對象高級-上下文管理-反射

del 屬性。 打印 對象 random pytho 訪問 活性 type()

目錄

  • 1 上下文管理
    • 1.1 上下文管理的安全性
    • 1.2 返回值
    • 1.3 方法的參數
    • 1.4 計算函數的運行時間
    • 1.5 主要應用場景
    • 1.6 contextlib.contextmanager
  • 2 反射
    • 2.1 反射相關的函數
    • 2.2 反射相關的魔術方法
      • 2.2.1 getattr
      • 2.2.2 setattr
      • 2.2.3 delattr
    • 2.3 getattribute

1 上下文管理

文件IO操作可以對文件對象使用上下文管理,它主要使用with..as..語法.

with open('123.txt') as f:
    print(f)

要想自己寫的類實現上下文管理,那麽需要用到兩個方法__exit__和__enter__.

方法 意義
__enter__ 進入與此對象相關的上下文,如果存放該方法,with語法會把該方法的返回值綁定到as子句中指定的變量上
__exit__ 退出與此對象相關的上下文
class Text:

    def __enter__(self):
        print('enter------')

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit-------')

with Text() as f :
    print('進來了')
print('出來了')


# enter------

# 進來了

# exit-------

# 出來了

實例化對象的時候,並不會調用__enter__方法,只有進入with語句體中,才會調用__enter__方法,然後執行語句體,最後離開with語句塊的時候,再調用__exit__方法.

with可以開啟一個上下文運行環境,在執行前做一些準備工作,執行後,做一些收尾工作,它並不會開啟一個新的作用域.

1.1 上下文管理的安全性

class Text:

    def __enter__(self):
        print('enter------')

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit-------')

with Text() as f :
    raise Exception


# Traceback (most recent call last):

# enter------

#   File "E:/Python - base - code/chapter08面向對象/練習3.py", line 169, in <module>

#     raise Exception

# exit-------

# Exception

我們可以看到,with語句體中當異常拋出時,__exit__已經可以執行完畢,所以上下文管理是安全的.

1.2 返回值

class Text:

    def __enter__(self):
        print('enter------')

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit-------')

with Text() as f :
    print(f)   # None

這裏之所以是None,是因為,__enter__函數的返回值為None,所以如果哪些類的實例化屬性或實例本身要在with語句內部使用,可以在__enter__函數中進行返回.

class Text:
    def __init__(self):
        self.name = 'daxin'

    def __enter__(self):
        print('enter------')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit-------')

with Text() as f:
    print(f.name)   # daxin

1.3 方法的參數

方法的參數如下:

  • __enter__(self):沒有其他參數
  • __exit__(self, exc_type, exc_val, exc_tb): 這三個參數都與異常有關系,如果上下文管理內部沒有產生異常,那麽三個屬性的值都為None,否則
    • exc_type: 異常類型
    • exc_val: 異常的值
    • exc_tb: 異常的追蹤信息

註意:__exit__函數的返回值很重要,當返回值等效為True,表示壓制異常(異常不會上報),等效False時,表示不壓制異常(此時異常會上報)

class A:
    def __init__(self):
        pass

    def __enter__(self):
        print('Enter ~~~~~')

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit ~~~~~')
        print(exc_type)
        print(exc_val)
        print(exc_tb)
        return True

with A():
    raise Exception('Test')


# Enter ~~~~~

# <class 'Exception'>

# Test

# <traceback object at 0x000001E4D0A5C808>

因為__exit__方法return 了True,所以異常被壓制,所以不會異常崩潰。

1.4 計算函數的運行時間

下面來計算一個函數的運行時間,主要有兩個辦法:

  1. 裝飾器(一般人都能想到)
  2. 上下文管理期(一般人很難想到)

裝飾器版本:

import time
import random
import datetime
import functools

def timer(fn):
    @functools.wraps(fn)  # 拷貝屬性信息
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        res = fn(*args, **kwargs)
        stop = (datetime.datetime.now() - start).total_seconds()
        print(stop)
        return res

    return wrapper

@timer
def add(x, y):
    time.sleep(random.randrange(1, 5))
    return x + y

add(4,5)

裝飾器版本2:類裝飾器

import time
import random
import datetime
import functools

class Timer:

    def __init__(self,fn):
        self.fn = fn
        functools.wraps(fn)(self)  # 拷貝用戶函數屬性信息

    def __call__(self, *args, **kwargs):
        start = datetime.datetime.now()
        res = self.fn(*args,**kwargs)
        stop = (datetime.datetime.now() - start).total_seconds()
        print('The fn run time is {}'.format(stop))
        return res

@Timer
def add(x, y):
    '''from add function'''
    time.sleep(random.randrange(1, 5))
    return x + y

print(add(4,5))
print(add.__name__)

上下文管理器方法1:

import time
import random
import datetime

def add(x, y):
    time.sleep(random.randrange(1, 5))
    return x + y

class Timer:

    def __init__(self):
        self.start = None

    def __enter__(self):
        self.start = datetime.datetime.now()

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.stop = (datetime.datetime.now() - self.start).total_seconds()
        print(self.stop)

with Timer():
    add(1, 3)

上下文管理版本2:將要計算的函數當作參數傳入上下問管理器中

import time
import random
import datetime

def add(x, y):
    time.sleep(random.randrange(1, 5))
    return x + y

class Timer:

    def __init__(self, fn):
        self.fn = fn
        self.start = None

    def __enter__(self):
        self.start = datetime.datetime.now()
        return self.fn

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.stop = (datetime.datetime.now() - self.start).total_seconds()
        print(self.stop)

with Timer(add) as f:
    f(1, 3)

1.5 主要應用場景

根據上下文管理的特性,總結出三個常用的場景:

  1. 增強功能:在代碼執行的前後增加代碼,以增強其功能。類似裝飾器的功能。
  2. 資源管理:打來的資源需要關閉,例如文件對象、網絡連接、數據庫連接等。
  3. 權限驗證:在執行代碼之前,做權限的驗證,在__enter__中處理。

1.6 contextlib.contextmanager

????????它是一個裝飾器,用於實現上下文管理,它裝飾一個函數,因為函數沒有像類那樣使用__enter__和__exit__來實現,所以使用contextlib.contextmanger可以使一個函數變為上下文管理器,但是對被裝飾的函數有一個要求,必須包含yeild關鍵字,也就是說這個函數必須返回一個生成器,且只有yield一個值。

這個裝飾器接受一個生成器作為參數

from contextlib import contextmanager

@contextmanager
def add(x, y):
    print('hello')
    yield x + y
    print('bye bye')

print('start')
with add(4, 5) as f:
    print(f)
print('end')


# start

# hello

# 9

# bye bye

# end

根據打印結果我們分析:

  1. 函數中yield語句前面的,在with語句執行時被觸發。
  2. yield返回值被 as 語句交給了f。
  3. yield語句後面的,在退出with語句時執行。

當我們傳入參數add(1, [5,]) 時,異常直接是函數異常退出了,並沒有執行yield後面的類似__enter__方法的語句,怎麽辦呢?可以使用try,finally來捕捉

from contextlib import contextmanager

@contextmanager
def add(x, y):
    try:
        print('hello')
        yield x + y
    finally:
        print('bye bye')

print('start')
with add(1, [5,]) as f:
    print(f)
print('end')

這樣就會打印yield後續語句,雖然會異常退出,但由於錯誤的參數由用戶自主傳遞,那就讓用戶自己去解決吧。

業務邏輯簡單,可以使用函數加contextlib.contextmanager裝飾器實現,業務邏輯復雜的話,可以使用類加__enter__和__exit__來解決。

2 反射

一個對象能夠在運行時,像照鏡子一樣,顯示出其類型信息,這種方法叫做反射。換句話是反射可以在程序運行的同時獲取類型定義的信息,比如通過一個對象,找出它的type、class、attribute或者method等。具有反射能力的函數有:type()、isinstance()、callable()、dir()、getattr()等。

2.1 反射相關的函數

內建函數 含義
getattr(object, name[, default]) 通過name返回object的屬性值。當屬性不存在,將使用default返回。
如果沒有設置default,則拋出AttributeError異常,name必須為字符串。
setattr(obj, name, value) obj的屬性存在,則覆蓋,不存在,則新增。
hasattr(obj, name) 判斷obj是否存在屬性,name必須為字符串,返回值為bool類型
class Person:

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

    def talk(self):
        print("{} is talking".format(self.name))

daxin = Person('daxin',20)
if hasattr(daxin,'name'):         # 判斷daxin是否含有name屬性
    print(getattr(daxin,'name'))  # 如果有,通過getattr獲取name屬性

if not hasattr(daxin,'sing'):     # 判斷daxin沒有sing方法
    setattr(daxin,'sing',lambda self:print("{} is singing".format(self.name)))  # 為實例綁定一個sing方法

daxin.sing()  # 實例調用

????????直接調用時無法執行,提示缺少self參數,想一下,我們定義的方法通常是在類中定義的,在類中我們指定的self參數,在實例調用時會進行傳遞(因為是實例是綁定在方法上的),而我們綁定的sing方法是綁定在實例本身上的,所以這種情況下,是無法幫我們傳遞self參數的,所以我們在函數內部也無法調用實例的參數。

這種動態增加屬性的方式是運行時改變類或者實例的方式,而裝飾器或者Mixin是在定義時就決定了的,因此反射具有更大的靈活性。

命令分發器實例:

class Dispather:
    def __init__(self):
        pass

    def register(self, name, func):
        setattr(self, name, func)

    def run(self):
        while True:
            cmd = input('>>>: ').strip()
            if cmd.lower() == 'quit':
                break
            else:
                getattr(self, cmd.lower())()

d = Dispather()
d.register('ls',lambda :print('hello world'))
d.run()

2.2 反射相關的魔術方法

魔術方法 含義
__getattr__(self, name) 定義當用戶試圖獲取一個不存在的屬性時的行為
__setattr__(self, name, value) 定義當一個屬性被設置時的行為
__delattr__(self, name) 定義當一個屬性被刪除時的行為

2.2.1 getattr

class A:
    def __init__(self):
        pass

    def __getattr__(self, item):
        print('__getattr__')
        return 'daxin'

daxin = A()
print(daxin.name)


# __getattr__

# daxin

????????訪問daxin的一個屬性name,如果不存在,最後會調用__getattr方法,它的返回值就是結果。如果沒有這個方法,就會拋出AttributeError異常,表示找不到屬性。
????????查找屬性的順序為:instance.__dict
--> instance.__class.__dict --> ... --> object的dict,找不到,調用實例的__getattr__

2.2.2 setattr

class A:
    def __init__(self):
        pass

    def __getattr__(self, item):
        print('__getattr__')
        return 'daxin'

    def __setattr__(self, key, value):
        self.key = value   # self.key依舊調用self.__setattr__方法
        # self.__dict__[key] = value 


daxin = A()
daxin.name = 'daxin'  # 調用__setattr__方法
print(daxin.name)

上面的代碼無法執行,會產生遞歸是為什麽呢?

  1. daxin.name = ‘daxin‘ 這裏會調用daxin的__setattr__方法。
  2. daxin的__stattr方法內部使用self.key = value的方法賦值,這種方法等同於self.__setattr = value,所以會產生遞歸。
  3. 利用self.__dict__[key]的方式是直接操作實例的字典,所以不會引起遞歸。

__setattr__()方法,可以攔截對實例屬性的增加、修改操作,如果要設置生效,需要自己修改操作實例的__dict__屬性。

class Person:
    def __init__(self,name):
        self.name = name
        self.__dict__['a'] = 5

    def __getattr__(self, item):
        print('getattr~~~~~')
        return getattr(self,item)

    def __setattr__(self, key, value):
        print('setattr~~~~~~')
        self.__dict__[key] = value
        # setattr(self,key,value)  # 不能這樣寫,這樣寫等同於調用對象的__setattr__方法,會產生遞歸


daxin = Person('daxin')
print(daxin.name)
print(daxin.a)

結果只會輸出1次getattr,因為初始化時,已經為字典創建了一個key,a,所以當訪問實例屬性a時,由於__dict__中存在,所以不會被__getattr__捕獲。

setattr本質上也是通過 instance.attribute = value 的方式賦值的。

2.2.3 delattr

刪除一個屬性時,觸發__delattr__方法的執行。可以阻止通過實例來刪除屬性的操作。

class Person:
    def __init__(self,name):
        self.name = name
        self.__dict__['a'] = 5

    def __getattr__(self, item):
        print('getattr~~~~~')
        return getattr(self,item)

    def __setattr__(self, key, value):
        print('setattr~~~~~~')
        self.__dict__[key] = value

    def __delattr__(self, item):
        print('delattr~~~~~~')
        del self.__dict__[item]  # 刪除實例的屬性, 也可以在這裏啥也不做,提示不能刪除,即可阻止實例的屬性被刪除。

daxin = Person('daxin')
print(daxin.name)
del daxin.a  # 觸發實例的__delattr__方法的執行。

2.3 getattribute

魔術方法 含義
__getattribute__(self, name) 定義當該類的屬性被訪問時的行為

實例所有的屬性訪問,第一個都會調用__getattribute__方法。

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

    def __getattr__(self, item):
        return 'getattr'

    def __getattribute__(self, item):
        pass
        # return 'ABC'
        # raise AttributeError('ABC')

daxin = Person('daxin',20)
print(daxin.name)

上面的例子得到以下結論:

  1. 當我們重寫__getattribute__方法時,它的返回值就是我們屬性訪問的結果。
  2. 如果在__getattribute__方法中拋出AttributeErro異常,那麽最好會執行__getattr__方法,因為屬性沒有找到。

__getattribute__方法中為了避免在該方法中無限遞歸,它的實現應該永遠調用基類的同名方法以訪問需要的任何屬性(object.__getattribute__(self, name))。

實例屬性查找順序:
instance.__getattribute__() --> instance.__dict__ --> instance.__class__.__dict__ --> object.__dict__ --> instance.__getattr__

26 - 面向對象高級-上下文管理-反射