27. 企業級開發基礎8:物件導向擴充套件

大牧莫邪發表於2017-05-23

前面的章節中,我們已經學習過物件導向的基本操作、物件導向的三大特徵的詳細操作,對於物件導向有了一個初步的瞭解和認知。 本節內容會針對物件導向的程式設計進行一部分的擴充套件和補充,方便我們在專案開發過程中的操作能更加的全面和完善。

0. 本節內容

0.1 型別屬性和物件成員屬性
0.2 物件屬性的外部宣告和限制
0.3 多繼承機制下的注意的問題
0.4 類的定製屬性~魔法方法
0.5 特殊的型別:列舉

1. 型別屬性和物件的成員屬性

在之前的章節中,我們就類和物件已經學習過了如下內容 * 型別的定義 * 型別中屬性的定義 * 型別中方法的定義 * 屬性和方法的私有化操作

當型別在處理的過程中,我們知道在init()函式中可以初始化類的成員屬性/變數,在建立物件的過程中,每個物件的成員屬性都是互相獨立且互不影響的;物件A是不能直接使用物件B的成員屬性的值的,而是要通過物件B呼叫獲取物件B的屬性; python的型別中,還提供了一種方式,可以直接定義類的屬性,這樣定義的屬性是當前型別建立的所有物件所共享的,也可以直接通過類名稱呼叫,這樣的屬性稱為:類屬性

類屬性:是定義在型別中的公開的屬性,可以讓通過當前型別直接操作,可以是當前型別建立的所有物件共享的資料 ```

# 建立一個Person型別
class Person(object):
    # 定義一個類屬性:線上人數
    onlineCount = 100
    # 型別初始化的方法
    def __init__(self, name):
        self.__name = name
    # 屬性訪問方法
    def get_name(self):
        return self.__name
    def set_name(self, name):
        self.__name = name
# 建立Person型別的物件
p1 = Person("tom")
p2 = Person("jerry")

# 訪問類屬性
print(Person.onlineCount)    # 執行結果 100
print(p1.onlineCount)          # 執行結果 100
print(p2.onlineCount)          # 執行結果 100

# 通過類名稱修改類屬性
Person.onlineCount = 15
# 再次訪問類屬性
print(Person.onlineCount)    # 執行結果 15
print(p1.onlineCount)          # 執行結果 15
print(p2.onlineCount)          # 執行結果 15

# 切記不能通過物件修改類屬性:下面的做法只是給p1物件新增了一個額外的成員屬性onlineCount
p1.onlineCount = 200
# 再次訪問類屬性
print(Person.onlineCount)    # 執行結果 15
print(p1.onlineCount)          # 執行結果 200
print(p2.onlineCount)          # 執行結果 15

```

類和物件,注意: 類中可能會出現三種屬性/變數 * 類屬性:直接定義在類的內部,初始化函式的外部的屬性,可以直接通過類名稱訪問或者修改,通過當前類建立的物件都可以共享/訪問類的類屬性 * 成員屬性:定義在初始化函式__init__(self)中,每個通過當前類建立的物件都有自己獨立的成員屬性的資料,並且物件和物件之間的資料不會有任何影響 * 區域性變數:在類的方法中的引數、方法中定義的變數都是區域性變數,區域性變數一旦方法執行完畢就會被回收

2. 物件屬性的外部宣告和限制

上面的程式碼中,我們使用p1.onlineCount=15發現沒有修改類屬性,而是給p1增加了一個成員屬性,這是怎麼回事呢?

觀察下面的程式碼: ```

# 建立了一個空型別
class Person:
    pass

# 建立Person的物件
p = Person()
# 給物件p追加成員屬性
p.name = "tom"
p.age = 19
p.gender = "男"
# 列印屬性資料
print(p.name, p.age, p.gender)
# 執行結果:tom 19 男

``` 在上述程式碼中,我們定義了一個空型別Person,在建立了Person的物件之後,可以在物件的引用變數上,給物件新增額外的成員屬性【切記,這裡新增的額外的成員屬性僅限於當前的這個物件,其他物件上不會出現】

這樣的操作方式,可以在一定程度上讓程式碼的操作更加靈活,但是同時也降低了程式碼的可讀性,試想一下~我們辛辛苦苦抽象定義好了型別Person,Person中已經出現了我們所有人知道的屬性,結果在操作的過程中,朝陽群眾A建立的Person物件多出來了2個其他人不知道的屬性,朝陽群眾B建立的Person物件又多出來了其他人不知道的3個屬性,這是一件非常恐怖的事情,會讓整個型別和物件的操作變得非常的混亂。 ```

# 原始的Person型別,只有一個name屬性
class Person:
    def __init__(self, name):
        self.__name = name
    def get_name(self):
        return self.__name
    def set_name(self, name):
        self.__name = name

# 朝陽群眾A建立的物件
p = Person("老A")
p.age = 20
p.sex = "男"
# 朝陽群眾B建立的物件
p = Person ("老B")
p.gender = "女"
p.address = "朝陽"
p.phone = "13838383838"

``` 觀察上述程式碼,兩個人建立的物件,一團混亂,光是一個性別兩個開發人員定義的擴充套件出來的成員變數都不一致,後續其他人在操作的時候都不知道應該呼叫什麼屬性來處理了。

