20. 物件導向之封裝

hbutmeng發表於2024-08-15

1. 物件導向的三大特性

物件導向程式設計有三大特性:封裝、繼承、多型

2. 理論

2.1 概念

封裝指的是把資料與功能都整合到一起,之前所說的”整合“二字其實就是封裝的通俗說法。
除此之外,針對封裝到物件或者類中的屬性,可以嚴格控制對它們的訪問,分兩步實現:隱藏與開放介面

2.2 封裝的目的

將某些屬性和方法隱藏起來,在程式外部看不到,其它程式無法呼叫

保護資料,可以防止外部程式碼隨意修改物件內部的資料

3. 如何進行封裝---隱藏屬性

3.1 隱藏屬性方法

Python的Class機制採用雙下劃線開頭的方式將屬性隱藏起來(設定成私有的)

但其實這僅僅只是一種變形操作,類中所有雙下滑線開頭的屬性都會在類定義階段、檢測語法時自動變成_類名__屬性名的形式

3.2 隱藏屬性訪問

在類外部無法直接訪問雙下滑線開頭的屬性,但知道了類名和屬性名就可以拼出名字:__類名__屬性,然後就可以訪問了,
如Person._Person__NAME,所以說這種操作並沒有嚴格意義上地限制外部訪問,僅僅只是一種語法意義上的變形

class Student:
    _ _school = 'MIT'  # 在需要被封裝資料屬性的位置,加_ _

    def _ _init_ _(self, name, age, grade):
        self.name = name
        self.age = age
        self.grade = grade

    def show_info(self):
        print(f'name is {self.name}, age is {self.age}, grade is {self.grade},'
              f'school is {self.__school}')

    def _ _lab(self):  # 在需要被封裝函式屬性的位置,加__
        print(f'{self.__school}的{self.name}正在做實驗')


print(Student._ _dict_ _)  # '_Student_ _school': 'MIT'    類的屬性字典變成變成 _類名__屬性名的形式

stu1 = Student('neymar', 32, 2)
stu1.show_info()  # name is neymar, age is 32, grade is 2,school is MIT
print(stu1._ _dict_ _)  # {'name': 'neymar', 'age': 32, 'grade': 2}

stu1.school = 'California'  # 隱藏school資料屬性後物件無法直接修改該屬性
print(stu1._ _dict_ _)  # {'name': 'neymar', 'age': 32, 'grade': 2, 'school': 'California'}
print(Student._ _dict_ _)  # '_Student__school': 'MIT'

# 透過  類名._類名__屬性名修改屬性
Student._Student_ _school = 'Harvard'
print(Student._ _dict_ _)  # '_Student__school': 'Harvard'
print(stu1._Student_ _school)  # Harvard
stu1._Student_ _lab()  # Harvard的neymar正在做實驗

3.3 隱藏方法變形

在類內部是可以直接訪問雙下劃線開頭的屬性的,因為在類定義階段類內部雙下劃線開頭的屬性統一發生了變形。

class Student:
    __school = 'MIT'  # 本質上變形成了_Student__school

    def __init__(self, name, age, grade):
        self.__name = name  # 本質上變形成了self._Student__name
        self.age = age
        self.grade = grade

    def __lab(self):  # 本質上變形成了_Student__lab
        print(f'{self.__school}的{self.__name}正在做實驗')

    def run(self):
        self.__lab()  # 本質上變形成了self._Student__lab())
        print(self.__school)  # 本質上變形成了self._Student__school

3.4 變形操作只會發生一次

變形操作只在類定義階段發生一次,在類定義之後的賦值操作,不會變形

class Student:
    _ _school = 'MIT'  # 本質上變形成了_Student_ _school

    def _ _init_ _(self, name, age, grade):
        self._ _name = name  # 本質上變形成了self._Student_ _name
        self.age = age
        self.grade = grade

    def _ _lab(self):  # 本質上變形成了_Student__lab
        print(f'{self._ _school}的{self._ _name}正在做實驗')

    def run(self):
        self._ _lab()  # 本質上變形成了self._Student_ _lab())
        print(self._ _school)  # 本質上變形成了self._Student_ _school

