1. 程式人生 > >Python中類的內建方法

Python中類的內建方法

1. __getattr__, __setattr__, __delattr__

呼叫物件的一個不存在的屬性時會觸發__getattr方法
刪除物件的一個屬性的時候會觸發__delattr__方法
設定物件屬性和刪除物件屬性會觸發__setattr____delattr__ 方法,但要注意的是,在呼叫這兩個方法時,方法內部必須操作類的屬性字典,否則會造成無遞迴

class Foo:
    x=1
    def __init__(self,y):
        self.y=y

    def __getattr__(self, item):
        print('run __getattr__'
) def __setattr__(self, key, value): print('run __setattr__') # self.key=value # 增加/修改類屬性的操作會觸發__setattr__方法,這個方法在__setattr__方法內,造成無限遞迴 # self.__dict__[key]=value # 使用這種方法會完成增加/修改類屬性的操作 def __delattr__(self, item): print('run __delattr__') # del self.item # 造成無限遞迴
self.__dict__.pop(item) #__setattr__新增/修改屬性會觸發它的執行 f1=Foo(10) print(f1.__dict__) # 因為重寫了__setattr__,凡是賦值操作都會觸發它的執行,除非直接操作屬性字典,否則永遠無法完成賦值 f1.z=3 print(f1.__dict__) #__delattr__刪除屬性的時候會觸發 f1.__dict__['a']=3 #我們可以直接修改屬性字典,來完成新增/修改屬性的操作 del f1.a print(f1.__dict__) #__getattr__只有在使用點呼叫屬性且屬性不存在的時候才會觸發
f1.xxxxxx

2. __getattribute__

前面介紹了__getattr__,那麼__getattribute__與之有什麼關係呢?


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

    def __getattribute__(self, item):
    print('不管屬性[%s]是否存在,我都會執行' % item)

f1=Foo(10)
f1.x
f1.xxxxxx

>>不管屬性[x]是否存在,我都會執行
不管屬性[xxxxxx]是否存在,我都會執行

__getattr____getattribute__共存時,執行順序是怎樣的?
#當__getattribute__與__getattr__同時存在,只會執行__getattrbute__,除非__getattribute__在執行過程中丟擲異常AttributeError

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

    def __getattr__(self, item):
        print('執行__getattr__, 操作的屬性[%s]' % item)
        # return self.__dict__[item]

    def __getattribute__(self, item):
        print('不管屬性[%s]是否存在, __getattribute__都會執行' % item)
        # raise AttributeError('哈哈')

f1=Foo(10)
f1.x
f1.xxxxxx

# 沒有丟擲異常的執行結果
>>不管屬性[x]是否存在, __getattribute__都會執行
不管屬性[xxxxxx]是否存在, __getattribute__都會執行

#丟擲異常的執行結果
>>不管屬性[x]是否存在, __getattribute__都會執行
執行__getattr__, 操作的屬性[x]
不管屬性[xxxxxx]是否存在, __getattribute__都會執行
執行__getattr__, 操作的屬性[xxxxxx]

3. __getitem__, __setitem__, __delitem__

將對類屬性的操作偽裝成字典形式的操作

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

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

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

    def __delitem__(self, key):
        print('run __delitem__')
        self.__dict__.pop(key)

    def __delattr__(self, item):
        print('run __delattr__')
        self.__dict__.pop(item)

f1=Foo('jack')
f1['age']=18
f1['age1']=19
del f1.age1
del f1['age']
f1['name']='Joe'
print(f1.__dict__)

4. __repr__, __str__, __format__

__str__

假定f為一個類的例項,print(f)等價於呼叫str(f),等價於呼叫f.__str__()
使用f.__str__()可以定製列印方法

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

    def __str__(self):
        return 'name is %s, age is %s' % (self.name, self.age)

f = Foo('Jack', 18)
print(f)

>>name is Jack, age is 18

__repr__

__repr____str__實現的功能是一樣的,如果兩個共存,只會呼叫__str__,在沒有定義__str__的情況下呼叫__repr__
此外在終端中__repr__可以實現定製列印輸出。
這裡寫圖片描述

__format__

