1. 程式人生 > >Python:類與結構體

Python:類與結構體

Python的類提供了面向物件程式設計的所有標準特性:類繼承機制允許有多個基類,一個派生類可以覆蓋基類中的任何方法,一個方法可以使用相同的名字呼叫基類中的方法。

1. Python的域(scopes)和名稱空間(namespaces)

名稱空間是名字和物件之間的對映。多數名稱空間使用Python的字典來實現,但除非出於效能考慮,我們通常不關心具體如何實現。名稱空間的例子有,內建的名稱例如abs(),內建的異常名,模組的全域性名稱,函式呼叫時的區域性名稱。在某種程度上,物件的屬性也構成名稱空間。

關於名稱空間最重要的一點是:不同名稱空間中的名稱沒有關係。例如 兩個不同模組中都可以包含名為maximize的函式,這不會造成混餚,因為使用這些模組時必須加上模組名作為字首。 另外,把任何點後的名稱叫做屬性。例如,在表示式z.real中,real是物件z的屬性。嚴格來說,引用模組中的名稱是對屬性的引用,在表示式modname.funcname中,modname是一個模組,funcname是它的一個屬性。這個例子中模組屬性和模組內定義的全域性名稱有著直接的對映,它們有著相同的名稱空間。屬性可能是隻讀的或者可寫的,上面的例子中,屬性就是可寫的,例如:modname.the_ answer = 42

。可寫的屬性可以被刪除, 例如 del modname.the_ answer 會刪除模組 modname中的 the_ answer屬性。

名稱空間在不同的時刻建立,有著不同的生命週期。包含內建名稱的名稱空間在Python直譯器啟動時被建立,且不會被刪除。 模組的全域性名稱空間在模組被匯入時被建立,正常情況下,模組的名稱空間會持續到直譯器退出。來自指令碼檔案或者互動式環境 被直譯器最頂層呼叫執行的語句,被認為是 _ main _ 模組的一部分,所以他們有著自己的全域性名稱空間。

函式的區域性名稱空間當函式被呼叫時被建立,函式返回時或者出現異常而函式又沒有提供處理方式時被刪除。當然,在遞迴呼叫中每一次呼叫都有他們自己的區域性名稱空間。

域(scpoe)是Python程式的一個名稱空間可以直接訪問的一個文字範圍,“直接訪問”在這裡的意思時當對一個名字的訪問沒有字首時,會嘗試在名稱空間內查詢這個名字。 在執行的任意時刻,至少有三個巢狀域,它們有名稱空間可以直接訪問。

  • 最內層的域,它會首先被搜尋,包含區域性名稱。任何封裝函式的域,從最近的封裝域開始搜尋,包含非區域性,非全域性的名稱。
  • 倒數第二個域,包含當前模組的全域性名稱。
  • 最外層的域,最後被搜尋,包含內建名字的名稱空間。

如果一個名字被聲名為全域性的,那麼所有的引用和賦值都是針對中間層的域,這一層域包含模組的全域性名稱。 意識到域是由文字決定是非常重要的,定義在模組中的一個函式的全域性域就是這個模組的名稱空間,無論這個函式在哪兒, 通過哪個別名被呼叫。

2. 類

2.1 類的定義

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

類定義,像函式定義一樣,在執行時才會起作用。你可以把類定義放在任何地方比如if語句的分支,或者在函式內部。
在實際應用時,定義在類中的語句通常都是函式定義,但是其它語句也是允許出現的,並且有的時候非常有用。 當進入一個類定義時,一個新的名稱空間被建立,並且被當作區域性域來使用。
如:

class people:   #定義一個類people
    def _init_(self,name,age,sex):
        self.Name=name
        self.Age=age
        self.Sex=sex

    def speak(self): #定義一個方法speak
        print "my name"+self.Name

2.2 物件

