聽說你還不懂面向對象??

定義

"把一組數據結構和處理它們的方法組成對象(object),把相同行爲的對象歸納爲類(class),通過類的封裝(encapsulation)隱藏內部細節,通過繼承(inheritance)實現類的特化(specialization)和泛化(generalization),通過多態(polymorphism)實現基於對象類型的動態分派。"
下面這段可能更好理解
在這裏插入圖片描述

類和對象

簡單的說,類是對象的藍圖和模板,而對象是類的實例。這個解釋雖然有點像用概念在解釋概念,但是從這句話我們至少可以看出,類是抽象的概念,而對象是具體的東西。在面向對象編程的世界中,一切皆爲對象,對象都有屬性和行爲,每個對象都是獨一無二的,而且對象一定屬於某個類(型)。當我們把一大堆擁有共同特徵的對象的靜態特徵(屬性)和動態特徵(行爲)都抽取出來後,就可以定義出一個叫做“類”的東西。

定義類

在Python中可以使用class關鍵字定義類,然後在類中通過之前學習過的函數來定義方法,這樣就可以將對象的動態特徵描述出來,代碼如下所示。

class Student(object):

    # __init__是一個特殊方法用於在創建對象時進行初始化操作
    # 通過這個方法我們可以爲學生對象綁定name和age兩個屬性
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def study(self, course_name):
        print('%s正在學習%s.' % (self.name, course_name))

    # PEP 8要求標識符的名字用全小寫多個單詞用下劃線連接
    # 但是部分程序員和公司更傾向於使用駝峯命名法(駝峯標識)
    def watch_movie(self):
        if self.age < 18:
            print('%s只能觀看《熊出沒》.' % self.name)
        else:
            print('%s正在觀看島國愛情大電影.' % self.name)
            

創建和使用對象

當我們定義好一個類之後,可以通過下面的方式來創建對象並給對象發消息

def main():
    # 創建學生對象並指定姓名和年齡
    stu1 = Student('趙四', 38)
    # 給對象發study消息
    stu1.study('Python程序設計')
    # 給對象發watch_av消息
    stu1.watch_movie()
    stu2 = Student('劉能', 15)
    stu2.study('思想品德')
    stu2.watch_movie()


if __name__ == '__main__':
    main()


趙四正在學習Python程序設計.
趙四正在觀看島國愛情大電影.
劉能正在學習思想品德.
劉能只能觀看《熊出沒》.

訪問可見性問題

對於上面的代碼,有C++、Java、C#等編程經驗的程序員可能會問,我們給Student對象綁定的name和age屬性到底具有怎樣的訪問權限(也稱爲可見性)。因爲在很多面向對象編程語言中,我們通常會將對象的屬性設置爲私有的(private)或受保護的(protected),簡單的說就是不允許外界訪問,而對象的方法通常都是公開的(public),因爲公開的方法就是對象能夠接受的消息。在Python中,屬性和方法的訪問權限只有兩種,也就是公開的和私有的,如果希望屬性是私有的,在給屬性命名時可以用兩個下劃線作爲開頭,下面的代碼可以驗證這一點。

class Test:

    def __init__(self, foo):
        self.__foo = foo

    def __bar(self):
        print(self.__foo)
        print('__bar')


def main():
    test = Test('hello')
    # __bar屬性是Test類私有的,不能單獨調用
    test.__bar()
    # AttributeError: 'Test' object has no attribute '__foo'
    print(test.__foo)


if __name__ == "__main__":
    main()

但是,Python並沒有從語法上嚴格保證私有屬性或方法的私密性,它只是給私有的屬性和方法換了一個名字來妨礙對它們的訪問,事實上如果你知道更換名字的規則仍然可以訪問到它們,下面的代碼就可以驗證這一點。之所以這樣設定,可以用這樣一句名言加以解釋,就是"We are all consenting adults here"。因爲絕大多數程序員都認爲開放比封閉要好,而且程序員要自己爲自己的行爲負責。