date_dic={
    'ymd':'{0.year}:{0.month}:{0.day}',
    'dmy':'{0.day}/{0.month}/{0.year}',
    'mdy':'{0.month}-{0.day}-{0.year}',
}
class Date:
    def __init__(self,year,month,day):
        self.year=year
        self.month=month
        self.day=day

    def __format__(self, format_spec):
        if not format_spec or format_spec not in date_dic:
            format_spec='ymd'
        fmt=date_dic[format_spec]
        return fmt.format(self)

d1=Date(2016,12,29)
print(format(d1, 'dmy'))
print('{:mdy}'.format(d1))
print(date_dic['mdy'].format(d1))

>>29/12/2016
12-29-2016
12-29-2016

5. __slots__

  1. __slots__是什麼:是一個類變數,變數值可以是列表,元祖,或者可迭代物件,也可以是一個字串(意味著所有例項只有一個數據屬性)
  2. 引子:使用點來訪問屬性本質就是在訪問類或者物件的__dict__屬性字典(類的字典是共享的,而每個例項的是獨立的)
  3. 為何使用__slots__:字典會佔用大量記憶體,如果你有一個屬性很少的類,但是有很多例項,為了節省記憶體可以使用__slots__取代例項的__dict__
    當你定義__slots__後,__slots__就會為例項使用一種更加緊湊的內部表示。例項通過一個很小的固定大小的陣列來構建,而不是為每個例項定義一個字典,這跟元組或列表很類似。在__slots__中列出的屬性名在內部被對映到這個陣列的指定小標上。使用__slots__一個不好的地方就是我們不能再給
    例項新增新的屬性了,只能使用在__slots__中定義的那些屬性名。
  4. 注意事項:__slots__的很多特性都依賴於普通的基於字典的實現。另外,定義了__slots__後的類不再 支援一些普通類特性了,比如多繼承。大多數情況下,你應該
    只在那些經常被使用到 的用作資料結構的類上定義__slots__比如在程式中需要建立某個類的幾百萬個例項物件 。
    關於__slots__的一個常見誤區是它可以作為一個封裝工具來防止使用者給例項增加新的屬性。儘管使用__slots__可以達到這樣的目的,但是這個並不是它的初衷。更多的是用來作為一個記憶體優化工具。
class Bar:
    __slots__ = ['x', 'y']

n = Bar()
n.x, n.y = 1, 2
n.z = 3  # 報錯

>>AttributeError: 'Bar' object has no attribute 'z'

6. __del__

__del__稱作析構方法
析構方法,當物件在記憶體中被釋放時,自動觸發執行。
注:此方法一般無須定義,因為在Python中,程式設計師在使用時無需關心記憶體的分配和釋放,因為此工作都是交給Python直譯器來執行,所以,解構函式的呼叫是由直譯器在進行垃圾回收時自動觸發執行的。
在程式執行結束之後,執行此方法

class Foo:

    def __del__(self):
        print('run __del__')

f1=Foo()
# del f1
print('------->')

>>------->
run __del__
#程式中沒有呼叫del方法,在整個程式執行結束之後呼叫__del__方法
class Foo:

    def __del__(self):
        print('run __del__')

f1=Foo()
del f1
print('------->')

>>run __del__
------->
# 呼叫del方法則先執行析構方法,然後執行

7. __call__

物件後面加括號,觸發執行。

注:構造方法的執行是由建立物件觸發的,即:物件 = 類名() ;而對於__call__方法的執行是由物件後加括號觸發的,即:物件() 或者 類()()

class Foo:

    def __init__(self):
        pass

    def __call__(self, *args, **kwargs):

        print('__call__')


obj = Foo() # 執行 __init__
obj()       # 執行 __call__

8. __iter__, __next__

使用__iter__, __next__實現迭代器協議

class Foo:
    def __init__(self,start,stop):
        self.num=start
        self.stop=stop
    def __iter__(self):
        return self
    def __next__(self):
        if self.num >= self.stop:
            raise StopIteration
        n=self.num
        self.num+=1
        return n

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

>>1
2
3
4

9. __get__, __set__, __delete__

9.1 描述符概念

描述符本質上就是一個新式類,在這個類中至少實現了__get__, __set__, __delete__中的一個。
上述概念也可以稱之為描述符協議。
__get__():呼叫一個屬性時,觸發
__set__():為一個屬性賦值時,觸發
__delete__():採用del刪除屬性時,觸發

9.2 描述符的作用

描述符是用來代理另外一個類的屬性,描述符只能定義為類的屬性,而不能在__init__函式中定義。

