Python class設計

  • 由於python不像c++一樣在設計時必須考慮記憶體的管理,因為在class的設計上複雜度已經下降許多。
  • python class內部沒有public, protected, private的區間,全部都是public,因此若是希望為private value時,應在變數前面加上兩個underline。
  • 當 Python 看到一個 attribute 後,會到該 object 一個內建的 attribute dict 中尋找,這個 attribute 是不是有定義,如果是 data 即回傳該 variable 的值,若是 function 則回傳該 method object;假使找不到,則再往上一層,也就是 class 或 super class object 找。
class Account:
    def __init__(self, number, name):
        # constructor
        self.number = number
        self.name = name
        self.balance = 0

    def deposit(self, amount):
        # membership function
        if amount <= 0:
            raise ValueError('must be positive')
        self.balance += amount

    def withdraw(self, amount):
        # membership function
        if amount <= self.balance:
            self.balance -= amount
        else:
            raise RuntimeError('balance not enough')

    def __del__(self):
        # destructor
        return "bye bye"

# 建立實例
acct = Account('123-456-789', 'Justin')

acct.deposit(1000)
print (acct.balance) #1000

acct.withdraw(500)
print (acct.balance) #500
  • 在class中desposit與withdraw都有self參數,為何呼叫時不見了? 其原因是acct.deposit(value)等同於Account.deposit(acct, value)。

  • 可用dir(ClassName)或dir(Instance)查看class的value以及所支援的所有method

    • 由於instance已經呼叫了constructor,因此會多出number, name, 以及balance三個屬性。
print(dir(Account))

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', 
'__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', 
'__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', 
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', 
'__sizeof__', '__str__', '__subclasshook__', '__weakref__', 
'deposit', 'withdraw']

# instance較class多出number, name以及balance三個屬性
print(dir(acct))
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', 
'__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', 
'__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__',
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__', '__weakref__', 
'balance', 'deposit', 'name', 'number', 'withdraw']
  • 使用type()可查看class或instance的類別名稱
print(type(Account))
<class 'type'>

print(type(acct))
<class '__main__.Account'>

建構子 (constructor)

  • 利用建構子 (constructor) 建立的物件被稱為實體 (instance) ,實際上建立物件的整個過程是執行 _init() 方法 (method) 。自行定義的類別會有預先定義好的 \init_() ,我們也可以改寫 (override) 成我們自己需要的。

  • 凡是實體的方法都需要一個特別的參數 -- self , self 是個預設的保留字 (reserved word) ,所指的是實體本身自己,在 init() 所定義的實體屬性 (attribute) 都需要額外加上 self。

類別建構與初始化

  • 一般使用物件是用初始化函數_init,下面看看\init和\new_的聯繫和差別。
class A(object):
    def __init__(self,*args, **kwargs):
        print "init %s" %self.__class__

    def __new__(cls,*args, **kwargs):
        print "new %s" %cls
        return object.__new__(cls, *args, **kwargs)

a = A()
  • 從輸出可以看到,當通過類產生實體一個物件的時候,_new方法首先被調用,然後是\init_方法。
  • 對於_new和\init_可以概括為:

    • _new方法在Python中是真正的構造方法(創建並返回實例),通過這個方法可以產生一個cls對應的實例物件,所以說\new_方法一定要有返回
    • 對於_init方法,是一個初始化的方法,self代表由類產生出來的實例物件,\init_將對這個物件進行相應的初始化操作
  • _new_是在新式類中新出現的方法,它有以下行為特性:

    • _new_ 方法是在類產生實體物件時第一個調用的方法,將返回實例物件
    • _new_” 方法始終都是類的靜態方法(即第一個參數為cls),即使沒有被加上靜態方法裝飾器
    • 第一個參數cls是當前正在產生實體的類,如果要得到當前類的實例,應當在當前類中的 _new 方法語句中調用當前類的父類的\new_ 方法
    • 如果(新式)類中沒有重寫_new方法,Python默認是調用該類的直接父類的\new方法來構造該類的實例,如果該類的父類也沒有重寫\new,那麼將一直按照同樣的規則追溯至object的\new_方法,因為object是所有新式類的基類。
    • _new決定是否要使用該類的\init方法,因為\new_ 可以調用其他類的構造方法或者直接返回別的類創建的物件來作為本類的實例。
    • 通常來說,新式類開始產生實體時,_new方法會返回cls(cls指代當前類)的實例,然後調用該類的\init方法作為初始化方法,該方法接收這個實例(即self)作為自己的第一個參數,然後依次傳入\new_方法中接收的位置參數和具名引數。
    • 但是,如果_new沒有返回cls(即當前類)的實例,那麼當前類的init_方法是不會被調用的。