Student._ _school = 'singapore'
print(Student._ _dict_ _)  # '_Student_ _school': 'MIT'  資料屬性並沒有發生修改

3.5 總結

(1)在類內部 將變數名 用雙下劃線 宣告
在類初始化的時候就會由原來的變數名 __變數名 變換 變換成 _類名_ _變數名
(2)在類內部封裝起來的屬性
內部呼叫 self 去呼叫的時候直接 self.__變數名即可呼叫
如果是物件呼叫 隱藏起來的屬性 那就需要 物件._類名__變數名 去呼叫
(3)在類內部封裝屬性後變形只會發生一次
當使用物件或者類.__變數名去修改屬性的時候 修改的是 新的屬性而不是原來的屬性

4. 封裝之後如何訪問---開放介面

4.1 原理

將資料和功能隱藏起來就限制了類外部對資料和功能的直接操作,類內部應該提供相應的介面來允許類外部間接地運算元據和功能,介面之上可以增加額外的邏輯對資料和函式的操作進行嚴格限制

4.2 隱藏資料屬性

class Student:

    def __init__(self, name, age, grade):
        self.__name = name
        self.__age = age
        self.__grade = grade

    def show_info(self):
        print(f'{self.__name}的年齡是{self.__age}年級是{self.__grade}')

    def set_info(self, name1, grade1):
        if not name1.startswith('mit_'):
            raise ValueError('名字必須以mit_開頭')
        if not grade1.isdigit():
            raise ValueError('年級必須是數字')
        self.__name = name1  # 符合以上兩個條件才允許修改屬性
        self.__grade = grade1


stu1 = Student(name='alan', age=30, grade=1)
stu1.show_info()  # alan的年齡是30年級是1

# 物件無法直接修改類中資料屬性
stu1.name = 'messi'
stu1.show_info()  # alan的年齡是30年級是1

# 想要修改類中資料屬性,必須透過介面進行修改
stu1.set_info(name1='messi', grade1='2')  # ValueError: 名字必須以mit_開頭
stu1.set_info(name1='mit_messi', grade1=2)
# AttributeError: 'int' object has no attribute 'isdigit'  int型別沒有isdigit方法

stu1.set_info(name1='mit_messi', grade1='2')  # 修改成功
stu1.show_info()  # mit_messi的年齡是30年級是2

4.3 隱藏函式屬性

# ATM程式的取款功能,由許多其它功能組成:插卡、輸入密碼、輸入取款金額、取款、列印回執等
# 而對於使用者來說,只需要withdraw這個功能介面即可,其餘功能都可以隱藏起來
class ATM:
    def __inert_card(self):
        print('插卡')

    def __input_pwd(self):
        print('輸入密碼')

    def __input_money(self):
        print('輸入取款金額')

    def __take_money(self):
        print('取款')

    def __print_bill(self):
        print('列印回執')

    def withdraw(self):
        self.__inert_card()
        self.__input_pwd()
        self.__input_money()
        self.__take_money()
        self.__print_bill()

obj = ATM()
obj.withdraw()

5. property屬性

5.1 引入

計算bmi

class Person:
    def __init__(self, name, height, weight):
        self.__name = name
        self.__height = height
        self.__weight = weight

    def bmi(self):
        info = f'{self.__name}的BMI數值是:{self.__weight / (self.__height ** 2)}'
        return info

person1 = Person(name='haaland', height=1.95, weight=75)
print(person1.bmi())  # haaland的BMI數值是:19.723865877712033
# bmi計算得到的結果是一個數字,應該和姓名、身高、體重一樣是一個資料屬性,而不是函式屬性
# 給計算bmi的函式加一個property屬性,將函式屬性偽裝成資料屬性
class Person:
    def __init__(self, name, height, weight):
        self.__name = name
        self.__height = height
        self.__weight = weight

    @property
    def bmi(self):
        info = f'{self.__name}的BMI數值是:{self.__weight / (self.__height ** 2)}'
        return info