class Foo(object):
    def __get__(self, instance, owner):
        print('run __get__')

    def __set__(self, instance, value):
        print('run __set__')

    def __delete__(self, instance):
        print('run __delete__')


class Bar(object):
    def __init__(self, n):
        self.x = Foo()

b1 = Bar(9)

# Foo類就是一個描述符,在Bar類中的`__init__`方法中定義了描述符Foo,這樣並沒有觸發描述符的三種操作,也就是上文提到的不可以在`__init__`中定義描述符
class Foo(object):
    def __get__(self, instance, owner):
        print('run __get__')

    def __set__(self, instance, value):
        print('instance: %s, value: %s' % (instance, value))
        print('run __set__')

    def __delete__(self, instance):
        print('run __delete__')


class Bar(object):
    x = Foo()

    def __init__(self, n):
        self.x = n

b1 = Bar(9)

>>instance: <__main__.Bar object at 0x000001C45102D7B8>, value: 9
>>run __set__

# 可以看到在類中定義描述符,並對描述符進行了賦值操作觸發了描述符的`__set__`方法

上面這個例子中還體現出了代理 的概念,x作為Bar類的屬性,其功能全部交由Foo實現,這就是x被Foo代理了,從列印結果可以看出Foo接收到了其代理的Bar類例項化得到的物件和引數,也就可以使用這個物件和引數實現一些功能。

9.3 描述符種類

  1. 資料描述符:至少實現了__get__()__set__()
  2. 非資料描述符:沒有實現__set__()

9.4 描述符注意事項

  • 描述符本身應該定義成新式類,被代理的類也應該是新式類
  • 必須把描述符定義成這個類的類屬性,不能為定義到建構函式中
  • 要嚴格遵循下述優先順序,優先順序由高到底分別是

    • 類屬性
    • 資料描述符
    • 例項屬性
    • 非資料描述符
    • 屬性不存在觸發__getattr__()

9.4.1 描述符優先順序之[類屬性>資料描述符]

類呼叫類的屬性實質上就是在呼叫類的屬性字典裡的屬性,如果類記憶體在於描述符同名的屬性,那麼類內的屬性對類屬性字典中該屬性名對應的屬性進行了重寫,因此類在呼叫該方法時呼叫的是類的屬性而不是資料描述符,下面通過一個例子說明這一點。

class Foo(object):
    def __get__(self, instance, owner):
        print('run __get__')

    def __set__(self, instance, value):
        print('instance: %s, value: %s' % (instance, value))
        print('run __set__')

    def __delete__(self, instance):
        print('run __delete__')


class Bar(object):
    x = Foo()

    def __init__(self, m):
        self.x = m

b1 = Bar(7)
print(Bar.__dict__)
Bar.x = 1
print(Bar.__dict__)

>>run __set__
>>'x': <__main__.Foo object at 0x000001308921D748>
>>'x': 1

# 上述程式執行結果對字典的列印只截取了與屬性 x 相關的部分,可以看出在Bar類中沒有x屬性時,Bar呼叫的 x 屬性時類Foo的一個物件
# 在為Bar類中 x 屬性重新賦值後,Bar類有了自己的 x 屬性,也就不會去呼叫Foo類中的x屬性,也就不會觸發Foo中的__set__方法

9.4.2 描述符優先順序之[資料描述符>例項屬性]

類的例項對所要呼叫的屬性的查詢屬性是: —>物件屬性字典—>類屬性字典。
類例項化得到的物件呼叫的屬性在屬性字典中找不到時會去類的屬性字典找,即使物件對屬性進行重新賦值,操作的也是類的屬性,而不會在自己的物件屬性字典中新增屬性。那麼,當物件呼叫的屬性是由資料描述符代理的,那麼在類中沒有同名的類屬性時就會去描述符所在的類屬性字典中去查詢。
下面通過一個例子說明這一點

# 類的定義依然使用上述例子
b1 = Bar(7)
b1.x
b1.x = 8

>>instance: <__main__.Bar object at 0x000001F00263D7F0>, value: 7
run __set__
run __get__
instance: <__main__.Bar object at 0x000001F00263D7F0>, value: 8
run __set__