str method

  • 類別 (class) 預設的 str() 方法 (method) 回傳實體 (instance) 物件 (object) 的字串 (string) 表達形式。
  • 預設的方式是印出類別名稱,實體所在的記憶體位址。
class Account:
    def __init__(self, number, name):
        # constructor
        self.number = number
        self.name = name
        self.balance = 0

    def __str__(self):
        return "{}_{}:{}".format(self.number, self.name, self.balance)

# 建立實例
acct = Account('123-456-789', 'Justin')
acct.deposit(1000)
print (acct) # 等同print(acct.__str__())

# 會呼叫__str__()得到如下:
123-456-789_Justin:500

用property 取代 getters, setters

  • 官網有許多種寫法

  • 寫 Java 的人習慣寫一堆 getX() 以及 setX(),這在 Python 中是不被鼓勵的,pythonic 方式是直接存取 attribute ,而當你需要進一步控制時,可以使用 property 達到 getter 以及 setter 效果,如此一來,客戶端的程式不需要修改就能正常運作。

    • property可將method當做attribute使用。
    • 要避免對吃資源的 method 使用 property ,因為存取 attribute 給人的感覺是很輕量的操作,這樣使用該物件的人就有可能忽視效能的問題。
class Person(object):

    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    @property
    def full_name(self):
        return "%s %s" % (self.first_name, self.last_name)

person = Person("Mike", "Driscoll")
print (person.full_name) #'Mike Driscoll'

類別屬性與實體屬性

  • Python 類別 (class) 的屬性 (attribute) 有兩種,一種是類別屬性 (class attribute) ,另一種是實體屬性 (instance attribute).

    • 兩者的可視範圍不同,instance attribute只能在該instance被看見,而class attribute在同一class中的不同instnace均為可見。

      class Account:
      # class attribute, 客戶數量
      count = 0
      
      def __init__(self, number, name):
        # instance atrtributes
        Account.count += 1    
        self.number = number
        self.name = name
        self.balance = 0
      
  • 兩者有何差別呢?通常類別屬性需要用類別名稱來存取,但若兩者的識別字 (identifier) 不同,實體物件 (object) 也可以存取類別屬性。

acct = Account('123-456-789', 'Justin')
acct2 = Account('123-456-790', 'John')

print(Account.count)    # 0
acct = Account('123-456-789', 'Justin')
print(Account.count)    # 1
acct2 = Account('123-456-790', 'John')
print(Account.count)    # 2
print (acct.count)      # 2
print(acct2.count)      # 2

static方法與class方法

  • static 方法的作用與函數 (function) 相同,也就是說不需要建立實體物件 (instance) 就可以使用,然而 static 方法仍是類別的一部分,因此呼叫 (call) 類別方法需連用類別名稱。

    • static方法定義時沒有參數
  • 類別方法需要一個特別的參數 (parameter) ,習慣上使用 cls ,這與實體方法的 self 類似,不同的是 cls 用來存取類別的屬性 (attribute) 。

  • 需要注意的是可利用類別名稱呼叫 static 方法與類別方法,也可以利用實體物件呼叫兩者。

class Account:
     def __init__(self, number, name):
        self.number = number
        self.name = name
        self.balance = 0

    @staticmethod
    def static_func():
        return("this is static method.")

    @classmethod
    def class_func(cls):
        return ("this is class method of {}".format(cls.__name__))

print(Account.static_func()) # this is static method.
print(Account.class_func()) # this is class method of Account

