Python的魔法函式

昀溪發表於2018-08-18

概要

  1. 如何定義一個類
  2. 類裡通常包含什麼
  3. 各個部分解釋
  4. 類是怎麼來的
  5. type和object的關係
  6. 判斷物件的型別
  7. 上下文管理器

類結構

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com


# 類名後面寫(object)這種寫法表示是新式類,不寫object是經典類,兩種的區別是多繼承的問題。
class OOP(object):

    # 定義類變數,類共享的,那麼例項A修改以後例項B也會受到影響(修改不可比那物件感官上看不到修改,如果是可變型別比如列表,感官上就有影響了),
    # 所以類變數是定義共有的屬性通常不能修改,雖然技術上也可以修改。不用例項化也可以訪問類變數。
    var1 = "hello"  # 不可變物件
    list1 = []  # 可變物件

    def __call__(self, *args, **kwargs):
        """
        這個函式不是解構函式也不是建構函式,物件或者例項可以像函式一樣呼叫,就像執行普通函式一樣。這個如果不需要也不用寫。
        :param args:
        :param kwargs:
        :return:
        """
        print("我是特殊函式 __call__")

    def __init__(self):
        """
        建構函式,用於初始化類的例項,例項化物件的時候就會自動呼叫這個方法,比如把例項變數繫結到例項上。
        這裡這個self其實有些特別,也就是例項化的時候會自動把例項物件傳遞進來,self就是例項物件。
        oop = OOP() 這裡其實是 OOP(oop) 所以self就是例項oop,這個self也就是為了接受隱式傳遞進來的例項名稱
        除了在 __init__ 方法中定義的東西屬於例項自己之外也就是在記憶體中也是獨立的,其他的內容包括類變數、類裡面的方法在記憶體中都只有一份所有
        例項公用。
多說一句,其實例項化的時候先執行__new__方法,該方法呼叫__init__方法。
""" # 在 __init__ 方法裡定義的變數是例項變數,例項獨享的。透過例項呼叫變數或者其他資源是先找例項本身,如果沒有就找類的。 # 比如類變數和例項變數同名,你透過例項訪問這個變數時,它會給你返回例項變數的值。 # 加 "_" 表示私有變數,不過使用 "_" 也可以透過某種方式直接訪問,所以要想使用嚴格的私有變數要使用 "__" 雙下劃線。 self._Var2 = "" print("我是建構函式 __init__") def __del__(self): """ 解構函式,當例項物件刪除時候呼叫,所以不需要設定引數,你也傳遞不了引數。它是在例項銷燬的時候自動呼叫的。 對於python直譯器來說,它有垃圾回收機制,只要有例項存在直譯器就認為這個類被使用。如果你del這個例項,其實你刪除的 是這個例項,也就意味著切斷了例項和類的關係,當直譯器發現某個類沒有被引用了就可以在記憶體中刪除了。 例項儲存的是指向類的指標,例項放在棧裡,類放在堆裡。基本資料型別放在棧裡,非基本資料型別真實的資料都是在堆裡,而這個變數名在棧裡。 這個方法你通常不用寫。 """ print("我是解構函式 __del__") def myMethod(self): """ 為什麼類裡面的每一個方法都是有self呢?因為我們呼叫的時候雖然是透過例項名稱來呼叫,但是實際上是 OOP.myMethod(oop)這種形式,你發現它還是會自動把例項傳進來,這個self就是用來接收這個例項的。所以 你在自己的方法裡面可以訪問變數。例項 oop 本身沒有 myMethod()方法,之所以可以呼叫成功就是 OOP.myMonth(oop) 完成的。 對於例項來說類裡面的方法都是公用的,你例項化多個物件其實這些物件是多個,但是它們所公用的類方法和類變數在記憶體中也只有一份, 哪個個例項呼叫這個方法那麼這個self就是哪個例項。記住一句話例項呼叫方法都是對例項自身程式操作的。 再說詳細點,2個針對於同一個類的例項,A例項操作一個方法會影響B例項麼?顯然不能,這就是self必須存在的原因,在JAVA中也是一樣,只是它使用this。 :return: """ print("自定義方法") # 設定成屬性用於獲取內部變數 @property def Var(self): return self._Var2 # 設定成屬性形式賦值給內部變數,這個必須寫在@property下面 @Var.setter def Var(self, value): self._Var2 = value def main(): oop = OOP() oop1 = OOP() # 呼叫例項的 __call__函式 oop() # 透過屬性方式修改變數 oop.Var = "world" print(oop.Var) # 判斷是否有某種屬性 # print(hasattr(oop, "_var1")) # 修改不可變物件(類變數) print("透過例項oop檢視類變數var1的值:", oop.var1) print("透過例項oop1檢視類變數var1的值:", oop1.var1) print("透過例項oop修改類變數var1的值:改為100") oop.var1 = 100 print("透過例項oop檢視類變數var1的值:", oop.var1) print("透過例項oop1檢視類變數var1的值:", oop1.var1) # 從值上看感覺類變數也屬於例項,因為修改不影響,下面列印id你就看出來了 # print("透過例項oop檢視類變數var1的記憶體地址:", id(oop.var1)) # print("透過例項oop1檢視類變數var1的記憶體地址:", id(oop1.var1)) # print("透過例項oop修改類變數var1的值:改為100") # oop.var1 = 100 # print("透過例項oop檢視類變數var1的記憶體地址:", id(oop.var1)) # print("透過例項oop1檢視類變數var1的記憶體地址:", id(oop1.var1)) # 修改類變數可變型別變數 print("oop例項向list1新增一個元素A") oop.list1.append("A") print("oop的 list1 內容為:", oop.list1) print("oop1的 list1 內容為:", oop1.list1) if __name__ == '__main__': main()

