Python3.4 Tutorial9 - Classes (Part 2)

walkerMK發表於2015-09-23

9.3 初看類

    類會引入一點新語法,三個新的物件型別,和一些新語義。

9.3.1 類定義的語法

    最簡單的類定義的結構如下:

 class ClassName:
      <statement-1>
      .
      .
      .
      <statement-N>

    類的定義和函式定義一樣,必須放在使用之前。(你可以安心的將類的定義放在一個分支語句if中,也可以放在函式內部。)

    實踐中,放在類定義內的語句同時是函式定義,但其他語句也是可以的,並且有一些會很有用——稍後會講這方面的內容。類中的函式定義通常擁有獨特的引數列表形式,這是由方法的呼叫約定所決定的——同樣,稍後會進行展開。

    當進入類的定義範圍(直譯器執行到類的定義處),一個新的名稱空間就被建立了,並被用作區域性作用域——因此,所有對區域性變數的賦值都會進入這個新的名稱空間中。特別是函式定義會將名稱繫結到這裡的新函式。

    一個類的定義正常結束後,一個類物件(Class Object)就被建立了。總的來說,類定義將名稱空間中的內容包裝了起來;下一小節會學習關係類物件更多的內容。最初的區域性作用域(進入類定義之前的作用域)會復原,並且類物件會繫結到這裡的類名中,類名由類定義頭(就是例子中的ClassName)決定。

9.3.2 類物件(Class Objects)

    類物件支援兩種操作:屬性引用(Attribute References)和例項化(Instantiation)。

    類的屬性引用使用Python所有屬性引用都使用的標準語法:obj.name。類物件建立後,類的名稱空間中的所有名稱都是合法的屬性名。所以,如果類的定義如下:

class MyClass:
     """A simple example class"""
     i = 12345
     def f(self):
         return 'hello world'

    那麼,MyClass.iMyClass.f都是合法的屬性引用,它們分別返回一個整數和一個函式物件。類屬性也可以被賦值,所以你可以通過賦值語句改變MyClass.i的值。__doc__也是合法的屬性,它返回屬於類的docstring:"A simple example class"

    類的例項化使用函式的形式。假裝類物件是一個無參函式,它返回類的一個新例項。例如(假設使用上面的類):

x = MyClass()

    建立這個類的新例項,並將它賦給區域性變數x。

    例項化操作(“呼叫”一個類物件)會建立一個空的物件。很對類在建立物件時,會將例項自定義到一個特殊的初始狀態。因此,類會定義一個名為__init__()的特殊方法,就像這樣:

def __init__(self):
     self.data = []

    當類定義了一個__init__()方法,例項化這個類的時候會為新建立的例項自動呼叫__init__()。所以在這個例子中,一個新的、初始化過的例項會通過下面的語句獲得:

x = MyClass()

    當然,為了更好的靈活性,__init__()方法也可以有引數。那種情況下,給類例項化操作符的引數會傳遞給__init__()。例如:

>>> class Complex:
 ...     def __init__(self, realpart, imagpart):
 ...         self.r = realpart
 ...         self.i = imagpart
 ...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

9.3.3 例項物件(Instance Objects)

    現在我們可以用例項物件做什麼呢?例項物件唯一能理解的操作是屬性引用。有兩中合法的屬性名,資料屬性和方法。     資料屬性相當於Smalltalk中的“例項變數”,或C++中的“資料成員”。資料屬性不需要被宣告;和區域性變數一樣,它們在第一次被賦值的時候建立。例如,如果x是上面建立的MyClass的例項,下面的程式碼段會列印出16,而不會產生錯誤:

x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print(x.counter)
del x.counter

    另一類例項屬性的引用是方法(Method)。方法是“屬於”物件的一個函式。(在Python中,方法這個術語不是類的例項獨有的:其他物件型別也有方法。例如,list物件有append, insert, remove, sort等方法。然而,在下面的討論中,除非特別指出,“方法”這個術語我們僅用來代表類例項物件的方法。)

    例項物件的合法方法名依賴於它所屬的類。在定義中,所有是類的函式物件定義的屬性,都代表著類例項的方法。所以在我們的例子中,x.f是一個合法的方法引用,因為MyClass.f是一個函式;但是x.i不是方法引用,因為MyClass.i不是函式。但x.fMyClass.f不是一個東西——它(x.f)是方法物件,不是函式物件。

9.3.4 方法物件(Method Objects)

    通常,方法在繫結後就可以正常呼叫了:

x.f()

    在MyClass的例子中,它會返回字串'hello world'。然而,不是非要用這種形式才能呼叫方法:x.f是一個方法物件,它可以被儲存到其他地方,以後再呼叫。例如:

