Python_類全面解析

、直接動手發表於2020-11-23

類(classes)

在Python中,定義類是通過class關鍵字,class後面緊接著是類名,即Student,類名通常是大寫開頭的單詞,緊接著是(object),表示該類是從哪個類繼承下來的。通常,如果沒有合適的繼承類,就使用object類,這是所有類最終都會繼承的類。

物件導向重要的概念就是類(Class)和例項(Instance),類是抽象的模板,而例項是根據類建立出來的一個個具體的“物件”,每個物件都擁有相同的方法,但各自的資料可能不同。

先回顧下 OOP 的常用術語:

  • 類:對具有相同資料和方法的一組物件的描述或定義。
  • 物件:物件是一個類的例項。
  • 例項(instance):一個物件的例項化實現。
  • 例項屬性(instance attribute):一個物件就是一組屬性的集合。
  • 例項方法(instance method):所有存取或者更新物件某個例項一條或者多條屬性的函式的集合。
  • 類屬性(classattribute):屬於一個類中所有物件的屬性,不會只在某個例項上發生變化
  • 類方法(classmethod):那些無須特定的物件例項就能夠工作的從屬於類的函式。

 另外,Python的類機制使用盡可能少的新語法和語義將類引入語言。Python的類提供了物件導向程式設計語言所有的 標準特性:類繼承機制允許有多個基類,一個派生類可以覆蓋基類中的任何方法,一個方法可以使用相同的名字呼叫 基類中的方法。

Table of Contents

1.名字和物件

物件有其特性,同一個物件可以有多個名字,這與其它語言中的別名很相似。別名有時候像指標,例如將物件當做 函式引數傳遞的時候非常高效,因為只傳遞了指標,這避免了pascal中的兩套引數傳遞機制。

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

在引入類之前,我們講Python的域規則。類的定義巧妙地運用了名稱空間,所以你需要知道域和名稱空間如何工作才能理解發生了什麼。

首先從定義開始。 名稱空間是名字和物件之間的對映。多數名稱空間使用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程式的一個名稱空間可以直接訪問的一個文字範圍,“直接訪問”在這裡的意思時當對一個名字的訪問沒有 字首時,會嘗試在名稱空間內查詢這個名字。 在執行的任意時刻,至少有三個巢狀域,它們有名稱空間可以直接訪問。

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

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

3 初識類

3.1 定義類
class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

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

3.2 類物件
類物件提供兩種操作,屬性引用和例項化。 屬性引用使用標準句法:obj.name. 有效的屬性名是類物件建立時類的名稱空間內的所有名字。 例如下面的類定義中,MyClass.i和MyClass.f都是有效的屬性名。
>>> class MyClass:
...     """A simple example class"""
...     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)
3.3 例項化物件
現在我們可以用例項化的物件做些什麼呢?它唯一可以進行的操作是屬性引用。有兩類有效的屬性名,資料屬性和方法。 資料屬性(data attributes)對應c++中的資料成員,資料屬性無需宣告,第一次給它賦值時就表明了它的存在。 另一種例項化的屬性引用叫做方法(Method).方法是物件內的一個函式。
3.4 方法物件
通常我們呼叫一個方法的方式是:
x.f()

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

>>> class MyClass:
...     """A simple example class"""
...     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'
3.5 漫談
資料屬性如果和方法屬性名稱相同,前者會覆蓋後者。所以為了避免名稱衝突,最好養成一些習慣,比如方法名稱大寫,資料屬性 名稱前加一個短小,唯一的字首。或者資料屬性用名詞,方法屬性用動詞。 資料屬性可以被方法引用,也可以被物件的使用者引用。換句話說,類不能實現為純抽象資料型別。 通常,方法的第一個引數是self.雖然這只是一個習慣用法,但是遵循一些習慣用法會讓你的程式可讀性更強。 函式定義沒有必要非在類裡面,例如:
# 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

一個方法可以通過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']

4.方法

4.1例項方法(instance method)
Python 的例項方法用得最多,也最常見。
class Kls(object):
    def __init__(self, data):
        self.data = data

    def printd(self):
        print(self.data)


ik1 = Kls('leo')
ik2 = Kls('lee')

ik1.printd()
ik2.printd()

輸出:

leo 
lee

上述例子中,printd為一個例項方法。例項方法第一個引數為self,當使用ik1.printd()呼叫例項方法時,例項ik1會傳遞給self引數,這樣self引數就可以引用當前正在呼叫例項方法的例項。利用例項方法的這個特性,上述程式碼正確輸出了兩個例項的成員資料。

4.2類方法
Python 的類方法採用裝飾器@classmethod來定義,我們直接看例子。
class Kls(object):
    num_inst = 0

    def __init__(self):
        Kls.num_inst = Kls.num_inst + 1

    @classmethod
    def get_no_of_instance(cls):
        return cls.num_inst


ik1 = Kls()
ik2 = Kls()

print ik1.get_no_of_instance()
print Kls.get_no_of_instance()

輸出:

2 
2