類物件提供兩種操作,屬性引用和例項化。
屬性引用使用標準句法:obj.name. 有效的屬性名是類物件建立時類的名稱空間內的所有名字。 例如下面的類定義中,MyClass.i和MyClass.f都是有效的屬性名。

>>> class MyClass:
...     i = 123
...     def f(self):
...         return 'hello world'
... 
>>> MyClass.i
123
>>> MyClass.i = 10

類的例項化使用函式記號,例如:

>>> x = MyClass()
>>> x.i
10

這個例項化操作建立了一個空物件,許多類在例項化時定義了一些初始化操作。例如:

>>> class MyClass():
...     def __init__(self):
...          self.data = []

當一個類定義了_ init _ 方法後,類例項化時會自動呼叫 _ 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)

2.3 例項化物件

現在我們可以用例項化的物件做些什麼呢?它唯一可以進行的操作是屬性引用。有兩類有效的屬性名,資料屬性和方法。
資料屬性對應c++中的資料成員,資料屬性無需宣告,第一次給它賦值時就表明了它的存在。
方法屬性是物件內的一個函式。

2.4 方法物件

通常我們呼叫一個方法的方式是:

x.f()

但是,由於x.f是一個方法物件,所以它可以儲存起來,以便以後呼叫

>>> class MyClass:
...     i = 12345
...     def f(self):
...         return 'hello world'
... 
>>> x = MyClass()
>>> x.f()
'hello world'
>>> xf = x.f
>>> xf()
'hello world'

你可能已經發現,f有一個引數,但是呼叫時為什麼沒有使用呢。其實,原因在於 x.f() 與 MyClass.f(x) 是等價的。

>>> MyClass.f(x)
'hello world'

資料屬性如果和方法屬性名稱相同,前者會覆蓋後者。所以為了避免名稱衝突,最好養成一些習慣,比如方法名稱大寫,資料屬性名稱前加一個短小,唯一的字首。或者資料屬性用名詞,方法屬性用動詞。
資料屬性可以被方法引用,也可以被物件的使用者引用。換句話說,類不能實現為純抽象資料型別。

函式定義沒有必要非在類裡面,例如:

def f1(self, x, y):
    return min(x, x+y)

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

一個方法可以通過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)
... 
>>> b = Bag()
>>> b.data
[]
>>> b.add('1')
>>> b.data
['1']
>>> b.addtwice('x')
>>> b.data
['1', 'x', 'x']

3. self的含義

self代表類的例項,而非類。
舉例說明:

class Test:
    def prt(self):
        print(self)
        print(self.__class__)

t = Test()
t.prt()

輸出結果為:

<__main__.Test object at 0x000000000284E080>
<class '__main__.Test'>

從上面的例子中可以很明顯的看出,self代表的是類的例項。而self.class則指向類。

3.1 self不必非寫成self

class Test:
    def prt(this):
        print(this)
        print(this.__class__)

t = Test()
t.prt()

改成this後,執行結果完全一樣。當然,最好還是尊重約定俗成的習慣,使用self。

3.2 總結

  • self在定義時需要定義,但是在呼叫時會自動傳入。
  • self的名字並不是規定死的,但是最好還是按照約定是用self。
  • self總是指呼叫時的類的例項。

4. 派生類

派生類的形式如下:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

BaseClassName必須在包含派生類的域內定義,BaseClassName可以是一個表示式,例如:

class DerivedClassName(modname.BaseClassName)

當派生類的物件引用了一個屬性時,會先在派生類內查詢這個屬性名,如果找不到,再到基類中查詢。 派生類可以覆蓋基類中的方法,即使基類中的方法被覆蓋了,也可以使用下面的方法來呼叫

BaseClassName.methodname(self, arguments)

5. 多重繼承

Python 支援有限的多重繼承:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

在舊風格的類中,唯一的規則是深度優先,從左到右。以上述類定義為例,如果一個屬性沒有在 DerivedClassName中被找到,那麼會繼續搜尋Base1,Base2等等。
在新風格的類中,對方法的解析次序是動態改變的,這是因為類的繼承關係會呈現出一個或多個菱形。例如新風格的類都由 object類派生出,這樣就會就多條路徑通向object。為了避免基類被多次搜尋,使用了線性化演算法將所有基類排列成從左到右的順序。