acct = Account('123-456-789', 'Justin')
print (acct.static_func())  # this is static method.
print (acct.class_func())   # this is class method of Account

類別 屬性封裝

  • Python 類別 (class) 的屬性 (attribute) 權限預設是公開的,因此類別以外的地方也可以存取。
    • 這些類別以外的地方,直接以句點運算子存取屬性值,然而有時候我們希望做到良好的資訊隱藏 (information hiding) ,也就是說我們不要讓類別定義以外的地方,存取屬性值,這時,我們可以將屬性設定為私有的,簡單來說,就是在屬性識別字 (identifier) 名稱前加上連續兩個底線符號。
    • 這樣一來,若是要在類別以外的地方使用屬性值,需要另外可存取的公開方法 (method)
class Account:
    def __init__(self, number, name):
        Account.count += 1
        self.__number = number
        self.__name = name
        self.__balance = 0

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError('must be positive')
        self.__balance += amount

    def withdraw(self, amount):
        if amount <= self.__balance:
            self.__balance -= amount
        else:
            raise RuntimeError('balance not enough')

     def get_balance(self):
        return self.__balance

//
acct = Account('123-456-789', 'Justin')
# print(acct.__balance) # AttributeError: 'Account' object has no attribute '__balance'
print (acct.get_balance())  # 0

類別 繼承

  • 繼承的格式如下, 類別名稱後的小括弧中註明父類別,可多重繼承。
    • ChildA的constructor中直接指定Base,優點是明確,缺點是若Base更名或是ChildA繼承自不同物件時也要連動改名。
    • ChildB、ChildC使用了super指定的其繼承自何方,可避免上述缺點。
    • 使用super還有一個原因是dependency injection Stackoverflow discussion
    • isinstance() 用於檢查 class 類型,若物件類型與Class相同,則回 True
    • issubclass() 用於檢查繼承,若某一 Class 是繼承,則回 True
class ChildA(Base):
    def __init__(self):
        Base.__init__(self)

class ChildB(Base):
    def __init__(self):
        super(ChildB, self).__init__()

class ChildC(Base):
    def __init__(self):
        # OK in python 3.x
        super().__init__()
  • 子類別 (subclass) 可依本身特性設定自己的屬性 (attribute) 與方法 (method) ,也會從父類別 (superclass) 繼承 (inherit) 屬性與方法。一般來說,沒有設定成私有的屬性及方法都會被繼承,子類別可由父類別公開的方法存取父類別私有的屬性。

  • 子類別也可依需要改寫 (override) 父類別的方法,這是說子類別需要用到與父類別具有相同名稱的方法,但是子類別需要的功能有所修改、擴充或增加,因此當子類別裡頭定義與父類別相同名稱的方法時,就會改寫父類別的方法。經過改寫,子類別的方法完全屬於子類別所有.

  • Demo 為父類別,定義四個方法, SubDemo 為子類別,改寫 Demo 的兩個方法,包括 init() 與 str() 。

class Demo:
    __x = 0

    def __init__(self, i):
        self.__i = i
        Demo.__x += 1

    def __str__(self):
        return str(self.__i)

    def hello(self):
        print("hello " + self.__str__())

    @classmethod
    def getX(cls):
        return cls.__x

class SubDemo(Demo):
    def __init__(self, i, j):
        super().__init(i)
        self.__i = i
        self.__j = j

    def __str__(self):
        # 這裡的 __str__() ,在 return 後的運算式 (expression) 
        # 先呼叫 super().__str__() ,因為 self.__i 已經變成父類別
        # Demo 私有屬性,因此需要先呼叫父類別的 __str__() 。

        return str(self.__i) + "+" + str(self.__j)

a = SubDemo(1234)
a.hello()
print("a.__x =", a.getX())
b = SubDemo(56, 78)
b.hello()
print("b.__x =", b.getX())
print()
print("a.__x =", a.getX())
print("b.__x =", b.getX())

