1. 程式人生 > >[Python]類與面向對象編程

[Python]類與面向對象編程

自定義 amount 最終 標識 裝飾器 順序 特殊屬性 元類 創建

1. class語句

類通常是由函數、變量和屬性組成的集合。使用class語句可以定義類,例如:

class Account(object):
    num_accounts = 0
    def __init__(self, name, balance):
        self.name = name
        self.balance = balance
        Account.num_accounts += 1
    def __del__(self):
        Account.num_accounts -= 1
    def deposit(self, amt):
        self.balance = self.balance + amt
    def withdraw(self, amt):
        self.balance = self.balance - amt
    def inquiry(self):
        return self.balance

在類主體執行期間創建的值放在類對象中,這個對象充當著命名空間,例如:

Account.num_accunts
Account.__init__
Account.__del__
Account.deposit
Account.withdraw
Account.inquiry

需要註意的是,class語句本身並不創建該類的任何類型。類僅設置將在以後創建的所有實例都使用的屬性。類中定義的函數稱為實例方法。類的實例作為第一個參數傳遞,根據約定,這個參數稱為self,但所有合法的標識符都可以使用。類變量是可在類的所有實例之間共享的值。比如上例的num_accounts變量用於跟蹤存在多少個Account實例。

?

2. 類實例

類的實例是以函數形式調用類對象來創建的。這種方法將創建一個新實例,而後將該實例傳遞給類的__init__()方法。__init__()方法的參數包括新創建的實例self和在調用類對象時提供的參數。例如:

a = Account("Guido", 1000.00)
b = Account("Bill", 10.00)

通過將屬性分配給self來將其保存到實例中。例如self.name = name表示將name屬性保存在衫例中。使用"."運算符可以訪問這些屬性以及類屬性,例如:

a.deposit(100.00)
b.withdraw(50.00)
name = a.name

盡管類會定義命名空間,但它們不會為在方法體內使用的名稱限定範圍。所以在實現類時,對屬性和方法的引用必須是完全限定的。比如之前的例子中使用的是self.balance而非balance。如果希望從一個方法中調用另一個方法,也可以采用這種方式,例如:

class Foo(object):
    def bar(self):
        print("bar!")
    def spam(self):
        bar(self) # 錯誤,拋出NameError異常
        self.bar()
        Foo.bar(self)

?

3. 繼承

繼承是一種創建新類的機制。原始類稱為基類或超類。新類稱為派生類或子類。通過繼承創建類時,所創建的類將“繼承”其基類定義的屬性。派生類可以重新定義屬性並添加自己的屬性。
在class語句中使用以逗號分隔的基類名稱列表來指定繼承。如果沒有有效的基類,將繼承object。繼承通常會重新定義現有方法的行為,例如:

import random
class EvilAccount(Account):
    def inquiry(self):
        if andom.randint(0, 4) == 1:
            return self.balance * 1.10
        else:
            return self.balance
c = EvilAccount("George", 1000.00)
c.deposit(10.0)
available = c.inquiry()

如果搜索一個屬性時未在實例或實例的類中找到匹配項,搜索將會在基類上進行。這個過程會一直繼續下去,直到沒有更多的基類可供搜索。子類可以定義自己的__init__()方法。因此,要由派生類調用基類的__init__()方法來對它們進行恰當的初始化。如果基類未定義__init__(),就可以忽略這一步。如果不知道基類是否定義了__init__(),可在不提供任何參數的情況下調用它,因為始終存在一個不執行任何操作的默認__init__()實現。例如:

class EvilAccount(Account):
    def __init__(self, name, balance, evilfactor):
        Account.__init__(self, name, balance)
        self.evilfactor = evilfactor
    def inquiry(self):
        if random.randint(0, 4) == 1:
            return self.balance * self.evilfactor
        else:
            return self.balance

有時,派生類重新實現了方法,但是還想調用原始的實現,可以將實例self作為第一個參數傳遞,例如:

class MoreEvilAccount(EvilAccount):
    def deposit(self, amount):
        self.withdraw(5.00)
        EvilAccount.deposit(self, amount)

但是這種寫法容易引起一些混淆,可以使用另一種方案,用super()函數,例如:

class MoreEvilAccount(EvilAccount):
    def deposit(self, amount):
        self.withdraw(5.00)
        super().deposit(amount)

Python支持多重繼承,通過讓一個類列出多個基類即可指定多重繼承,例如:

class DepositCharge(object):
    fee = 5.00
    def deposit_fee(self):
        print(self.fee)

class WithdrawCharge(object):
    fee = 2.50
    def withdraw_fee(self):
        print(self.fee)

class MostEvilAccount(EvilAccount, DepositCharge, WithdrawCharge):
    def deposit(self, amt):
        self.deposit_fee()
        super().deposit(amt)
    def withdraw(self, amt):
        self.withdraw_fee()
        super().withdraw(amt)

