1. 程式人生 > 實用技巧 >19.面向物件:封裝、繫結方法與非繫結方法、繼承

19.面向物件:封裝、繫結方法與非繫結方法、繼承

  • 引子

  • 封裝

  • 隱藏屬性
  • 類內的裝飾器:特性(property)
  • 繫結方法與非繫結方法

  • 繼承

  • 先抽象再繼承

  • 繼承背景下的屬性查詢

  • 封裝

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

    在python中用雙下劃線開頭的方式將屬性隱藏起來(設定成私有的),但其實這僅僅只是一種變形操作
# 類中所有雙下劃線開頭的名稱如__x都會在類定義時自動變形成:_類名__屬性名的形式:
class People:
# 類的資料屬性就應該是共享的,但是語法上是可以把類的資料屬性設定成私有的 ↓
    __country = "China"        # 變形為 _People__country = "china"

    def __init__(self,name,age):
        self.__name = name  # self._People__name = name
        self.__age = age    # self._people__age = age

    def __func(self):  # _People__func
        print('xx')

    def tell_name(self):
        print(self.__name)  # self._People__name

print(People._People__country) # 只有在類內部才可以通過__開頭的形式訪問到.
print(People._People__func)

print(People.__dict__)
obj1 = People("egon",18)
print(obj1.__dict__)
# __開頭的屬性的特點:
# 1、並沒有真的藏起來,只是變形了
# 2、該變形只在類定義階段、掃描語法的時候執行,此後__開頭的屬性都不會變形
obj1.__gender = "male" 
print(obj1.__dict__)   
print(obj1.__gender )
# 3、該隱藏對外不對內
obj1.tell_name()


  • 封裝的真諦在於明確地區分內外,封裝的屬性可以直接在內部使用,而不能被外部直接使用,然而定義屬性的目的終歸是要用,外部要想用類隱藏的屬性,需要我們為其開闢介面,讓外部能夠間接地用到我們隱藏起來的屬性,那這麼做的意義何在???

    1:隱藏資料:將資料隱藏起來這不是目的。隱藏起來然後對外提供操作該資料的介面,然後我們可以在介面附加上對該資料操作的限制,以此完成對資料屬性操作的嚴格控制。
# 為何要隱藏屬性
# 1、隱藏資料屬性為了嚴格控制類外部訪問者對屬性的操作
class People:

    def __init__(self,name,age):
        self.__name = name  # self._People__name = name
        self.__age = age

    def tell_info(self):
        print("<%s:%s>" %(self.__name,self.__age))

    def set_info(self,name,age):
        if type(name) is not str:
            print("名字必須是字串")
            return
        if type(age) is not int:
            print("年齡必須是數字")
            return
        self.__name = name
        self.__age = age

obj1 = People("egon",18)  
obj1.tell_info()     # <egon:18>

obj1.set_info("tom",28)  
obj1.tell_info()     # <tom:28>

obj1.set_info(123123123123,28)  # 名字必須是字串
obj1.tell_info()     # <egon:18>
2:隱藏方法:目的是隔離複雜度
# 取款是功能,而這個功能有很多功能組成:插卡、密碼認證、輸入金額、列印賬單、取錢
# 對使用者來說,只需要知道取款這個功能即可,其餘功能我們都可以隱藏起來,很明顯這麼做
# 隔離了複雜度,同時也提升了安全性

# 隔離複雜度的例子
class ATM:
    def __card(self):
        print('插卡')
    def __auth(self):
        print('使用者認證')
    def __input(self):
        print('輸入取款金額')
    def __print_bill(self):
        print('列印賬單')
    def __take_money(self):
        print('取款')

    def withdraw(self):
        self.__card()
        self.__auth()
        self.__input()
        self.__print_bill()
        self.__take_money()

a=ATM()
a.withdraw()

真正的封裝是,經過深入的思考,做出良好的抽象,給出“完整且最小”的介面,並使得內部細節可以對外透明

(注意:對外透明的意思是外部呼叫者可以順利的得到自己想要的任何功能,完全意識不到內部細節的存在)

  • 類內的裝飾器:

  • 特性(property)

    什麼是特性property
    property是一種特殊的屬性,訪問它時會執行一段功能(函式)然後返回值

    例一:BMI指數(bmi是計算而來的,但很明顯它聽起來像是一個屬性而非方法,如果我們將其做成一個屬性,更便於理解)

    成人的BMI數值:

    過輕:低於18.5

    正常:18.5-23.9

    過重:24-27

    肥胖:28-32

    非常肥胖, 高於32

      體質指數(BMI)=體重(kg)÷身高^2(m)

      EX:70kg÷(1.75×1.75)=22.86