xf = x.f
while True:
    print(xf())

    會持續列印hello world直到時間的盡頭。

    方法被呼叫時究竟發生了什麼呢?你可能注意到了,雖然x.f()定義時指明瞭一個引數,但呼叫時並沒有傳入引數。引數去哪了?呼叫一個需要引數的函式時,不傳入引數的話Python肯定會引發異常——即使引數並沒有被使用...

    你可能已經猜到答案了:方法的特別之處就是,物件會作為函式的第一個引數被傳入。在我們的例子中,x.f()MyClass.f(x)是等價的。一般地,使用有n個的元素引數列表呼叫一個方法,等價於使用有n+1個元素的引數列表呼叫函式,多出來的1個元素是函式的物件,並且它作為第一個引數被傳入(這裡比較拗口,不懂的請參考原文:When the method object is called with an argument list, a new argument list is constructed from the instance object and the argument list, and the function object is called with this new argument list)。

    如果你還不理解方法是怎樣工作的,看一下實現方式可能會理清思路。當一個不是資料屬性的例項屬性被引用時,它所屬的類會被搜尋到。如果這個名字代表了一個合法的類屬性,那麼它是一個函式物件,在一個抽象物件中將例項物件和函式物件打包(指向)在一起的時候方法物件就被建立了:這就是方法物件。如果呼叫方法物件時傳遞了一個引數列表,一個新的引數列表會從例項物件和傳入的引數列表中被建立,並且會用這個新的引數列表來呼叫函式物件。

9.3.5 類變數和例項變數

    一般來說,例項變數是每個例項獨有的資料,類變數是在這個類的所有例項間共享的屬性或方法:

class Dog:

     kind = 'canine'         # class variable shared by all instances

     def __init__(self, name):
         self.name = name    # instance variable unique to each instance

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # shared by all dogs
'canine'
>>> e.kind                  # shared by all dogs
'canine'
>>> d.name                  # unique to d
'Fido'
>>> e.name                  # unique to e
'Buddy'

    就像9.1中討論的那樣,共享的資料在涉及到像list和dictionary這樣的可變物件時,可能會有出乎意料的效果。例如,下面程式碼中的list物件tricks 不能被用作類變數,因為一個list物件會被所有的Dog 類的例項共享:

class Dog:

    tricks = []             # mistaken use of a class variable

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # unexpectedly shared by all dogs
['roll over', 'play dead']

    正確設計的類應該使用一個例項變數代替:

class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []    # creates a new empty list for each dog

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

9.4 隨筆

    資料屬性可以重寫(Override)同名的方法屬性;為了避免意外的命名衝突在大型程式中造成難以發現的Bug,使用一些約定來最小化衝突的可能性是明智的。可能的約定包括:大寫方法名的首字母,給資料屬性名加一個短小的獨特字串(可能只有一個下劃線'_')字首,或者方法名用動詞、資料屬性用名詞。

    資料屬性可能被方法引用,就和被普通的物件使用者(“客戶”)引用一樣。換句話說,類不是用來實現純抽象的資料型別。實際上,Python中沒有真正實現資料隱藏的方法——這一切都是基於約定的。(另一方面,Python是用C實現的,如果必要的話可以實現完全的細節隱藏和物件訪問控制;可以用C編寫Python擴充實現。)

    客戶在使用資料屬性時應該小心——客戶可能會在亂用資料屬性的時候,造成本該由方法維護的不可變資料的混亂。(這裡可能翻譯的不太準確,給出原文:clients may mess up invariants maintained by the methods by stamping on their data attributes)注意,客戶可以在不影響方法有效性的情況下,自己給資料物件新增資料屬性,和命名衝突的避免一樣——命名約定在這裡也可以避免很多令人頭疼的問題。

    沒有在方法中引用資料屬性(或其他方法)的快捷方式。我發現這實際上增加了方法的可讀性:在瀏覽方法的時候不會搞混區域性變數和例項變數。

    通常,方法的第一個引數叫做self。這只是個約定:self這個名字對Python絕對沒有特別的意義。然而,如果不遵循這個約定,你的程式碼對其他Python程式設計師來說可讀性比較差,而且一些類的瀏覽器程式可能會依賴這項約定。

    任何是類屬性的函式物件,都定義了該類例項的一個方法。函式定義不一定非要寫在類的定義結構中:將一個函式物件賦值給類中的一個區域性變數也是Ok的。例如:

# Function defined outside the class
def f1(self, x, y):
    return min(x, x+y)

class C:
    f = f1
    def g(self):
        return 'hello world'
    h = g

    現在fgh都是C類中引用了函式對的屬性了,因此它們也都是C類例項的方法——gh是完全一樣的。注意,這種做法通常只會混淆一個程式的讀者。

    方法可以通過使用self引數的方法屬性來呼叫其他方法:

class Bag:
    def __init__(self):
        self.data = []
    def add(self, x):
        self.data.append(x)
    def addtwice(self, x):
        self.add(x)
        self.add(x)

    方法可以和原來的函式一樣引用全域性名稱。方法關聯的全域性作用域,是它的定義所在的模組。(一個類永遠不會作為全域性作用域來使用。)雖然很少遇到一個在方法中使用全域性資料的好理由,但有許多合理使用全域性作用域的地方:一種情況,匯入到全域性作用域中的方法和模組可以被方法使用,就好像方法和類是在裡面定義的一樣。通常,類包含的方法是它自己在全域性作用域中定義的,下一小節中我們會找到一些合理的原因來解釋為什麼方法需要引用它自己的類。

    每個值都是一個物件,因此有一個類(也叫型別(type))。它儲存在object.__class__中。

相關文章