class Test:

    def __init__(self, foo):
        self.__foo = foo

    def __bar(self):
        print(self.__foo)
        print('__bar')


def main():
    test = Test('hello')
    test._Test__bar()
    print(test._Test__foo)


if __name__ == "__main__":
    main()

hello
__bar
hello

面向對象的支柱

面向對象有三大支柱:封裝、繼承和多態
後面兩個概念在下面中進行詳細的說明,這裏我們先說一下什麼是封裝。我自己對封裝的理解是**“隱藏一切可以隱藏的實現細節,只向外界暴露(提供)簡單的編程接口”。**我們在類中定義的方法其實就是把數據和對數據的操作封裝起來了,在我們創建了對象之後,只需要給對象發送一個消息(調用方法)就可以執行方法中的代碼,也就是說我們只需要知道方法的名字和傳入的參數(方法的外部視圖),而不需要知道方法內部的實現細節(方法的內部視圖)。

@property裝飾器

之前我們討論過Python中屬性和方法訪問權限的問題,雖然我們不建議將屬性設置爲私有的,但是如果直接將屬性暴露給外界也是有問題的,比如我們沒有辦法檢查賦給屬性的值是否有效。我們之前的建議是將屬性命名以單下劃線開頭,通過這種方式來暗示屬性是受保護的,不建議外界直接訪問,那麼如果想訪問屬性可以通過屬性的getter(訪問器)和setter(修改器)方法進行對應的操作。如果要做到這點,就可以考慮使用@property包裝器來包裝getter和setter方法,使得對屬性的訪問既安全又方便,代碼如下所示。

class Person(object):

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

    # 訪問器 - getter方法
    @property
    def name(self):
        return self._name

    # 訪問器 - getter方法
    @property
    def age(self):
        return self._age

    # 修改器 - setter方法
    @age.setter
    def age(self, age):
        self._age = age

    def play(self):
        if self._age <= 16:
            print('%s正在玩飛行棋.' % self._name)
        else:
            print('%s正在玩鬥地主.' % self._name)


def main():
    person = Person('王大錘', 12)
    person.play()
    person.age = 22
    person.play()
   # person.name = '白元芳'  #由於沒有給name設置修改器,所以name不會修改,報錯AttributeError: can't set attribute 


if __name__ == '__main__':
    main()

_slots__魔法

我們講到這裏,不知道大家是否已經意識到,Python是一門動態語言。通常,動態語言允許我們在程序運行時給對象綁定新的屬性或方法,當然也可以對已經綁定的屬性和方法進行解綁定。但是如果我們需要限定自定義類型的對象只能綁定某些屬性,可以通過在類中定義__slots__變量來進行限定。需要注意的是_slots__的限定只對當前類的對象生效,對子類並不起任何作用

class Person(object):

    # 限定Person對象只能綁定_name, _age和_gender屬性,綁定了_gender屬性,雖然沒有具體函數和含義,但也不報錯
    #有寫函數的屬性,但上面__slots__沒有標明,也會報錯
    __slots__ = ('_name', '_age', '_gender')

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

    @property
    def name(self):
        return self._name

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, age):
        self._age = age

    def play(self):
        if self._age <= 16:
            print('%s正在玩飛行棋.' % self._name)
        else:
            print('%s正在玩鬥地主.' % self._name)
	def gay():
		if 

def main():
    person = Person('王大錘', 22)
    person.play()
    person._gender = '男'
    # AttributeError: 'Person' object has no attribute '_is_gay'
    person._is_gay = True#在開始沒有定義
if __name__ == '__main__':
    main()


王大錘正在玩鬥地主.

 AttributeError: 'Person' object has no attribute '_is_gay' 

靜態方法:@staticmethod