6. 私有變數和類區域性引用

例項的私有變數只能在物件內部使用,python中常常使用例如 _ spam 的形式來代表API的非公有部分,無論是函式,方法還是資料成員。類私有成員的特性的一種有效的用法是可以避免與子類中定義的名字衝突,這種機制叫做 mangling:

class Mapping:
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)

    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)

    __update = update # private copy of original update() method


class MappingSubclass(Mapping):
    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

注意上述程式碼中 __ update 的使用,避免了子類中對update的覆蓋影響到基類 __ init__ 中的 update。

7. 結構體

有時候我們可能需要像C中的struct那樣的資料型別,把少量的資料項放在一起。Python中可以使用定義一個空類來實現這一點:

# filename:p.py
class Employee:
    pass

john = Employee() # Create an empty employee record


# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000
>>> import p
>>> p.john
<p.Employee instance at 0xb71f50ac>
>>> p.john.name
'John Doe'
>>> p.john.dept
'computer lab'
>>> p.john.salary
1000

8. 異常類

使用者定義的異常也可以用類來表示,使用這種機制可以創建出可擴充套件,層次化的異常。 raise 語句有兩種新的形式

raise Class, instance
raise instance

第一種形式中,instance必須是Class的一個例項,或者是由它派生出的類。 第二種形式是下面這種形式的縮寫

raise instance.__class__, instance

下面這個例子會依次打印出B,C,D

class B:
    pass
class C(B):
    pass
class D(C):
    pass

for c in [B,C,D]:
    try:
        raise c()
    except D:
        print "D"
    except C:
        print "C"
    except B:
        print "B"
>>> import f
B
C
D

注意如果 B寫在最前面,會打印出BBB,這是因為raise C和raise D時,執行到except B是都會 print “B”. 因為B是C,D的基類.

9. 迭代器

現在你可能已經注意到了多數容器物件都可以使用for語句來迴圈

>>> for elem in [1,2,3]:
...     print elem
... 
1
2
3
>>> for elem in (1,2,3):
...     print elem
... 
1
2
3

這一風格清晰,簡捷,方便。迭代器的使用在Python中非常普便。for語句的背後,其實是對容器物件呼叫 iter(). 這個函式返回一個迭代器物件,它定義了next()函式,每次訪問容器中的一個元素。當沒有元素的時候,next()返回一個 StopIteration異常,告訴for語句迴圈結束了。

>>> s = 'asdf'
>>> it = iter(s)
>>> it
<iterator object at 0xb71f590c>
>>> it.next()
'a'
>>> it.next()
's'
>>> it.next()
'd'
>>> it.next()
'f'
>>> it.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

理解了迭代機制,就可以很容易地把迭代器加入你的類中,定義__ iter__ ()方法,返回一個有next()方法的物件。 如果一個類定義了next()函式,__ iter__ () 可以僅返回 self:

# q.py
class Reverse:
    def __init__(self, data):
        self.data = data
        self.index = len(data)
    def __iter__(self):
        return self
    def next(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index -1
        return self.data[self.index]

>>> import q
>>> rev = q.Reverse('spam')
>>> iter(rev)
<q.Reverse instance at 0xb71f588c>
>>> for char in rev:
...     print char
... 
m
a
p
s

10. 生成器(Generators)

生成器是建立迭代器的一個簡單而強大的工具。它們像正常函式一樣,只是需要返回資料時使用 yield語句。

# d.py
def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
>>> import d
>>> for char in d.reverse('golf'):
...     print char
... 
f
l
o
g

任何可以使用生成器做的事,都可以使用前一版本的reverse實現,生成器之所以實現緊湊是因為自動建立了 __ iter() 和 next() 方法。