關於類裡面的類變數和例項變數還需要在說一下

#!/usr/bin/env python
# -*- coding: utf-8 -*-

class A:
    # 類變數
    a = 11

    def __init__(self, x, y):
        # 例項變數
        self.x = x
        self.y = y

a1 = A(1, 2)
# 這裡你以為是修改的類A裡面的a嗎?其實不是,為什麼,看下面
a1.a = 100
"""
這樣定義其實會讓a1這個例項多一個bb屬性出來(新建到a1這個例項中),
所以上面那個a1.a 其實並不是A類裡面的類變數a,而是屬於例項自己的
"""
a1.bb = 200
print(a1.x, a1.y, a1.a, a1.bb)  # 這也就是a1為什麼會有bb


a2 = A(5, 6)
# 這裡的a值還是11,a2這個例項本身沒有a這個變數,但是它自己沒有就會向上查詢,它的上一級就是A這個類
print(a2.x, a2.y, a2.a)


A.a = 500  # 這樣才會修改類變數,修改之後例項化的物件(之前或者之後)理論上都受到影響
print(A.a)
a3 = A(8, 9)
print("例項a3的a: ", a3.a)

print("例項a2的a: ", a2.a)
# a1之所以不受影響是因為之前 a1自己定義了一個a屬性,這樣它自己就有a屬性,從而就不用去找類的a屬性了
print("例項a1的a: ", a1.a)

類中其他特殊方法(也叫做魔法方法)

__dic__

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com


class TestObj(object):

    # 類變數
    var1 = 999

    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.list1 = ['A', 'B']
        self.set1 = (1, 2)
        self.dic1 = {"a": "a", "b": "b"}


def main():
    TO = TestObj(name="Tom", age=23)
    print(TO.__dict__)
    TO.name = "張三"
    print(TO.__dict__)


if __name__ == '__main__':
    main()

作用:檢視例項裡面的屬性,鍵為屬性名,值為屬性值,那都包含哪些屬性呢? 所有 self.XXX 的都是屬性。而且類變數也算。它永遠返回的是例項當前的最新值。從輸出看到類變數並沒有輸出。

其實這個屬性透過類也可以呼叫

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com


class TestObj(object):

    # 類變數
    var1 = 999

    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.list1 = ['A', 'B']
        self.set1 = (1, 2)
        self.dic1 = {"a": "a", "b": "b"}


def main():
    # TO = TestObj(name="Tom", age=23)
    # print(TO.__dict__)
    # TO.name = "張三"
    # print(TO.__dict__)

    # 透過類來呼叫
    print(TestObj.__dict__)


if __name__ == '__main__':
    main()

除了輸出類變數之外還有一些類本身的東西。

__str__和__unicode__方法

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com


class TestObj(object):

    # 類變數
    var1 = 999

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


def main():
    TO = TestObj(name="Tom", age=23)
    # 列印例項
    print(TO)


if __name__ == '__main__':
    main()