多重繼承

  • 設計類別 (class) 時,父類別 (superclass) 可以有多個,這是說子類別 (subclass) 能夠繼承 (inherit) 多個父類別,使子類別可以有多種特性。
  • 多重繼承 (mutliple inheritance) 的使用很簡單,寫父類別的小括弧中,利用逗點分開每個父類別即可。
    • 當子類別繼承 (inheritance) 超過一個來源的時候,會以寫在最左邊的父類別優先繼承,這是說,多個父類別如果有相同名稱的屬性 (attribute) 與方法 (method) ,例如 init() 、 str() 等,就會以最左邊的父類別優先。
class SubClass(SuperClass1, SuperClass2):
    pass

多型

  • 多型可使物件的型態具有通用的效力

  • SubDemo1 與 SubDemo2 繼承 (inherit) 自 Demo ,因此兩個子類別也都繼承了 hello() 方法 (method) ,雖然 SubDemo1 與 SubDemo2 所建立的物件各自是不同型態,然而由繼承關係兩種型態可通用 hello() 。

class Demo:
    def __init__(self, i):
        self.i = i

    def __str__(self):
        return str(self.i)

    def hello(self):
        print("hello " + self.__str__())

class SubDemo1(Demo):
    def __init__(self, i, j):
        super().__init__(i)
        self.j = j

    def __str__(self):
        return super().__str__() + str(self.j)

class SubDemo2(Demo):
    def __init__(self, i, j):
        super().__init__(i)
        self.j = j
        self.k = str(self.i) + str(self.j)

    def __str__(self):
        return self.k

a = SubDemo1(22, 33)
b = SubDemo2(44, "55")
a.hello()
b.hello()

Class Private 跟 Module Private 的差別

  • 在Python中雖然有Private variable,但是不像一般程式語言一樣具有強制的效力,如果外界執意要使用還是可以呼叫的到。在StackOverflow有很清楚的解釋其中的差別。

  • Module private 是指是以一個下底線 _ 開頭的變數,例如 _number 或 _getNumber().

    • 以此例子來說,我們在 moduleA.py裡面定義了一個 Module private _foo(),因為Python有個規定是在Import的時侯會略過所有以underscore開頭的成員,所以_foo()會被忽略,在執行 print _foo() 的時侯就會發生 Exception。
    • 但是如果執意要用還是可以透過Import __foo,得到想要的結果
# Module private, moduleA.py

def _foo():
    return 'hi'

from moduleA import *
    print _foo()
  • Class Private 基本上是以二個下底線 開頭,而且不超過一個下底線 _ 結尾的的變數名稱,例如 number 或 __getNumber(),但是init 就不是 class private,但這種兩個下底線開頭和結尾叫 Magic methods有其特別的用途,但這個不在本文討論範圍。
    • dir(f)是輸出: > ['Fooaoo', '_Fooboo', 'doc', 'module', 'coo'] _boo被加上_Foo的Prefix,這種行為稱做 Name Mangling,多加上Class Name在前面。所以透過 f.boo()就沒辦法直接呼叫,但是透過加上Class Name的Prefix就可以了,例如 print f._Fooboo().
# class private

class Foo():
    __aoo = 123
    def __boo(self):
        return 123
    def coo(self):
        return 456
    f = Foo()

print dir(f)
# ['_Foo__aoo', '_Foo__boo', '__doc__', '__module__', 'coo']

print f._Foo__boo()
print f.__boo()

總結

  • Module private : _number

    • 達到 Private 的效果:Import 除非明確把 name 打出來,不然 from A import * 時會自動略過所有以一個下底線 _ 開頭的變數
    • 使用時機:在 Module level 下,告知外界,這些變數/函式是Private性質的,不建議外界直接存取
    • 強制使用方式: 直接Import即可,例如: from A import _Number
  • Class Private : __number

    • 達到 Private 的效果:Class 物件被建立的時侯,所有 __XXX 會被 name mangling成 __ClassName__XXX
    • 使用時機:跟 Module private 一樣,就是表現出這些成員是內部使用的,不建議外界直接存取
    • 強制使用方式: 使用的時侯,加上 Class name 的的 prefix
    • 繼承的時侯,這些被 name mangled 的成員也會被繼承,但是是使用Parent的ClassName的Prefix,要注意一下。 在Python的世界中,Private Variable沒辦法強制讓外界無法使用,透過Naming可以讓別人知道這些Private Variable不應該直接被存取,避免誤用的Bug發生。但是如果執意要呼叫,都還是有方法的。