withdraw_fee()實際並未使用在自己的類中初始化的fee值。屬性fee是在兩個不同的基類中定義的類變量,程序使用了其中一個。要找到使用了多重繼承的屬性,可以在列表中對所有基類按從“最特殊”的類到“最不特殊”的類。而後在搜索屬性時,就會按這個順序搜索列表,直至找到該屬性的第一個定義。對於任何給定的類,通過打印它的__mro__屬性即可查看基類的順序。

?

4. 靜態方法和類方法

靜態方法是一種普通函數,就位於類定義的命名空間中。要定義靜態方法,可使用@staticmethod裝飾器,例如:

class Foo(object):
    @staticmethod
    def add(x, y):
        return x + y

要調用靜態方法,只需用類名作為它的前綴,例如:

x = Foo.add(3, 4)

類方法是將類本身作為對象進行操作的方法。類方法使用@classmethod裝飾器定義,根據約定,類是作為第一個參數(名為cls)傳遞的,例如:

class Times(object):
    factor = 1
    @classmethod
    def mul(cls, x):
        return cls.factor * x

class TwoTimes(Times):
    factor = 2

x = TwoTimes.mul(4)

?

5. 特性

特性是一種特殊的屬性,訪問它時會計算它的值,例如:

class Circle(object):
    def __init__(self, radius):
        self.radius = radius
    @property
    def area(self):
        return math.pi * self.radius ** 2
    @property
    def perimeter(self):
        return 2 * math.pi * self.radius

在這個例子中,Circle實例存儲了一個實例變量c.radius。c.area和c.perimeter是根據該值計算得來的。@property裝飾器支持以簡單屬性的形式訪問後面的方法,無需添加額外的()來調用該方法。方法本身是作為一類特性被隱式處理的,當創建一個實例然後訪問實例的方法時,不會返回原始函數對象,會得到綁定方法。綁定方法是一個對象,表示將在對象中調用()運算符時執行的方法調用。這種綁定方法對象是由在後臺執行的特性函數靜默創建的。使用@staticmethod和@classmethod定義靜態方法和類方法時,實際上就指定了使用不同的特性函數。
特性還可以攔截操作,以設置和刪除屬性。這是通過向特性附加其他setter和deleter方法來實現的,例如:

class Foo(object):
    def __init__(self, name):
        self.__name = name
    @property
    def name(self):
        return self.__name
    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError("Must be a string!")
        self.__name = value
    @name.deleter
    def name(self):
        raise TypeError("Can‘t delete name")

f = Foo("Guido")
n = f.name
f.name = "Monty" # 調用setter
f.name = 45 # 調用setter(TypeError)
del f.name

?

6. 描述符

使用特性後,對屬性的訪問將由一系列用戶定義的get、set和delete函數控制。這種屬性控制方式可以通過描述符對象進一步推廣。描述符就是一個表示屬性值的對象,通過實現一個或多個特殊的__get__()、__set__()和__delete__()方法,可以將描述符與屬性訪問機制掛鉤,例如:

class TypedProperty(object):
    def __init__(self, name, type, default=None):
        self.name = "_" + name
        self.type = type
        self.default = default if default else type()
    def __get__(self, instance, cls):
        return getattr(instance, self.name, self.default)
    def __set__(self, instance, value):
        if not isinstance(value, self.type):
            raise TypeError("Must be a %s" % self.type)
        setattr(instance, self.name, value)
    def __delete__(self, instance):
        raise AttributeError("Can‘t delete attribute")

class Foo(object):
    name = TypedProperty("name", str)
    num = TypedProperty("num", int, 42)

在這個例子中,類TypedProperty定義了一個描述符,分配屬性時它將進行類型檢查,例如:

f = Foo()
a = f.name # 隱式調用Foo.name.__get__(f.Foo)
f.name = "Guido" # 調用Foo.name.__set__(f, "Guido")
del f.name # 調用Foo.name.__delete__(f)

?

7. 數據封裝和私有屬性

默認情況下,類的所有屬性和方法都是“公共的”。這意味著對它們的訪問沒有任何限制。在基類中的所有內容都會被 派生類繼承,並可從派生類內進行訪問。這可能導致在派生類中定義的對象與在基類中定義的對象之間發生命名空間沖突,為了解決該問題,類中所有以雙下劃線開頭的名稱都會變成具有_類名__Foo形式的新名稱。例如:

class A(object):
    def __init__(self):
        self.__X = 3 # 變為self._A__X
    def __spam(self): # 變成_A__spam()
        pass
    def bar(self):
        self.__spam() # 只調用A.__spam()

class B(A):
    def __init__(self):
        A.__init__(self)
        self.__X = 37 # 變為self._B__X
    def __spam(self): # 變為_B__spam()
        pass

