python基礎學習(四)---面向對象編程

面向對象編程

面向對象編程的三個基礎概念是數據封裝(類)、繼承和多態,在這三個記住概念的基礎上,進一步擴展出更加高級的多重繼承、定製類和元類等概念,這些概念和相關的功能使得python稱爲一種強大的面向對象的高級動態語言。

數據封裝

面向對象編程中最重要的概念就是類和實例,類是對象的抽象的概念,而實例是對象的具象的結果。python中通過class關鍵字定義一個類,

class person(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def show(self):
        print("name: %s, age: %s" % (self.name, self.age))

laowang = person("laowang", 20)
laowang .show()
___________
輸出:
name: laowang , age: 20

在類中爲了代碼安全性的要求,有些屬性和方法不應該被外部直接獲取到,需要對這些屬性或方法設置訪問限制,這一點和其他面向對象編程語言是一樣的,即屬性和方法有privatepublic的區分。在python不使用這兩個關鍵字來設置訪問權限,而是直接在屬性名或函數名之前添加兩個下劃線__來表示這個屬性是私有的屬性,不允許外部直接修改,如果需要修改該屬性,可以增加一個專門的函數用來修改該屬性。

class person(object):
    def __init__(self, name, age):
        self.__name = name
        self.__age = age
    def show(self):
        print("name: %s, age: %s" % (self.__name, self.__age))
    def set_age(self, age):
        self.__age = age

qq = person("laowang", 20)
qq.show()
qq.set_age(90)
qq.show()
____________
輸出:
name: laowang, age: 20
name: laowang, age: 90

設置的訪問限制的屬性無法通過類似qq.__age的形式直接訪問,因爲在解釋器中它的名稱已經被改變爲qq._person__age,通過這一變量可以訪問到該屬性,但是並不建議這麼做,因爲不同版本的解釋器的修改方法可能不一致,這會導致代碼的不兼容,同時這麼做也是不安全的。
如果在設置的訪問限制之後,再直接使用__age強制的修改屬性的內容,此時其實是新建了一個類的屬性,原先類中的__age其實並未改變。

class person(object):
    def __init__(self, name, age):
        self.__name = name
        self.__age = age
    def show(self):
        print("name: %s, age: %s" % (self.__name, self.__age))
    def set_age(self, age):
        self.__age = age

qq = person("laowang", 20)
qq.show()
qq.__age = 50
qq.__age
qq.show()
qq.set_age(90)
qq.show()
————————————————————
輸出:
name: laowang, age: 20
name: laowang, age: 20
name: laowang, age: 90

繼承和多態

當我們新建一個類時,可以從一個現有的類上繼承,這個新建的類被稱爲子類,這個被繼承的類被稱爲子類的基類、父類或超類。
繼承可以獲得父類的全部功能,包括屬性和方法,同時可以在此基礎上進行添加新的屬性和方法,或者重寫原有的方法。同時,由於繼承存在層層傳遞的關係,子類的實例,也是父類的實例,也是父類的父類的實例,以此類推,直到最上層的那個類,這也是爲什麼子類可以使用父類以及父類的父類中所定義的屬性和方法,這也是由繼承所導致的多態性,一個類可以具有包括其自身及所有父類的。
同時在使用子類,一定是先使用子類所定義的方法和屬性,如果沒有,纔會去依次向上追溯尋找可用的屬性或方法,如果直到最終的根類object都沒有,纔會爆錯。

#!/usr/bin/python
# -*- encoding:utf-8 -*-

class person(object):
    def __init__(self, name, age):
        self.__name = name
        self.__age = age
    def show(self):
        print("name: %s, age: %s" % (self.__name, self.__age))
    def set_age(self, age):
        self.__age = age

class doctor(person):
    def set_major(self, major):
        self.__major = major
    def show_major(self):
        print("major : %s" % self.__major)

laowang = doctor("laowang", 20)
laowang.show()
laowang.set_major("Pediatrics")
laowang.show_major()
print(isinstance(laowang, doctor))
print(isinstance(laowang, person))
_________________________
輸出:
name: laowang, age: 20
major : Pediatrics
True
True

多態的好處在於,如果一個函數的輸入可以是多種子類,但如果這些子類具有一個公共的父類,那麼就可以在定義是時將輸入的參數定義爲該父類,在使用時可以輸入其各自子類,而不用關注它具體的類型(注意在函數內不應使用只有某一個子類才具有的屬性或函數),在他具體執行的時候,纔會根據它具體的類型,執行對應的函數。

#!/usr/bin/python
# -*- encoding:utf-8 -*-

class person(object):
    def show(self):
        print("this is person speaking")

class doctor(person):
    def show(self):
        print("this is doctor speaking")

def show(person):
    person.show()

show(person())
show(doctor())
——————————————————————————
輸出:
this is person speaking
this is doctor speaking

動態語言特性

對於上述情況,甚至我們不需要嚴格要求輸入的類是定義時要求的類的子類,只需要傳入的類具有show方法即可,這和JAVA這種靜態語言有很大不同,JAVA要求定義時如果是要求傳入一個類,那麼在使用時比如傳入該種類或其子類,而python作爲一門動態語言可以使用上述這種被稱爲鴨子模型。比如,很多對象只要有read方法,都可以被視爲file-like object,而很多函數的傳入參數就是file-like object類型,此時你可以不必傳入文件對象,而是直接傳入實現了read方法的對象即可。

作爲動態語言,python具有另一個特性,就是可以直接給實例綁定新特性,新特性如果和類已有的特性重名,由於實例特性的優先級較高,在直接訪問時會覆蓋掉類的特性,但是類的特性依然存在。所以,在給實例添加新特性時,最好不要使用和已有屬性相同的名字,不然會導致調用的混亂。

>>> class Chinese(object):
...     country = "China"                                                                                                                                                
... 
>>> laowang = Chinese()
>>> print(laowang.country)
China
>>> laowang.country = 'PRC'    #修改實例的country特性                                                                                                                                    
>>> print(laowang.country)     #實例的country被修改                                                                                                                                          
PRC
>>> print(Chinese.country)      #但是類的country值沒有被修改                                                                                                                                         
China
>>> del laowang.country         #刪除實例的country特性
>>> print(laowang.country)      #類的country特性仍保留
China
>>> laowang.hometown = "taiwan" #添加新的實例特性
>>> print(laowang.hometown)     #新的實例特性可以正常使用
taiwan

同樣的,我們可以給一個實例綁定一個新的方法,這裏需要使用types模塊中的MethodType方法,

>>> def set_sex(self, sex):
...     self.sex = sex
... 
>>> from types import MethodType
>>> laowang.set_sex = MethodType(set_sex, laowang)
>>> laowang.set_sex("male")
>>> laowang.sex
'male'

如果想給所有的實例都綁定新方法,則需要直接給類綁定新方法

>>> Chinese.set_sex = set_sex

但是如果這樣隨意給實例或類添加新的特性,代碼的安全性可能得不到保證,所以python提供了__slots__變量,在定義函數的時候通過它來限定實例能添加的屬性。

>>> class Chinese(object):
...     __slots__ = ('country', 'hometown')
... 
>>> laowang = Chinese()
>>> laowang.country = 'China'
>>> laowang.hometown = 'taiwan'
>>> laowang.sex = 'male'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Chinese' object has no attribute 'sex'
Error in sys.excepthook:
..........
Original exception was:
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Chinese' object has no attribute 'sex'
>>> 

同時,該限制只在該類中起作用,在其子類中並不起作用,子類但如果子類也使用了__slots__變量,那麼就將接收父類__slots__的限制,此時子類實例可以添加的屬性就是子類和父類__slots__指定內容的並集。

>>> class taiwanren(Chinese):
...     pass
... 
>>> jojo = taiwanren()
>>> jojo.sex = 'female'
>>> class taiwanren(Chinese):
...     __slots__ = ()
... 
>>> jj = taiwanren()
>>> jj.sex = 'male'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'taiwanren' object has no attribute 'sex'
Error in sys.excepthook:
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/apport_python_hook.py", line 53, in apport_excepthook
    if not enabled():
  File "/usr/lib/python3/dist-packages/apport_python_hook.py", line 28, in enabled
    return re.search('^\s*enabled\s*=\s*0\s*$', conf, re.M) is None
AttributeError: module 're' has no attribute 'search'

Original exception was:
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'taiwanren' object has no attribute 'sex'

@property

在自定義類時,如果我們想給一個屬性賦值,可以直接調用class.age = 20,但這樣會直接把age屬性暴露給外部,而且如果想要在賦值的同時對傳入的參數進行檢查,就必須使用set_age這樣的方法,但是這樣的方法使用起來比較麻煩,python提供了一種在直接給屬性賦值的同時進行參數檢查的方法,即@property,如下代碼所示的age屬性。

#!/usr/bin/python
# -*- encoding:utf-8 -*-

class person(object):
    @property
    def age(self):
        return self.__age

    @age.setter
    def age(self, age):
        if not isinstance(age, int):
            raise ValueError('age must be an integer')
        if age < 0 or age > 120:
            raise ValueError('invalid age, beyond [0, 120]')
        self.__age = age

pp = person()
pp.age = 30
print(pp.age)
pp.age = 190
——————————
輸出:
30
Traceback (most recent call last):
  File "proper.py", line 21, in <module>
    pp.age = 190
  File "proper.py", line 14, in age
    raise ValueError('invalid age, beyond [0, 120]')
ValueError: invalid age, beyond [0, 120]

同時,如果想要給一個屬性設置爲只讀的,那麼只要在類中只給它創建getter的方法,即直接的@property方法,而不創建setter的方法,如下所示的sex屬性。

#!/usr/bin/python
# -*- encoding:utf-8 -*-

class person(object):

    def __init__(self, sex):
        self.__sex = sex

    @property
    def age(self):
        return self.__age

    @age.setter
    def age(self, age):
        if not isinstance(age, int):
            raise ValueError('age must be an integer')
        if age < 0 or age > 120:
            raise ValueError('invalid age, beyond [0, 120]')
        self.__age = age

    @property
    def sex(self):
        return self.__sex 

pp = person('female')
pp.age = 30
print('person age:%d, sex:%s' % (pp.age, pp.sex))
pp.sex = 'male'
——————————————
輸出:
person age:30, sex:female
Traceback (most recent call last):
  File "proper.py", line 28, in <module>
    pp.sex = 'male'
AttributeError: can't set attribute

多重繼承

繼承可以使子類直接獲得父類的所有功能,並且在此基礎上擴展父類的功能。但是正如類的概念來自真實世界中的對象,每個對象都可以有不同的分類,比如catdogbatparrot,既可以按照能飛不能飛來分類,也可以按照哺乳動物和非哺乳動物來分類,如果按照簡單的單一繼承關係,我們需要很複雜的層次關係來實現這種邏輯,所以可以使用多重繼承,來讓一個子類獲得其所有相關父類的繼承關係,這樣在我們設計類時,只需要按照大類設計所有的基本的類,隨後讓子類直接多重繼承其所有所歸屬的大類。爲了

#!/usr/bin/python
# -*- coding: utf-8 -*-

class animal(object):
    pass
class mammal(animal):
    pass
class unmammal(animal):
    pass
class runnable(animal):
    pass
class flyable(animal):
    pass

class dog(mammal, runnable):
    print("I am a dog")

定製類

在上文中我們看到,使用__slots__可以限定類可以添加的屬性額範圍,這一形如__xxxx__的變量或函數名在定義類時一般也是有特殊的,比如__len()__方法使得類可以作用於len()函數,__str()__函數讓類在被print直接打印時,輸出函數制定的字符串,__repr()__函數的作用和__str__類似,但是他會打印一些用於調試服務的信息。

枚舉類

在python中,每一組數據結構都被抽象成了一個類,因此自定義的數據可以在這些基本數據結構的基礎上繼承並使用其提供的豐富的接口,一個典型的應用就是在創建枚舉類時。
使用python自帶的枚舉類型,首先要引入枚舉模塊,然後就可以直接創建自己的枚舉類型,隨後可以直接通過week.Mon直接引用一個常量,或者使用for循環枚舉它所有成員。

>>> from enum import Enum
>>> week = Enum('week', ('Mon', 'Tues', 'Wed', 'Thur', 'Fri', 'Sat', 'Sun'))
>>> week.Tues
<week.Tues: 2>
>>> week['Mon']
<week.Mon: 1>
>>> for name,member in week.__members__.items():
...     print(name,  '-->', member, ',', member.value)
... 
('Mon', '-->', <week.Mon: 1>, ',', 1)
('Tues', '-->', <week.Tues: 2>, ',', 2)
('Wed', '-->', <week.Wed: 3>, ',', 3)
('Thur', '-->', <week.Thur: 4>, ',', 4)
('Fri', '-->', <week.Fri: 5>, ',', 5)
('Sat', '-->', <week.Sat: 6>, ',', 6)
('Sun', '-->', <week.Sun: 7>, ',', 7)

value是自動分配給成員的int性常量,默認從1開始,如果想要自定義更復雜的枚舉類型,可以從Enum派生自定義的類,在類的前面加上@unique裝飾器用於檢查成員沒有重複。

元類

在上文中我們說到,python是一種動態語言,而動態語言和靜態語言最大的不同之處在於,函數和類的定義不是在編譯時定義的,而是在運行時動態創建的。type函數可以查看一個類型或變量的類型,如果輸入一個class,那麼他的類型就是type,如果輸入一個類型的實例,即一個變量,那麼他的類型就是該class
但是type()還可以在運行時動態的創建class,而無需在編譯之前通過class來定義。

>>> def foo(self, name='laowang'):
...     print('hello, i am %s.' % name)
... 
>>> person = type('person', (object,), dict(hello=foo))
>>> lw = person()
>>> lw.hello()
hello, i am laowang.
>>> print(type(person))
<type 'type'>
>>> print(type(lw))
<class '__main__.person'>

注意到,使用type()創建一個新的class,要依次傳入3個參數:

  1. class的名稱
  2. 繼承的父類集合,注意如果要繼承多個父類,要使用tuple的寫法,如果是繼承單個父類,要注意單個tuple的寫法(object,)
  3. class的方法名稱和已有的函數的綁定,使用dict的寫法,左側是要綁定的類的方法名稱,右側是已有的函數名稱
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章