輸出是物件的地址

這是預設輸出,如果你想改變輸出就是透過__str__來設定的,__unicode__是python2的方法,在python3中使用__str__

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com


class TestObj(object):

    # 類變數
    var1 = 999

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

    # 定義__str__方法
    def __str__(self):
        return "我是TestObj"


def main():
    TO = TestObj(name="Tom", age=23)
    # 列印例項
    print(TO)


if __name__ == '__main__':
    main()

作用:在類裡定義這個方法,那麼在列印物件時將輸出該方法的返回值。在python3中是__str__ 在python2中是__unicode__。這個方法用的最多的是在Django的module中。

__getitem__、__setitem__和__delitem__方法

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com


class TestObj(object):

    # 類變數
    var1 = 999

    def __init__(self, name, age):
        self.dic1 = {}

    def __getitem__(self, item):
        print("__getitem__", item)
        return self.dic1[item]

    def __setitem__(self, key, value):
        print("__setitem__", key, value)
        self.dic1[key] = value

    def __delitem__(self, key):
        print("__deleteitem__", key)


def main():
    TO = TestObj(name="Tom", age=23)
    TO["name"] = "Lucy"
    TO["age"] = 23
    print(TO.dic1)
    print(TO["name"])
    del TO["name"]


if __name__ == '__main__':
    main()

執行結果

透過這三個方法可以讓操作例項跟操作字典一樣。至於使用場景我也不知道,目前沒有用到過。

這些魔法方法屬於誰

#!/usr/bin/env python
# -*- coding: utf-8 -*-


class Company(object):
    def __init__(self, employee_list):
        self.employee = employee_list

    def __getitem__(self, item):
        return self.employee[item]


company = Company(["Tom", "Lucy", "Lily"])
print(company[:2])

for item in company:
    print(item)

換成字典就不行了

__getitem__可以讓我們增加一些更加方便的方式去操作物件,但是上面之所以不能切片了是因為字典本身不能切片,而且__getitem__裡面的item是索引並不是鍵,所以當我們換成字典的時候就會出錯。其實這種魔法方法不是為類而建立的的也不是object才有的,而是一種可以豐富對類的操作的一種方式。

上下文管理器

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
正常使用try語句我們是這樣的
"""
def exe_try():
    dic1 = {"A": "2"}
    try:
        print(dic1["B"])
    except KeyError as e:
        # 丟擲異常執行這裡
        print("KeyError")
    else:
        # 不丟擲異常執行這裡
        print("沒有異常")
    finally:
        # 無論是否丟擲異常最後都執行這裡
        print("finished")


# exe_try()

"""
你是否想過一個問題,開啟檔案會丟擲異常,通常開啟檔案後也需要關閉檔案流,為什麼用 with open()語句可以不用手動關閉檔案流呢?
這就是上下文管理器
"""

class Sample:

    def __init__(self):
        # 首先執行這個方法
        print("__init__")

    def __enter__(self):
        # 然後會自動呼叫這個方法,可以理解為獲取資源
        print("__enter__")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # 這個函式會自動呼叫,當跳出with語句的時候,目的是為了釋放資源
        print("__exit__")

    def toDo(self):
        print("to do something")


# 這個用法是不是很像 with open()呢?
with Sample() as sample:
    sample.toDo()


"""
__enter__和__exit__構成了上下文管理器
"""

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
如何把上下文管理器更加簡化一下呢?
"""

import contextlib

@contextlib.contextmanager # 這個裝飾器把下面的函式包裝成上下文管理器,主要利用了yiele的特性
def myFun(arg1):
    print("begin", arg1)  # 相當於 __enter__ 裡面的程式碼
    yield {}  # 這裡必須有個生成器
    print("finished")  # 相當於 __exit__ 裡面的程式碼


with myFun("AAA") as my:
    print("BBB")

類是如何建立的

一切皆物件,任何一個物件都可以找到它屬於什麼型別,那麼類也是物件,那類的型別是什麼呢?

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com

__metaclass__ = type


class TestObj(object):

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

    # 定義__str__方法
    def __str__(self):
        return "我是TestObj"


def main():
    TO = TestObj(name="Tom", age=23)
    print(type(TO))
    print(type(TestObj))


if __name__ == '__main__':
    main()

輸出

類的型別就是type,也就是說類是透過type來建立的。到底是怎麼建立的呢?

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com

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