這種方案似乎隱藏了數據,但並沒有嚴格的機制來實際阻止對類的“私有”屬性進行訪問。盡管這種變形似乎是一個額外的處理步驟,但變形過程實際上只在定義類時發生一次。而且,名稱變形不會在getattr()、hasattr()、setattr()或delattr()等函數中發生,因為在這些函數中,屬性名稱指定為字符串。對於這些函數,需要顯示使用變形名稱。

?

8. 對象表示和屬性綁定

在類的內部,實例是使用字典來實現的,可以用實例的__dict__屬性的形式訪問該字典。這個字典包含的數據對每個實例而言都是唯一的。對實例的修改始終會反映到局部__dict__屬性中。同樣,如果直接對__dict__進行修改,所做的修改也會反映在該屬性中。
實例被特殊屬性__class__鏈接回它們的類。在特殊屬性__base__中將類鏈接到它們的基類。只要使用obj.name = value設置屬性,就會調用特殊方法obj.__setattr__("name", value)。如果使用del obj.name刪除了一個屬性,就會調用特殊方法obj.__delattr__("name")。
來查找屬性時,將調用特殊方法obj.__getattrribute__("name")。如果搜索過程失敗,最終會嘗試調用類的__getattr__()方法(如果已定義)來查找該屬性。如果這也失敗,就會拋出AttributeError異常。

?

9. 運算符重載

用戶可以定義Python的所有內置運算符,比如,如果希望向Python添加一種新的數字類型,可以定義一個類並在該類中定義__add__(),例如:

class complex(object):
    def __init__(self, real, imag=0):
        self.real = float(real)
        self.imag = float(imag)
    def __repr__(self):
        return "Complex(%s, %s)" % (self.real, self.imag)
    def __str__(self):
        return "(%g+%gj)" % (self.real, self.imag)
    def __add__(self, other):
        return Complex(self.real + other.real, self.imag + other.imag)
    def __sub__(self, other):
        return Complex(self.real - other.real, self.imag - other.imag)

在這個例子中,__repr__()方法創建一個字符串,可以計算該字符串來重新創建對象。__str__()方法創建具有良好輸出格式的字符串。__add__()和__sub__()實現數學運算。
?

10. 抽象基類

要定義抽象基類,需要使用abc模塊。該模塊定義一個元類(ABCMeta)和一組裝飾器,可以按如下方式使用:

from abc import ABCMeta, abstractmethod, abstractproperty
class Foo:
    __metaclass__ = ABCMeta
    @abstractmethod
    def spam(self, a, b):
        pass
    @abstractproperty
    def name(self):
        pass

要定義抽象類,需要將其元類如上所示設置為ABCMeta。因為抽象類的實現離不開元類。在抽象類中,@abstractmethod和@abstractproperty裝飾器指定Foo的子類必須實現一個方法或特性。抽象類不能直接實例化。這一限制也適用於派生類,如果派生類沒有實現一個或多個抽象方法,那麽嘗試創建派生類實例將會失敗。
抽象基類支持對已經存在的類進行註冊,使其屬於該基類。這是用register()方法完成的,例如:

class Grok(object):
    def spam(self, a, b):
        print("Grok.spam")

Foo.register(Grok)

?

10. 元類

在Python中定義類時,類定義本身將成為一個對象。例如:

class Foo(object): pass
isinstance(Foo, object) # True

類對象的這種創建方式是由一種名為元類的特殊對象控制的。即元類就是知道如何創建和管理類的對象。如果查看Foo的類型,將會發現它的類型為type。
使用class語句定義新類時,類主體將作為其自己的私有字典內的一系列語句來執行。語句的執行與正常代碼執行過程相同,只是會在私有成員上發生名稱變形。最後,類的名稱、基類列表和字典將傳遞給元類的解構函數,以創建相應的類對象。類創建的最後一步,也就是調用元類type()的步驟,可以自定義。
類可以顯式地指定其元類,這通過在基類元組中提供metaclass關鍵字參數來實現,例如:

class Foo(metaclass=type)
    __metaclass__ = type
    ...

如果沒有顯示指定元類, class語句將檢查基類元組中的第一個條目。在這種情況下,元類與第一個基類的類型相同。如果沒有指定基類,class語句將檢查全局變量__metaclass__是否存在。如果找到了該變量,將使用它來創建類。如果沒有找到任何__metaclass__值,Python將使用默認的元類(type())。
?

10. 類裝飾器

類裝飾器是一種函數,它接受類作為輸入並返回類作為輸出,例如:

registry = { }
def register(cls):
    registry[cls.__clsid__] = cls
    return cls

要使用該函數,可以在類定義前將它用作裝飾器,例如:

class Foo(object):
    __clsid__ = "123-456"
    def bar(self):
        pass

等同的方式如下:

class Foo(object):
    __clsid__ = "123-456"
    def bar(self):
        pass
register(Foo)

[Python]類與面向對象編程