直接對實例設值

  • python可對自訂class中隨時新增參數值。
class Object(object):
    pass
Obj = Object()
# 直接設值
Obj.name = "whatever"
print( Obj.name) # whatever
  • 但是若class有定義_slot_屬性時,則無法新增參數值。
    • 擁有 _slots 屬性的類在產生實體物件時不會自動分配 \dict ,而 obj.attr 即 obj.\dict_['attr'], 所以會引起 AttributeError
    • 對於擁有 _slots 屬性的類的實例 Obj 來說,只能對 Obj 設置 \slots_ 中有的屬性.
class Object(object):
    __slots__ = {'a', 'b'}

Obj = Object()
Obj.name = "whatever
AttributeError: 'Object' object has no attribute 'name'"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>

Obj.a = 1
print(Obj.a)    # 1

如何印出一個物件的所有屬性和方法

  • 這個做法其實就是 theObject._dict , 也就是 vars(obj) 其實就是返回了 o.\dict_
for property, value in vars(theObject).iteritems():
    print property, ": ", value

什麼是metaclass

  • stackoverflow
  • Metaclass是創建class的東西。,而一個class是用來創建物件。
    • 但是我們知道,Python中的類也是物件。
    • Metaclass就是用來創建類這些物件的,它們是類的類,你可以形象化地理解為:
MyClass = MetaClass()
MyObject = MyClass()

Mixin樣式

  • Mixin模式是一種在python裡經常使用的模式,適當合理的應用能夠達到複用代碼,合理組織代碼結構的目的。Python的Mixin模式可以通過多重繼承的方式來實現。

  • Mixins與Traits這兩個名詞,技術上來說其實有些不同,然而不同語言可能也會有不同的表現,因此在這邊暫且當它們是同義詞,重點在瞭解:Mixins、Traits實際上是一種在多重繼承與單一繼承間的妥協,是一種受限的多重繼承,在繼承來源為抽象的共用實作時,可避免多重繼承時的許多問題,然而,它並不能解決所有的問題。

    • 使用Mixins,一個顯見的問題是,仍舊有可能發生名稱衝突的問題。
    • 另一個隱式的問題是,Mixins的來源與被Mixin的對象之間,實際上會建立起依賴關係,被Mixin的對象中,必須實現Mixins預期之協定,通常是某個或某些方法名稱,視情況而定。
  • 使用Mixin類實現多重繼承要非常小心

    • 先它必須表示某一種功能,而不是某個物品,如同Java中的Runnable,Callable等
    • 其次它必須責任單一,如果有多個功能,那就寫多個Mixin類
    • 然後,它不依賴於子類的實現
    • 最後,子類即便沒有繼承這個Mixin類,也照樣可以工作,就是缺少了某個功能。(比如飛機照樣可以載客,就是不能飛了^_^)
  • 把一些基礎和單一的功能比如一般希望通過interface/protocol實現的功能放進Mixin模組裡面還是不錯的選擇。

  • mixin模組自帶實現。在使用的時候一般把mixin的類放在父類的右邊似乎也是為了強調這並不是典型的多繼承,是一種特殊的多繼承,而是在繼承了一個基類的基礎上,順帶利用多重繼承的功能給這個子類添點料,增加一些其他的功能。保證Mixin的類功能單一具體,混入之後,新的類的MRO樹其實也會相對很簡單,並不會引起混亂。
class CommonEqualityMixin(object):
    def __eq__(self, other):
        return (isinstance(other, self.__class__)
            and self.__dict__ == other.__dict__)

    def __ne__(self, other):
        return not self.__eq__(other)