python為了處理這樣的問題,提供了一個特殊的類屬性__slots__ ,該屬性的值是一個元組,元組中定義了類中可以出現的所有成員屬性的名稱 ```

# 建立一個Person型別
class Person:
    # 通過__slots__屬性定義可以擴充套件的成員屬性名稱
    __slots__ = ("__name", "address", "age", "gender", "email")
    # 初始化方法
    def __init__(self, name):
        self.__name = name
    # 屬性的set/get方法
    def get_name(self):
        return self.__name
    def set_name(self, name):
        self.__name = name
# 建立物件
p = Person("tom")
# 擴充套件屬性
p.address = "朝陽"
p.age = 20
p.sex = "男"
# 執行結果
~ AttributeError: 'Person' object has no attribute 'sex'

``` 通過上述程式碼就可以看到,python提供了一個__slots__類屬性,屬性的值是一個元組,元組中規範了可能出現在類的成員屬性列表。

類在建立好物件之後,可以在物件上直接掛在屬性,這在一定程度上對於程式處理的靈活度有所提升,但是同樣的,過於靈活的程式碼都會極大的降低程式碼的可讀性,所以python提供了__slots__這樣的類屬性進行規範,規範類屬性中只能出現的成員屬性的列表,防止惡意的擴充套件。

3. 多繼承機制下的注意的問題

多繼承機制,在操作的過程中,同樣也是提高了程式碼的處理靈活性,很大程度的擴充套件了程式碼的功能

在使用多繼承機制進行程式設計開發的過程中一定要注意一個問題:當前類繼承了一個或者多個父類,當前類就同時繼承了父類中的公開的屬性和函式,如果不同的父類中出現相同的屬性/函式,就需要明確執行的過程 ```

# 定義一個型別Son
class Son(object):
    def fealty(self):
        print("孝順父母")
# 常見一個型別Student
class Student(object):
    def fealty(self):
        print("尊師重道")

# 建立一個Person型別,繼承自Son和Student
class Person(Son, Student):
    pass

# 建立物件,執行方法
p = Person()
p.fealty()
# 問題:這裡的fealty()函式,會不會報錯?如果不報錯,怎麼執行?
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# 我們可以看到,在繼承的兩個父類中都出現了fealty()函式
# 這裡執行時,按照型別定義時繼承的順序進行查詢
#  查詢到對應的函式立刻執行並且不再向後查詢
# 上述案例中,繼承順序是(Son, Student)
# 首先在Son型別中查詢是否有fealty()方法,查詢到立刻執行。
# 執行結果:孝順父母
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

```

上述案例中,我們看到一旦出現多重繼承,就會出現這樣繼承的多個父類中出現了多個相同名稱的變數或者方法的情況,使用的這些變數和方法的時候一定要注意一個原則,先繼承誰就使用誰的變數或者方法!

4. 類的定製屬性~魔法方法

上面的程式碼中,我們已經看到了,類似__slots__這樣的變數在前後加了雙下劃線的,在python中會有特殊的含義,這裡會繼續介紹一些常見的在物件導向開發過程中出現的一些這樣的魔法方法

4.1. 物件格式化列印輸出【__str__()】

常規情況下,物件直接輸出,會輸出物件的描述資訊,晦澀難懂 ```

# 定義型別
class Person(object):
    def __init__(self, name):
        self.__name = name
# 建立物件
p = Person("jerry")
# 輸出物件
print(p) 
# 執行結果:<__main__.Person object at 0x0000028727259550>
```

對當前型別進行如下改造 ```

# 定義型別
class Person(object):
    def __init__(self, name):
        self.__name = name
    def __str__(self):
        return "i am a person, my name is " + self.__name
# 建立物件
p = Person("jerry")
# 輸出物件
print(p) 
# 執行結果:i am a person, my name is jerry
p
# 執行結果:<__main__.Person object at 0x0000028727259550>

``` 我們突然發現,直接列印物件,輸出的結果竟然是我們在__str__()方法中定義的字串。其實我們在使用使用物件的時候,就會預設呼叫物件的__str__()方法獲取物件的字串描述資訊,這個__str__()方法是從object物件繼承而來的,我們這裡只是對它進行了方法重寫。

另外,在命令列操作過程中,如果不用print()方法列印而是直接輸入物件,會發現執行的結果又是讓人晦澀難懂的東西了,在命令列直接使用物件呼叫的不是物件的__str__()方法,而是__repr__()方法,只需要簡單的修改即可 ```

# 定義型別
class Person(object):
    def __init__(self, name):
        self.__name = name
    def __str__(self):
        return "i am a person, my name is " + self.__name
    # 將方法__str__賦值給__repr__
    __repr__ = __str__
# 建立物件
p = Person("jerry")
# 輸出物件
print(p) 
# 執行結果:i am a person, my name is jerry
p
# 執行結果:i am a person, my name is jerry

```