class People:
    def __init__(self,name,weight,height):
        self.name = name
        self.weight = weight
        self.height = height
    @property
    def bmi(self):
        return self.weight / (self.height ** 2)

p1 = People('egon',75,1.8)

# print(p1.bmi())  # 正常訪問p1.bmi()需要加括號
# 23.148148148148145

print(p1.bmi)    #  用@property將bmi功能偽裝成了資料屬性
# 23.148148148148145
property的使用:
class People:
    def __init__(self,name):
        self.__name = name
        
        
#    def get_name(self):
#        return self.__name 

    @property            # 1.檢視行為
    def name(self):      # 被property裝飾的name函式,其實是__name被藏起來了
        return self.__name

    @name.setter           # 
    def name(self,x):       # 2.修改行為
        if type(x) is not str:
            raise Exception("名字必須是字串型別")
        self.__name = x

    @name.deleter      # 3.刪除行為
    def name(self):
        print("不允許刪除")

p1 = People('egon')
# print(p1.get_name)  # 訪問

# 對應檢視name行為   
print(p1.name)   # p1.name通過@property訪問到的self.__name的屬性

# 對應修改name行為
p1.name = 123 # 丟擲異常
p1.name = "EGON"  # 正常修改
print(p1.name)

# 對應刪除name行為
del p1.name
print(p1.name)

# ps: 配合property可以先將一個屬性藏起來,__name這個屬性被藏起來了,藏起來之後定義三個函式都叫name
#      這三個函式裡面分別寫上name的1、2、3的三種行為,跟操作一一對應



# 瞭解性知識點:這種方式也可以達到上面操作的效果

class People:
    def __init__(self,name):
        self.__name = name

    def get_name(self):
        return self.__name

    def set_name(self,x):
        if type(x) is not str:
            raise Exception("名字必須是字串型別")
        self.__name = x

    def del_name(self):
        print("不允許刪除")

    name = property(get_name,set_name,del_name)

p1 = People('egon')


  • 繫結方法與非繫結方法

  • 一:繫結方法

    特點:繫結給誰就應該由誰來呼叫,誰來呼叫就會將自己當做第一個引數傳入

      1. 但凡在類中定義一個函式,預設就是繫結給物件的,應該由物件來呼叫,
        會將物件當作第一個引數自動傳入
      1. 如果想要將函式繫結給類的話就需要用到繫結到類的方法:用classmethod裝飾器裝飾的方法。
        類中定義的函式被classmethod裝飾過,就繫結給類,應該由類來呼叫,
        類來呼叫會類本身當作第一個引數自動傳入
  • 二:非繫結方法

    特點:不與類和物件繫結,意味著誰都可以來呼叫,但無論誰來呼叫就是一個普通

    函式,沒有自動傳參的效果

    • 類中定義的函式被staticmethod裝飾過,就成一個非繫結的方法即一個普通函式,誰都可以呼叫,
      但無論誰來呼叫就是一個普通函式,沒有自動傳參的效果
    注意:與繫結到物件方法區分開,在類中直接定義的函式,沒有被任何裝飾器裝飾的,都是繫結到物件的方法,可不是普通函式,物件呼叫該方法會自動傳值,而staticmethod裝飾的方法,不管誰來呼叫,都沒有自動傳值一說
class People:
    def __init__(self,name):
        self.name = name

    # 但凡在類中定義一個函式,預設就是繫結給物件的,應該由物件來呼叫,
    # 會將物件當作第一個引數自動傳入
    def tell(self):
        print(self.name)

    # 類中定義的函式被classmethod裝飾過,就繫結給類,應該由類來呼叫,
    # 類來呼叫會類本身當作第一個引數自動傳入
    @classmethod
    def f1(cls):  # cls = People
        print(cls)

    # 類中定義的函式被staticmethod裝飾過,就成一個非繫結的方法即一個普通函式,誰都可以呼叫,
    # 但無論誰來呼叫就是一個普通函式,沒有自動傳參的效果
    @staticmethod
    def f2(x,y):
        print(x,y)

p1 = People('egon')
p1.tell()

print(People.f1)
People.f1()

print(People.f2)
print(p1.f2)

People.f2(1,2)
p1.f2(3,4)



# 示例場景

'''
# settings.py 配置檔案模組

NAME = "xxx"
AGE = 103
GENDER = "male"

'''

import settings