# 可以看出在執行類例項化時執行了Bar類的__set__方法,在例項物件呼叫該屬性時呼叫了Bar類的__get__方法,對x屬性進行重新賦值的操作時,再一次觸發了Bar類的__set__方法
# 如果列印b1.__dict__,可以返現b1的屬性字典中沒有x這個屬性
# 如果將Bar類中 x = Foo()這個描述符去掉,替換為一個不同類屬性,那麼再次列印b1的屬性字典會發現b1的屬性字典中添加了x這個屬性

9.4.3 描述符優先順序之[例項屬性>非資料描述符]

首先引入一個例子

class Foo(object):
    def __get__(self, instance, owner):
        print('run __get__')


class Bar(object):
    x = Foo()
    def __init__(self, m):
        self.x = m

b1 = Bar(7)
b1.x = 8

>>{'x': 8}

從上述例子可以看出由於描述符中沒有__set__方法,在物件對x屬性進行賦值操作中無法觸發描述符中不存在的__set__方法, 這導致結果是b1在自己的屬性字典中添加了屬性。
這體現了例項屬性的優先順序高於非資料描述符,優先順序其實就是一種查詢順序。

9.4.4 描述符優先順序之[非資料描述符>屬性不存在]

當屬性不存在時,呼叫該屬性(只調用不賦值)時,存在非資料描述符就呼叫描述符中的__get__方法,如果不存在描述符也就無法呼叫非資料描述符中的__get__方法,如果物件本身所在的類中沒有定義__getattr__方法就報錯,如果定義了就觸發__getattr__方法。

9.5 描述符應用

9.5.1 簡單描述符應用

通過描述符可以為控制類內屬性的行為,比如可以強行控制類內資料屬性的型別,初始化時使用的資料型別不是目標資料型別則報錯

class Typed(object):
    def __init__(self, val, typ):
        self.val = val
        self.typ = typ

    def __get__(self, instance, owner):
        print('instance: [%s], owner: [%s] in __get__' % (instance, owner))
        if instance is None:
            return self

        return instance.__dict__[self.val]

    def __set__(self, instance, value):
        print('instance: [%s], value: [%s] in __set__' % (instance, value))
        if not isinstance(value, self.typ):
            raise TypeError('Type of %s should be %s' % (self.val, self.typ))
        instance.__dict__[self.val] = value

    def __delete__(self, instance):
        print('instance: [%s]' % instance)
        instance.__dict__.pop(self.val)


class People(object):
    name = Typed('name', str)
    age = Typed('age', int)
    salary = Typed('salary', float)

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

# 型別沒有錯誤的輸出
p1 = People('Jack', 3, 3.0)
>>
instance: [<__main__.People object at 0x000001EA58E6DC50>], value: [Jack] in __set__
instance: [<__main__.People object at 0x000001EA58E6DC50>], value: [3] in __set__
instance: [<__main__.People object at 0x000001EA58E6DC50>], value: [3.0] in __set__

# 型別出錯
p1 = People(777, 3, 3.0)
>>
TypeError: Type of name should be <class 'str'>
# 可以看出這裡丟擲的異常是Typed類中`__set__`中定義的錯誤輸出方式

在描述符簡單應用這一節中已經實現了對類屬性的定製,但這樣僅僅是實現了功能,如果新增屬性(例如People類中新增gender屬性), 那麼就需要在類中再新增一行salary = Typed('salary', float),這樣就弄的很麻煩。可不可以使用一種方法,為類內沒一個屬性都新增一個型別檢測的功能呢?答案是有的,可以通過為類設計裝飾器試下這一想法,下面對類的裝飾器的使用方法簡要介紹一下。

9.5.2 類裝飾器

無參裝飾器


#decorator with no arguments
def deco(cls):
    print('cls decorator')
    return cls

@deco
class People(object):
    def __init__(self, name, age, gender, salary):
        self.name = name
        self.age = age
        self.gender = gender
        self.salary = salary

p1 = People('J', 18, 'male', 9.9)
>>
cls decorator

有參裝飾器

# decorator with arguments
def deco(*args, **kwargs):
    def wrapper(cls):
        for k, v in kwargs.items():
            print('key: %s, value: %s' % (k, v))
        return cls
    return wrapper


@deco(name=str, age=int, gender=str, salary=float)  # People = deco(People)
class People(object):
    def __init__(self, name, age, gender, salary):
        self.name = name
        self.age = age
        self.gender = gender
        self.salary = salary

p1 = People('J', 18, 'male', 9.9)

>>
key: name, value: <class 'str'>
key: age, value: <class 'int'>
key: gender, value: <class 'str'>
key: salary, value: <class 'float'>

