1. 程式人生 > >Python類與物件技巧(1):字串格式化、封裝屬性名、可管理的屬性、呼叫父類方法

Python類與物件技巧(1):字串格式化、封裝屬性名、可管理的屬性、呼叫父類方法

1. 自定義字串的格式化

_formats = {
    'ymd' : '{d.year}-{d.month}-{d.day}',
    'mdy' : '{d.month}/{d.day}/{d.year}',
    'dmy' : '{d.day}/{d.month}/{d.year}'
    }
    
class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    def __format__(self, code):
        if code == '':
            code = 'ymd'
        fmt = _formats[code]
        return fmt.format(d=self)

d = Date(2018, 11, 4)
print(d.year, d.month, d.day) # >>> 2018  11  4
print(format(d)) 
# >>> 2018-11-4
print('This day is {:mdy}'.format(d))
# >>> This day is 11/4/2018

__format__()方法給Python的字串格式化功能提供了一個鉤子。 這裡需要強調的是格式化程式碼的解析工作由類決定。

2. 在類中封裝屬性名

如果想封裝類的例項上面的“私有”資料,但是Python語言並沒有訪問控制。Python不依賴語言特性去封裝資料,而是通過遵循一定的屬性和方法命名約定來達到這個效果。

  • 任何以單下劃線_開頭的名字都應該是內部實現
class A:
    def __init__(self):
        self._internal = 0 # 內部屬性
        self.public = 1    # 共有屬性
        
    def public_method(self):
        pass
    
    def _internal_method(self):
        pass    

Python並不會真的阻止訪問內部名稱。但是這麼做肯定是不好的,可能會導致脆弱的程式碼(所以說對於初級程式設計師而言,python的嚴密性與可靠性都是遠不如C++的)。 同時注意到,使用下劃線開頭的約定同樣適用於模組名和模組級別函式。 例如,以單下劃線開頭(比如_socket)的模組,就是內部實現。 類似的,模組級別函式比如 sys._getframe() 在使用的時候就得加倍小心了(因為內部方法的呼叫,原則上外部不應該隨意使用)。

  • 任何以雙下劃線__開頭的名字會導致訪問名稱變成其他形式
class B:
    def __init__(self):
        self.__private = 0 # 訪問名稱會發生變化

    def __private_method(self):
        pass

    def public_method(self):
        pass
        self.__private_method()

使用雙下劃線開始會導致訪問名稱變成其他形式。 比如,在類B中,私有屬性會被分別重新命名為 _B__private _B__private_method 這樣重新命名的目的就是繼承——這種屬性通過繼承是無法被覆蓋的。例如:

class C(B):
    def __init__(self):
        super().__init__()
        self.__private = 1 # 沒有覆蓋B.__private
    
    # 沒有覆蓋B.__private__method()
    def __private_method(self):
        pass

這裡,私有名稱 __private 和 __private_method 被重新命名為 _C__private 和 _C__private_method ,這個跟父類B中的名稱是完全不同的。

Note:提到單下劃線和雙下劃線來命名私有屬性,到底哪種方式好呢? 大多數而言,應該讓非公共名稱以單下劃線開頭。但是,如果程式碼涉及到子類, 並且有些內部屬性應該在子類中隱藏起來,那麼才考慮使用雙下劃線方案。

3. 建立可以管理的屬性

如果想給某個例項attribute增加除訪問與修改之外的其他處理邏輯,比如型別檢查或合法性驗證。自定義某個屬性的一種簡單方法是將它定義為一個property。property的一個關鍵特徵是它看上去跟普通的attribute沒什麼兩樣, 但是訪問它的時候會自動觸發 getter 、setter 和 deleter 方法。下面增加對一個屬性簡單的型別檢查:

class Person:
    def __init__(self, first_name):
        self.first_name = first_name
        
    # Getter function : 使得first_name成為一個屬性
    @property
    def first_name(self):
        return self._first_name
    
    # setter function : 關聯屬性的裝飾器
    @first_name.setter
    def first_name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._first_name = value
    
    # deleter function (optional) : 關聯屬性的裝飾器
    @first_name.deleter
    def first_name(self):
        raise AttributeError("Can't delete attribute")

p = Person('ziheng')
print(p.first_name) # calls the getter
# >>> ziheng
p.first_name = 'xiaohe' # calls the setter 必須是字串才會收集
print(p.first_name)
# >>> xiaohe
del p.first_name
# >>> AttributeError: Can't delete attribute

在實現一個property的時候,底層資料需要儲存在某個地方。 因此,在get和set方法中,會看到對 _first_name 屬性的操作,這也是實際資料儲存的地方。

4. 呼叫父類方法

為了呼叫父類(超類)的一個已經覆蓋的方法,使用 super() 函式。super() 函式的一個常見用法是在 __init__() 方法中確保父類被正確的初始化。

class A:
    def __init__(self, x):
        self.x = x
        
class B(A):
    def __init__(self, x, y):
        super().__init__(x)
        self.y = y

obj = B(2017, 2018)
print(obj.x) # 2017
print(obj.y) # 2018

實際上,如何正確使用 super() 函式還需要知道更多,比如說在複雜的多繼承中直接呼叫父類使用super()存在很大差別。

class Base:
    def __init__(self):
        print('Base.__init__')
        
class A(Base):
    def __init__(self):
        Base.__init__(self)
        print('A._init__')

class B(Base):
    def __init__(self):
        Base.__init__(self)
        print('B.__init__')

class C(A,B):
    def __init__(self):
        A.__init__(self)
        B.__init__(self)
        print('C.__init__')

obj = C()

執行結果:

Base.__init__
A._init__
Base.__init__
B.__init__
C.__init__

Comment:Base.__init__呼叫了兩次!!!

class Base:
    def __init__(self):
        print('Base.__init__')
        
class A(Base):
    def __init__(self):
        super().__init__()
        print('A._init__')

class B(Base):
    def __init__(self):
        super().__init__()
        print('B.__init__')

class C(A,B):
    def __init__(self):
        super().__init__()
        print('C.__init__')

obj = C()

執行結果:

Base.__init__
B.__init__
A._init__
C.__init__

為了弄清原理,需要解釋Python是如何實現繼承的。 對於定義的每一個類,Python會計算出一個方法解析順序(MRO)列表。 這個MRO列表就是一個簡單的所有基類的線性順序表。例如:

>>> C.__mro__
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Base'>, <class 'object'>)

當使用 super() 函式時,Python會在MRO列表上搜索下一個類。 只要每個重定義的方法統一使用 super() 並只調用它一次, 那麼控制流最終會遍歷完整個MRO列表,每個方法也只會被呼叫一次。

文章參考《python3-cookbook》