之前,我們在類中定義的方法都是對象方法,也就是說這些方法都是發送給對象的消息。實際上,我們寫在類中的方法並不需要都是對象方法,例如我們定義一個“三角形”類,通過傳入三條邊長來構造三角形,並提供計算周長和麪積的方法,但是傳入的三條邊長未必能構造出三角形對象,因此我們可以先寫一個方法來驗證三條邊長是否可以構成三角形,這個方法很顯然就不是對象方法,因爲在調用這個方法時三角形對象尚未創建出來(因爲都不知道三條邊能不能構成三角形),所以這個方法是屬於三角形類而並不屬於三角形對象的。我們可以使用靜態方法來解決這類問題,代碼如下所示。

from math import sqrt


class Triangle(object):

    def __init__(self, a, b, c):
        self._a = a
        self._b = b
        self._c = c

    @staticmethod
    def is_valid(a, b, c):
        return a + b > c and b + c > a and a + c > b

    def perimeter(self):
        return self._a + self._b + self._c

    def area(self):
        half = self.perimeter() / 2
        return sqrt(half * (half - self._a) *
                    (half - self._b) * (half - self._c))


def main():
    a, b, c = 3, 4, 5
    # 靜態方法和類方法都是通過給類發消息來調用的
    if Triangle.is_valid(a, b, c):
        t = Triangle(a, b, c)
        print(t.perimeter())
        # 也可以通過給類發消息來調用對象方法但是要傳入接收消息的對象作爲參數
        # print(Triangle.perimeter(t))
        print(t.area())
        # print(Triangle.area(t))
    else:
        print('無法構成三角形.')


if __name__ == '__main__':
    main()

12
6.0

類方法

和靜態方法比較類似,Python還可以在類中定義類方法,類方法的第一個參數約定名爲cls,它代表的是當前類相關的信息的對象(類本身也是一個對象,有的地方也稱之爲類的元數據對象),通過這個參數我們可以獲取和類相關的信息並且可以創建出類的對象,代碼如下所示。

from time import time, localtime, sleep


class Clock(object):
    """數字時鐘"""

    def __init__(self, hour=0, minute=0, second=0):
        self._hour = hour
        self._minute = minute
        self._second = second

    @classmethod
    def now(cls):
        ctime = localtime(time())
        return cls(ctime.tm_hour, ctime.tm_min, ctime.tm_sec)

    def run(self):
        """走字"""
        self._second += 1
        if self._second == 60:
            self._second = 0
            self._minute += 1
            if self._minute == 60:
                self._minute = 0
                self._hour += 1
                if self._hour == 24:
                    self._hour = 0

    def show(self):
        """顯示時間"""
        return '%02d:%02d:%02d' % \
               (self._hour, self._minute, self._second)


def main():
    # 通過類方法創建對象並獲取系統時間
    clock = Clock.now()
    while True:
        print(clock.show())
        sleep(1)
        clock.run()


if __name__ == '__main__':
    main()

類之間的關係
簡單的說,類和類之間的關係有三種:is-a、has-a和use-a關係。

  • is-a關係也叫繼承或泛化,比如學生和人的關係、手機和電子產品的關係都屬於繼承關係。
  • has-a關係通常稱之爲關聯,比如部門和員工的關係,汽車和引擎的關係都屬於關聯關係;關聯關係如果是整體和部分的關聯,那麼我們稱之爲聚合關係;如果整體進一步負責了部分的生命週期(整體和部分是不可分割的,同時同在也同時消亡),那麼這種就是最強的關聯關係,我們稱之爲合成關係。
  • use-a關係通常稱之爲依賴,比如司機有一個駕駛的行爲(方法),其中(的參數)使用到了汽車,那麼司機和汽車的關係就是依賴關係。
  • 在這裏插入圖片描述
    利用類之間的這些關係,我們可以在已有類的基礎上來完成某些操作,也可以在已有類的基礎上創建新的類,這些都是實現代碼複用的重要手段。複用現有的代碼不僅可以減少開發的工作量,也有利於代碼的管理和維護,這是我們在日常工作中都會使用到的技術手段。

繼承和多態