"""
TestObj 物件名稱
type() 函式
"TestObj" 類名稱
(object,) 繼承自哪裡,可以為空
{} 方法名稱和方法,字典形式 方法就是上面定義的
"""
TestObj = type("TestObj", (object,), {"__init__": __init__})


def main():
    TO = TestObj(name="Tom", age=23)
    print(type(TO))
    print(type(TestObj))


if __name__ == '__main__':
    main()

現在知道為什麼TestObj的type是type了吧。

關於type和object

type用來生成類,而類用來生成例項,所以我們透過type命令可以檢視這個例項或者說是物件是由誰生成的也就是其型別

a = 1
print(type(1), " 生成1")
print(type(int), " 生成int")
print(type(type), " 生成type")

# 基類
class MyClass:
    pass

# object是所有類都要繼承的類也就是最頂層的類,或者說所有類的父類,類的祖先

print("MyClass的基類是:", MyClass.__bases__)
print("type的基類是:", type.__bases__)
print("object的type是:", type(object))
print("Object的基類是:", object.__bases__)

type的基類是object, 而object的type是type,看起來是個環形。object是type的例項,而type又繼承自object,type也是自己的例項。在Python中
一切為物件,list、str、int等都是物件,可能有人問這命名是類啊,沒錯它們是類但也是物件。這一點和JAVA有所區別。這些東西之所有是類但同時也是物件
是因為他們都是type的例項。它們繼承自object但是它們也是type的例項,只有例項才可以叫做物件,否則它們就是類。

type是自己的例項,object是type的例項,而type又繼承了object,str是type的例項同時繼承了objcet。

判斷物件型別

我們知道獲取物件型別透過type來檢視。但是還有一個叫做isinstance,這兩個有什麼區別呢?

#!/usr/bin/env python
# -*- coding: utf-8 -*-


class A:
    pass


class B(A):
    def test(self):
        print("Test")


b = B()

print(isinstance(b, B))
print(isinstance(b, A))

print(type(b) is A)

結果是type不認為例項b是A的型別,但是isinstance則任務b是A的型別,b是B的例項,而B繼承了A所以說b是A的型別也沒有錯。那他倆有什麼區別呢?

發現用 isinstance 和 type 得到的結果不同,因為type(b) 指向的就是B這個類,而A就是A這個類,顯然不相同,雖然B繼承自A,但是用 isinstance 就會得到相同的結果,它會追溯它判斷b是不是A這個型別,因為b是B的例項,而B繼承自A,所以結果為 True。

說到底type不會認為子類的物件的型別是父類,而isinstance則會子類的物件也是父類的型別。

多繼承時super的執行順序

#!/usr/bin/env python
# -*- coding: utf-8 -*-


class A:
    def __init__(self):
        print("A")


class B(A):
    def __init__(self):
        print("B")
        # 呼叫父類方法,Python2寫法
        # super(Son, self).__init__()

        # 呼叫父類方法,Python3簡化寫法
        super().__init__()  #


class C(A):
    def __init__(self):
        print("C")
        super().__init__()  #


class D(B, C):
    def __init__(self):
        print("D")
        super().__init__()  #


# 列印D的MRO看一下
print(D.__mro__)

d = D()

常規上我們說super是呼叫父類方法,沒錯但是不太嚴謹,它是呼叫MRO路徑尋找的下一個類的方法。當你使用單繼承的時候看不出來,
當使用多繼承的時候就會看出來。

__new__方法

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com

class TestObj(object):

    def __init__(self, name, age):
        self.name = name
        self.age = age
        print("__init__方法執行了")

    def __new__(cls, *args, **kwargs):
        """
        該方法會在__init__之前呼叫.
        :return:
        """
        print("__new__方法執行了")

        """
        如果你的類裡自己重寫了 __new__ 方法那麼下面的這個返回必須寫,否則你定義的類將不會執行__init__方法。
        這個類是繼承自object,那麼你其實並不知__new__原本具體都做什麼,出於某些原因你必須要重寫__new__方法,
        當你的邏輯寫完之後,就要透過 object.__new__(cls) 來呼叫父類的__new__方法,完成初始化。
        """
        return object.__new__(cls)


def main():
    TO = TestObj(name="Tom", age=23)
    print(TO.name)


if __name__ == '__main__':
    main()

 

相關文章