4.2. 玩轉自己~物件的應用直接呼叫【__call__()】

當我們建立好物件之後,可以將物件的引用變數當成方法執行會出現什麼樣的情況呢 ```

# 定義型別
class Person(object):
    def __init__(self, name):
        self.__name = name
# 建立物件
p = Person("jerry")
# 直接執行
p()
# 執行結果:TypeError: 'Person' object is not callable

``` 肯定是不能這麼幹的~,所以出現錯誤:Person物件不是一個可執行的東東

但是可以進行如下的改造 ```

# 定義型別
class Person(object):
    def __init__(self, name):
        self.__name = name
    def __call__(self):
        print("一種快捷執行物件中某些初始化操作的特殊方法__call__")
# 建立物件
p = Person("jerry")
# 直接執行
p()
# 執行結果:一種快捷執行物件中某些初始化操作的特殊方法__call__

``` 此時又發現,這樣直接將引用變數當成方法執行又變的可行了。 __call__()方法,主要用於物件快捷執行而存在的一個魔術方法,方便進行物件中某些重要資料的初始化整理工作等。

在python中,還有一系列的魔法方法,可以讓一個類具有各種特殊的處理功能,如__iter__()方法,讓一個類建立的物件可以像列表那樣進行資料的迭代;__getitem__()函式可以在迭代的基礎上進行索引取值等操作,

5. 特殊的型別:列舉

某些情況下,在我們專案開發過程中,會針對一些不會改變的資料進行標記,~常見的做法就是通過定義常量的情況進行處理,如:在一個員工管理系統中,針對一年十二個月發放工資,這裡的十二個月需要進行標記~每個月的天數、績效這些都不一定一致,可以按照下面的方式進行處理: ```

# 通過列表中定義一堆的變數來表示12個月份
month = ["JAN", "FAB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]
# 但是列表表示的方式,列表中的資料並不是非常的安全,有可能在操作的過程中被修改

# 通過元組中定義一堆的變數來表示12個月份
month = ("JAN", "FAB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC")
# 這樣就比第一種方案簡單多了,也方便後續的各種操作

# 通過型別中定義一堆的變數來表示12個月份
class Month(object): 
    "JAN" = 1
     "FAB" = 2
    "MAR" = 3
    "APR" = 4 
    "MAY" = 5 
    "JUN" = 6 
    "JUL" = 7 
    "AUG" = 8 
    "SEP" = 9 
    "OCT" = 10 
    "NOV" = 11
    "DEC" = 12
# 這樣更加正式一些,不過寫起來確實挺麻煩,後續的操作也不怎麼友好

```

5.1. 使用列舉

上述程式碼中,我們通過三種方式進行了列舉的定義和處理,但是每一種方式都多多少少存在一些遺憾,python中提供了一種特殊的型別:列舉,來處理這樣定義常量的問題:

列舉的語法結構:是不是和上面我們使用元組的方式特別相像呢?! ```

from enum import Enum
# Month = Enum("列舉名稱", (元組中的列舉值))
M = Enum("Month",  ("JAN", "FAB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"))
# 使用列舉
print(M.JAN)   #執行結果:Month.JAN
print(M.JAN.value) # 執行結果:1

通過將我們原始的條件判斷,加上列舉操作,可以簡化程式碼的同時提高程式碼的可讀性 參考如下程式碼,明顯第二種程式碼的可讀性更高,更加方便我們的專案維護操作

if month == 1:
    print("1月份發放工資")
-------------------------------------------
if month = Month.JAN:
     print("1月份發放工資")

```

5.2. 自定義列舉

Python提供的列舉已經完全足夠適用於我們專案中使用的各種場景了 如果列舉的細節處理程度還是不滿足您的專案,可以通過python提供的方式進行自定義列舉的定義 ```

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
#  自定義列舉語法結構
# from enum import Enum, unique
#
# @unique
# class EnumName(Enum):
#     列舉元素
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# 建立一個自定義列舉,用於定義一週中星期的每一天,方便做日誌記錄
from enum import Enum, unique

@unique
class Weekday(Enum):
    MON = 1
    TUE = 2
    WED = 3
    THU = 4
    FRI = 5
    SAT = 6
    SUN = 7
# 使用列舉,和常規的使用方式一致
if today == Weekday.SAT:
    print("提醒:今天是傳送週報的日子,不要忘記哦")

```

列舉,是為了方便在專案中定義有字面意義的常量,提高程式碼的可讀性而出現的一種特殊的型別,底層封裝的其實就是給列舉的名稱賦值了整數資料,所以我們可以在程式中使用整數常量作為條件處理判斷的地方,使用列舉能提高程式碼的可讀性和維護性。

相關文章