在上述例子中,我們需要統計類Kls例項的個數,因此定義了一個類變數num_inst來存放例項個數。通過裝飾器@classmethod的使用,方法get_no_of_instance被定義成一個類方法。在呼叫類方法時,Python 會將類(class Kls)傳遞給cls,這樣在get_no_of_instance內部就可以引用類變數num_inst。
由於在呼叫類方法時,只需要將型別本身傳遞給類方法,因此,既可以通過類也可以通過例項來呼叫類方法。

4.3靜態方法
在開發中,我們常常需要定義一些方法,這些方法跟類有關,但在實現時並不需要引用類或者例項,例如,設定環境變數,修改另一個類的變數,等。這個時候,我們可以使用靜態方法。 Python 使用裝飾器@staticmethod來定義一個靜態方法。
IND = 'ON'


class Kls(object):
    def __init__(self, data):
        self.data = data

    @staticmethod
    def checkind():
        return IND == 'ON'

    def do_reset(self):
        if self.checkind():
            print('Reset done for: %s' % self.data)

    def set_db(self):
        if self.checkind():
            print('DB connection made for: %s' % self.data)


ik1 = Kls(24)
ik1.do_reset()
ik1.set_db()

輸出:

Reset done for: 24 
DB connection made for: 24

在程式碼中,我們定義了一個全域性變數IND,由於IND跟類Kls相關,所以我們將方法checkind放置在類Kls中定義。方法checkind只需檢查IND的值,而不需要引用類或者例項,因此,我們將方法checkind定義為靜態方法。
對於靜態方法,Python 並不需要傳遞類或者例項,因此,既可以使用類也可以使用例項來呼叫靜態方法。

4.4例項方法,類方法與靜態方法的區別
我們用程式碼說明例項方法,類方法,靜態方法的區別。注意下述程式碼中方法foo,class_foo,static_foo的定義以及使用。
class Kls(object):
    def foo(self, x):
        print('executing foo(%s,%s)' % (self, x))

    @classmethod
    def class_foo(cls,x):
        print('executing class_foo(%s,%s)' % (cls,x))

    @staticmethod
    def static_foo(x):
        print('executing static_foo(%s)' % x)


ik = Kls()

# 例項方法
ik.foo(1)
print(ik.foo)
print('==========================================')

# 類方法
ik.class_foo(1)
Kls.class_foo(1)
print(ik.class_foo)
print('==========================================')

# 靜態方法
ik.static_foo(1)
Kls.static_foo('hi')
print(ik.static_foo)

輸出:

executing foo(<__main__.Kls object at 0x0551E190>,1)
<bound method Kls.foo of <__main__.Kls object at 0x0551E190>>
==========================================
executing class_foo(<class '__main__.Kls'>,1)
executing class_foo(<class '__main__.Kls'>,1)
<bound method type.class_foo of <class '__main__.Kls'>>
==========================================
executing static_foo(1)
executing static_foo(hi)
<function static_foo at 0x055238B0>

對於例項方法,呼叫時會把例項ik作為第一個引數傳遞給self引數。因此,呼叫ik.foo(1)時輸出了例項ik的地址。

對於類方法,呼叫時會把類Kls作為第一個引數傳遞給cls引數。因此,呼叫ik.class_foo(1)時輸出了Kls型別資訊。
前面提到,可以通過類也可以通過例項來呼叫類方法,在上述程式碼中,我們再一次進行了驗證。

對於靜態方法,呼叫時並不需要傳遞類或者例項。其實,靜態方法很像我們在類外定義的函式,只不過靜態方法可以通過類或者例項來呼叫而已。

值得注意的是,在上述例子中,foo只是個函式,但當呼叫ik.foo的時候我們得到的是一個已經跟例項ik繫結的函式。呼叫foo時需要兩個引數,但呼叫ik.foo時只需要一個引數。foo跟ik進行了繫結,因此,當我們列印ik.foo時,會看到以下輸出 :

<bound method Kls.foo of <__main__.Kls object at 0x0551E190>>

當呼叫ik.class_foo時,由於class_foo是類方法,因此,class_foo跟Kls進行了繫結(而不是跟ik繫結)。當我們列印ik.class_foo時,輸出:

<bound method type.class_foo of <class '__main__.Kls'>>

當呼叫ik.static_foo時,靜態方法並不會與類或者例項繫結,因此,列印ik.static_foo(或者Kls.static_foo)時輸出:

<function static_foo at 0x055238B0>

概括來說,是否與類或者例項進行繫結,這就是例項方法,類方法,靜態方法的區別

我們用程式碼說明例項方法,類方法,靜態方法的區別。

5. 派生

派生類的形式如下:
class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

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

class DerivedClassName(modname.BaseClassName):

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

BaseClassName.methodname(self, arguments)

6 多重繼承

Python 支援有限的多重繼承:
class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

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

7 私有變數和類區域性引用

例項的私有變數只能在物件內部使用,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.

8 結構體

有時候我們可能需要像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

9 異常(Exceptions)也是類

使用者定義的異常也可以用類來表示,使用這種機制可以建立出可擴充套件,層次化的異常。 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的基類.

10 迭代器

現在你可能已經注意到了多數容器物件都可以使用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]

11 生成器(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() 方法。



轉載連結:
link.
link.

相關文章