person1 = Person(name='haaland', height=1.95, weight=75)
# 函式屬性變成了資料屬性
print(person1.bmi)  # haaland的BMI數值是:19.723865877712033

5.2 為什麼要用property

將一個類的函式定義成特性以後,物件再去使用的時候obj.name,根本無法察覺自己的name是執行了一個函式然後計算出來的,這種特性的使用方式遵循了統一訪問的原則

物件導向的封裝有三種方式:

public     這種就是不封裝,對外公開

protected   這種封裝方式對外不公開,但對子類公開

private    這種方式對任何都不公開

python並沒有在語法上將三種封裝方式內建到class機制中,透過property方法可以實現

5.3 setter與deleter裝飾器

setter與deleter裝飾器一般與property結合使用

class Student:
    def __init__(self, name, age, grade):
        self.__name = name
        self.__age = age
        self.__grade = grade

    @property  # 將函式屬性包裝成資料屬性
    def name_tool(self):
        return f'MIT_{self.__name}'

    @name_tool.setter  # 給以上包裝後的資料屬性設定值
    def name_tool(self, value):
        if not len(value) <= 3:
            raise ValueError('名字必須是三個字及以下')
        self.__name = value

    @name_tool.deleter  # 給以上包裝後的資料屬性刪除值
    def name_tool(self):
        del self.__name


stu1 = Student(name='lavigne', age=19, grade=2)
# 獲取完整名字    觸發property,此時name_tool函式中的self都是物件stu1本身
print(stu1.name_tool)  # MIT_lavigne
print(stu1.__dict__)  # {'_Student__name': 'lavigne', '_Student__age': 19, '_Student__grade': 2}

# 修改名字      觸發setter,所有self都是物件stu1本身
stu1.name_tool = 'lav'
print(stu1.__dict__)  # {'_Student__name': 'lav', '_Student__age': 19, '_Student__grade': 2}

# 刪除名字      觸發deleter,所有self都是物件stu1本身
del stu1.name_tool
print(stu1.name_tool)  # AttributeError: 'Student' object has no attribute '_Student__name'. Did you mean: '_Student__age'?
print(stu1.__dict__)  # {'_Student__age': 19, '_Student__grade': 2}

5.4 使用property內建函式對外提供統一介面

class Student:
    def __init__(self, name, age, grade):
        self.__name = name
        self.__age = age
        self.__grade = grade

    def __full_name(self):
        return f'MIT_{self.__name}'

    def __set_name(self, value):
        if not len(value) <= 3:
            raise ValueError('名字必須是三個字及以下')
        print(self.__dict__)
        self.__name = value
        print(self.__dict__)

    def __del_name(self):
        print(self.__dict__)
        del self.__name
        print(self.__dict__)

    # 不用裝飾器,使用property內建函式,對外提供統一介面
    name_tool = property(fget=__full_name, fset=__set_name, fdel=__del_name)


stu1 = Student(name='lavigne', age=20, grade=3)

# 獲取完整名字    只需要呼叫介面即可,觸發property第一個引數函式
print(stu1.name_tool)  # MIT_lavigne

# 修改名字      只需要呼叫介面即可,觸發property第二個引數函式
stu1.name_tool = 'lav'
# {'_Student__name': 'lavigne', '_Student__age': 20, '_Student__grade': 3}
# {'_Student__name': 'lav', '_Student__age': 20, '_Student__grade': 3}

# 刪除名字      只需要呼叫介面即可,觸發property第三個引數函式
del stu1.name_tool
# {'_Student__name': 'lav', '_Student__age': 20, '_Student__grade': 3}
# {'_Student__age': 20, '_Student__grade': 3}

相關文章