class Foo(CommonEqualityMixin):
    def __init__(self, item):
        self.item = item
  • 物件使用Mixin的繼承依然遵守”is-a”關係,從含義上看依然遵循單繼承的原則。
    • 下面的Airplane類實現了多繼承,不過它繼承的第二個類我們起名為PlaneMixin,而不是Plane,這個並不影響功能,但是會告訴後來讀代碼的人,這個類是一個Mixin類。所以從含義上理解,Airplane只是一個Vehicle,不是一個Plane。這個Mixin,表示混入(mix-in),它告訴別人,這個類是作為功能添加到子類中,而不是作為父類,它的作用同Java中的介面。
class Vehicle(object):
    pass

class PlaneMixin(object):
    def fly(self):
        print 'I am flying'

class Airplane(Vehicle, PlaneMixin):
    pass
  • Mixin 強調的是功能而不像繼承那樣包括所有功能和資料欄,但利用 mixin 同 樣也可以實現代碼複用,下面這段代碼來自Stack Overflow,當然 functools.total_ordering() 裝飾器已經提供相同功能了,這裡僅用來說明 mixin 實現代碼複用。
lass Comparable(object):
    def __ne__(self, other):
        return not (self == other)

    def __lt__(self, other):
        return self <= other and (self != other)

    def __gt__(self, other):
        return not self <= other

    def __ge__(self, other):
        return self == other or self > other


class Integer(Comparable):
    def __init__(self, i):
        self.i = i

class Char(Comparable):
    def __init__(self, c):
        self.c = c

abstract class

  • 如果一個類別定義時不完整,有些狀態或行為必須留待子類別來具體實現,則它是個抽象類別(Abstract Class)。例如,在定義銀行帳戶時,你也許想將 一些帳戶的共同狀態與行為定義在父類別中。
    • 顯然地,這個類別的定義不完整,self.id、self.name、self.balance沒有定義,嘗試使用這個類別進行操作時,就會發生直譯錯誤.
    • 你可以繼承這個類別來實作未完整的定義,現在的問題是,實際上開發人員還是可以用Account()實例化。
    • 可以修改一下Account的定義要求使用者繼承後必須實做方法。
class Account:
    def __init__(self):
        raise NotImplementedError("Account is abstract")

    def withdraw(self, amount):
        if amount >= self.balance:
            self.balance -= amount
        else:
            raise ValueError('餘額不足')

    def __str__(self):
        return ('Id:\t\t' + self.id +
               '\nName:\t\t' + self.name +
               '\nBalance:\t' + str(self.balance))

acct = Account()
# print(acct) error
  • 由直譯錯誤是一種方式,實際上視你的需求而定(是否可實例化、子類別是否定義初始化方法等),還有許多模擬的方式,不過在Python中,可以使用Meta class與@abstractmethod來達到規範的需求。

  • 在描述流程輸廓時,並沒有提及如何顯示訊息、沒有提及如何取得使用者輸 入等具體的作法,只是歸納出一些共同的流程步驟:

import random
from abc import ABCMeta, abstractmethod

class GuessGame(metaclass=ABCMeta):
    @abstractmethod
    def message(self, msg):
        pass

    @abstractmethod
    def guess(self):
        pass     

    def go(self):
        self.message(self.welcome)
        number = int(random.random() * 10)
        while True:
            guess = self.guess();
            if guess > number:
                self.message(self.bigger)
            elif guess < number:
                self.message(self.smaller)
            else:
                break
        self.message(self.correct)

# 現在GuessGame是個抽象類別,如果你嘗試實例化GuessGame:
game = GuessGame()

# 則會引發錯誤:
TypeError: Can't instantiate abstract class GuessGame with abstract methods guess, message
  • 如果你真的想要模擬Java中interface的作用,則可以定義一個抽象類別,完全沒有實作的方法即可。
import random
from abc import ABCMeta, abstractmethod

class Flyer(metaclass=ABCMeta): 
    # 就像是Java中的interface
    @abstractmethod
    def fly(self):
        pass

class Bird:
    pass

class Sparrow(Bird, Flyer):
    # 就像Java中繼承Bird類別並實作Flyer介面
    def fly(self):
        print('麻雀飛')

s = Sparrow()
s.fly()

results matching ""

    No results matching ""