9.5.3

在9.5.1中實現了為類屬性限制其型別,但實現方法比較麻煩,無法做到通用,裝飾器可以為類新增功能,並通用於這個類,結合這兩點,使用裝飾器搭配描述符實現類屬性型別限制,如下所示

class TypeCheck(object):
    def __init__(self, name, expect_type):
        self.name = name
        self.expect_type = expect_type

    def __get__(self, instance, owner):
        if instance in None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, self.expect_type):
            raise TypeError('%s should be type of %s' % (self.name, self.expect_type))
        instance.__dict__[self.name] = self.expect_type

    def __delete__(self, instance):
        instance.__dict__.pop(self.name)

# decorator with arguments
def deco(*args, **kwargs):
    def wrapper(cls):
        for k, v in kwargs.items():
            setattr(cls, k, TypeCheck(k, v))
        return cls
    return wrapper


@deco(name=str, age=int, gender=str, salary=float) # 新增salary的屬性檢測
class People(object):
    def __init__(self, name, age, gender, salary):  # 新增salary屬性
        self.name = name
        self.age = age
        self.gender = gender
        self.salary = salary

p1 = People(7, 18, 'male', 9.9)

>>
TypeError: name should be type of <class 'str'>

9.5.4 描述符的威力

上述例子已經展現出描述符的一些特性,或許看起來沒什麼,但Python中包括@classmethod,@staticmethd,@property甚至是__slots__屬性都是基於描述符實現的

10. __enter__, __exit__

使用__enter__, __exit__可以實現with語句,通過下面這個例子簡要說明一下這兩個函式在with語句中的執行順序

class Open(object):
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        print('run __enter__')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('run __exit__')
        print('exc_type: [%s]' % exc_type)
        print('exc_val: [%s]' % exc_val)
        print('exc_tb: [%s]' % exc_tb)
        return True

with Open('a.txt') as f:
    print('test start...')
    print(f.name)
    print(dfsdfasdfasdasfsa)
    print('anything else')

print('---end---')

>>
run __enter__
test start...
a.txt
run __exit__
exc_type: [<class 'NameError'>] # 異常型別
exc_val: [name 'dfsdfasdfasdasfsa' is not defined] # 異常內容
exc_tb: [<traceback object at 0x00000170FAD639C8>] # traceback
---end---

# 執行with語句觸發`__enter__`,拿到返回值賦值給f,執行with裡面的程式碼塊,如果沒有異常,程式碼塊執行結束後執行`__exit__`,exit的三個返回值都為None
# 如果with中的語句執行過程中發生異常,則後續程式不再執行,exit語句中如果最後return True則with整個程式碼塊後的語句還可以繼續執行,如果沒有return True那麼則直接丟擲異常終止程式。

with語句的應用:

  • 使用with語句的目的就是把程式碼塊放入with中執行,with結束後,自動完成清理工作,無須手動干預
  • 在需要管理一些資源比如檔案,網路連線和鎖的程式設計環境中,可以在__exit__中定製自動釋放資源的機制,無須再去關心這個問題。

11. __copy__, __deepcopy__

12. __hash__

13. 操作符過載

在C++中有一種強大的特性叫做操作符過載,可以實現類與類之間的運算與比較,例如兩個類的相加,在Python中通過重新定義相應的內建方法可以實現同樣的功能
下面從兩個角度進行介紹:比較操作符,運算操作符

13.1 比較操作符

__gt__, __lt__, __ge__, __le__, __eq__

function name description
__gt__(self, other) 判斷self物件是否大於other物件
__lt__(self, other) 判斷self物件是否小於other物件
__ge__(self, other) 判斷self物件是否大於等於other物件
__le__(self, other) 判斷self物件是否小於等於other物件
__eq__(self, other) 判斷self物件是否等於other物件

13.2 運算操作符

__add__, __sub__, __mul__, __divmod__

13.3 關係操作符

__and__, __or__, __xor__

class A:
    def __init__(self, val):
        self.val = val

    def __or__(self, other):
        return bool(self.val | other.val)

    def __and__(self, other):
        return bool(self.val & other.val)

    def __xor__(self, other):
        return bool(self.val ^ other.val)


a1 = A(1)
a2 = A(0)

print(a1 | a2)
print(a1 & a2)
print(a1 ^ a2)

>>
True
False
True