class People:
    def __init__(self,name,age,gender):
      #  self.id = self.create_id()
        self.name = name
        self.age = age
        self.gender = gender

    def tell_info(self):  # 列印詳細資訊
        print('<%s:%s:%s>' %(self.name,self.age,self.gender))

    @classmethod         # 繫結給類的方法
    def from_conf(cls):  # cls 自動傳入類
        print(cls)
        return cls(settings.NAME, settings.AGE, settings.GENDER)

#    @staticmethod    # 非繫結方法 誰都可以來調但沒有自動傳參效果
#    def create_id():
#        import uuid           # 呼叫隨機產生id模組
#        return uuid.uuid1()  # uuid.uuid1 可以產生隨機編號



p1 = People("egon",18,"male")  # 例項化得到一個物件

p2 = People.from_conf()  # 通過配置檔案完成例項化 繫結給類的方法應該由類去調
print(p2.__dict__)


# 需求:每個使用者都要有一個自己的隨機id號
# print(p1.create_id())    
# print(People.create_id())

# print(p1.__dict__)  # 例項化後用戶就有自己的id了

  • 繼承

  • 什麼是繼承?
    繼承是建立新類的一種方式,新建的類稱之為子類。
    被繼承的類稱之為父類、基類、超類
    繼承的特點:子類可以遺傳父類的屬性
  • 類是用來解決物件之間冗餘問題的
  • 而繼承則是解決類與類之間冗餘問題的

注意:新建的類可以繼承一個或多個父類(python支援多繼承)

class Parent1(object):  # 定義父類
    pass

class Parent2(object):  # 定義父類 
    pass

class Sub1(Parent1):    # 定義子類,單繼承,繼承基類Parent1
    pass

class Sub2(Parent1,Parent2): # 定義子類,多繼承,既繼承基類parent1又繼承基類parent2
    pass

# ps:__base__只檢視從左到右繼承的第一個子類,__bases__則是檢視所有繼承的父類

print(Sub1.__bases__)  # 檢視基類
# (<class '__main__.Parent1'>,)  # 單繼承

print(Sub2.__bases__)  # 檢視基類
# (<class '__main__.Parent1'>, <class '__main__.Parent2'>)  # 多繼承





# 但凡是繼承了object類的子類以及該子類的子子孫孫類都是新式類
# 反之就是經典類

# 在python2中有新式類與經典類之分,在python3中全都是新式類
# 在python2中,沒有顯式的繼承object類的類,以及該類的子類,都是經典類
# 在python2中,顯式地宣告繼承object的類,以及該類的子類,都是新式類
# 在python3中,無論是否繼承object,都預設繼承object,即python3中所有類均為新式類

# print(Parent1.__bases__)
# print(Parent2.__bases__)

  • 先抽象再繼承

    繼承描述的是子類與父類之間的關係,是一種什麼是什麼的關係。要找出這種關係,必須先抽象再繼承

    抽象即抽取類似或者說比較像的部分。

    抽象分成兩個層次:

    1.將奧巴馬和梅西這倆物件比較像的部分抽取成類;

    2.將人,豬,狗這三個類比較像的部分抽取成父類。

    抽象最主要的作用是劃分類別(可以隔離關注點,降低複雜度)

    繼承:是基於抽象的結果,通過程式語言去實現它,肯定是先經歷抽象這個過程,才能通過繼承的方式去表達出抽象的結構。

    抽象只是分析和設計的過程中,一個動作或者說一種技巧,通過抽象可以得到類

  • 繼承背景下的屬性查詢

# 示例1
class Bar:    # 定義類
    def f1(self):      
        print('Bar.f1')

    def f2(self):
        print('Bar.f2')
        self.f1()  # obj.f1()

class Foo(Bar):
    def f1(self):
        print("Foo.f1")

obj = Foo()
obj.f2()  # 先從物件obj找沒有--》去物件的類Foo裡面找沒有--》去Bar裡面找有,會列印'Bar.f2'
          # 緊接著找self.f1()調的是obj.f1(),按順序查詢列印結果'foo.f1'

# 示例2
class Bar:
    def __f1(self):  # 在定義階段掃描語法變形成_Bar__f1
        print('Bar.f1')

    def f2(self):
        print('Bar.f2')
        self.__f1()  # 在類定義階段掃描語法變形成 self._Bar_f1

class Foo(Bar):
    def __f1(self):  # _Foo__f1
        print("Foo.f1")

obj = Foo()
obj.f2()  # 按順序查詢到'Bar.f2'並列印, 緊接著查詢self.__f1(),列印結果Bar.f1