剛纔我們提到了,可以在已有類的基礎上創建新類,這其中的一種做法就是讓一個類從另一個類那裏將屬性和方法直接繼承下來,從而減少重複代碼的編寫。提供繼承信息的我們稱之爲父類,也叫超類或基類;得到繼承信息的我們稱之爲子類,也叫派生類或衍生類。子類除了繼承父類提供的屬性和方法,還可以定義自己特有的屬性和方法,所以子類比父類擁有的更多的能力,在實際開發中,我們經常會用子類對象去替換掉一個父類對象,這是面向對象編程中一個常見的行爲,對應的原則稱之爲里氏替換原則。下面我們先看一個繼承的例子。

class Person(object):
    """人"""

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

    @property
    def name(self):
        return self._name

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, age):
        self._age = age

    def play(self):
        print('%s正在愉快的玩耍.' % self._name)

    def watch_av(self):
        if self._age >= 18:
            print('%s正在觀看愛情動作片.' % self._name)
        else:
            print('%s只能觀看《熊出沒》.' % self._name)


class Student(Person):
    """學生"""

    def __init__(self, name, age, grade):
        super().__init__(name, age)
        self._grade = grade

    @property
    def grade(self):
        return self._grade

    @grade.setter
    def grade(self, grade):
        self._grade = grade

    def study(self, course):
        print('%s的%s正在學習%s.' % (self._grade, self._name, course))


class Teacher(Person):
    """老師"""

    def __init__(self, name, age, title):
        super().__init__(name, age)
        self._title = title

    @property
    def title(self):
        return self._title

    @title.setter
    def title(self, title):
        self._title = title

    def teach(self, course):
        print('%s%s正在講%s.' % (self._name, self._title, course))


def main():
    stu = Student('王大錘', 15, '初三')
    stu.study('數學')
    stu.watch_av()
    t = Teacher('駱昊', 38, '磚家')
    t.teach('Python程序設計')
    t.watch_av()


if __name__ == '__main__':
    main()
    
初三的王大錘正在學習數學.
王大錘只能觀看《熊出沒》.
駱昊磚家正在講Python程序設計.
駱昊正在觀看愛情動作片.

子類在繼承了父類的方法後,可以對父類已有的方法給出新的實現版本,這個動作稱之爲方法重寫(override)。通過方法重寫我們可以讓父類的同一個行爲在子類中擁有不同的實現版本,當我們調用這個經過子類重寫的方法時,不同的子類對象會表現出不同的行爲,這個就是多態(poly-morphism)。

from abc import ABCMeta, abstractmethod


class Pet(object, metaclass=ABCMeta):
    """寵物"""

    def __init__(self, nickname):
        self._nickname = nickname

    @abstractmethod
    def make_voice(self):
        """發出聲音"""
        pass


class Dog(Pet):
    """狗"""

    def make_voice(self):
        print('%s: 汪汪汪...' % self._nickname)


class Cat(Pet):
    """貓"""

    def make_voice(self):
        print('%s: 喵...喵...' % self._nickname)


def main():
    pets = [Dog('旺財'), Cat('凱蒂'), Dog('大黃')]
    for pet in pets:
        pet.make_voice()


if __name__ == '__main__':
    main()

旺財: 汪汪汪...
凱蒂:......
大黃: 汪汪汪...

在上面的代碼中,我們將Pet類處理成了一個抽象類,所謂抽象類就是不能夠創建對象的類,這種類的存在就是專門爲了讓其他類去繼承它。Python從語法層面並沒有像Java或C#那樣提供對抽象類的支持,但是我們可以通過abc模塊的ABCMeta元類和abstractmethod包裝器來達到抽象類的效果,如果一個類中存在抽象方法那麼這個類就不能夠實例化(創建對象)。上面的代碼中,Dog和Cat兩個子類分別對Pet類中的make_voice抽象方法進行了重寫並給出了不同的實現版本,當我們在main函數中調用該方法時,這個方法就表現出了多態行爲(同樣的方法做了不同的事情)。

感謝大佬的整理,已star

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章