1. 程式人生 > >Python面向物件程式設計——類的學習

Python面向物件程式設計——類的學習

面向物件程式設計

面向物件程式設計——Object Oriented Programming,簡稱OOP,是一種程式設計思想。OOP把物件作為程式的基本單元,一個物件包含了資料和操作資料的函式。

    面向過程的程式設計把計算機程式視為一系列的命令集合,即一組函式的順序執行。為了簡化程式設計,面向過程把函式繼續切分為子函式,即把大塊函式通過切割成小塊函式來降低系統的複雜度。而面向物件的程式設計把計算機程式視為一組物件的集合,而每個物件都可以接收其他物件發過來的訊息,並處理這些訊息,計算機程式的執行就是一系列訊息在各個物件之間傳遞。

    在Python中,所有資料型別都可以視為物件,當然也可以自定義物件。自定義的物件資料型別就是

面向物件中的類(Class)的概念。

    通過例子來說明面向過程和麵向物件在程式流程上的不同之處

    面向過程:處理學生的成績表,為了表示一個學生的成績,面向過程的程式可以用一個dict表示並通過函式實現列印成績:

std1 = { 'name': 'Michael', 'score': 98 }
std2 = { 'name': 'Bob', 'score': 81 }

def print_score(std):   
     print('%s: %s' % (std['name'], std['score']))
    面向物件:首選思考的不是程式的執行流程,而是Student這種資料型別應該被視為一個物件,這個物件擁有name和score這兩個屬性(Property)。如果要列印一個學生的成績,首先必須創建出這個學生對應的物件,然後,給物件發一個print_score訊息,讓物件自己把自己的資料打印出來。
class Student(object):

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

    def print_score(self):
        print('%s: %s' % (self.name, self.score))
給物件發訊息實際上就是呼叫物件對應的關聯函式,我們稱之為物件的方法(Method)。面向物件的程式寫出來就像這樣:
bart = Student('Bart Simpson', 59)
bart.print_score()

    小結:  面向物件的設計思想是抽象出Class,根據Class建立Instance。  面向物件的抽象程度又比函式要高,因為一個Class既包含資料,又包含操作資料的方法。資料封裝、繼承和多型是面向物件的三大特點

一、類和例項

1.定義類:

class Student(object):
     pass

 使用class定義一個類,類名通常是大寫開頭的單詞,(object),表示該類是從哪個類繼承下來的,在類裡面複製的變數是類的變數,即類的屬性

2.類的例項化:

>>>bart = Student()
>>>bart
<__main__.Student object at 0x10a67a590>
>>> Student
<class '__main__.Student'>

    左邊建立一個變數,右邊寫上類的名稱 ,稱之為類的例項化,被例項化後的物件稱之為例項(instance)。變數bart指向的就是一個Student的例項,後面的0x10a67a590是記憶體地址,每個object的地址都不一樣,而Student本身則是一個類。

3.“魔術方法”__init__( ) :

    __init__( )如果在類裡定義了,在建立例項的時候它就能幫你自動地處理很多事情——比如新增例項屬性,__init__( )是initialize(初始化)的縮寫,因此即使在建立例項的時候不去引用_init_()方法,其中的命令也會先被自動地執行。

class Student(object):

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

    def print_score(self):
        print('%s: %s' % (self.name, self.score))

    除了必寫的self引數之外,__init__( )同樣可以有自己的引數,在例項化的時候往類後面的括號中放進引數,相應的所有引數都會傳遞到__init__( )方法中,和函式的引數用法完全相同。

class CocaCola:
    formula = ['caffeine','sugar','water','soda']
    def __init__(self,logo_name):
        self.local_logo = logo_name #左邊是變數作為類的屬性,右邊是傳入的這個引數作為變數
    def drink(self):
        print('Energy!')
coke = CocaCola('可口可樂')
coke.local_logo
>>> 可口可樂

二、資料封裝

    面向物件程式設計的一個重要特點就是資料封裝。在上面的Student類中,每個例項就擁有各自的name和score這些資料。要訪問這些資料,可以直接在Student類的內部定義訪問資料的函式,這樣,就把“資料”給封裝起來了。這些封裝資料的函式是和Student類本身是關聯起來的,我們稱之為類的方法。

    類是建立例項的模板,而例項則是一個一個具體的物件,各個例項擁有的資料都互相獨立,互不影響;方法就是與例項繫結的函式,和普通函式不同,方法可以直接訪問例項的資料;通過在例項上呼叫方法,我們就直接操作了物件內部的資料,但無需知道方法內部的實現細節。

1.類屬性引用:(公有、私有)

公有

>>> bart = Student('Bart Simpson', 98)
>>> bart.score
98
>>> bart.score = 59
>>> bart.score
59

    類的屬性會被該類的例項共享,類的屬性與正常變數並無區別

私有

     如果要讓內部屬性不被外部訪問,可以把屬性的名稱前加上兩個下劃線__,在Python中,例項的變數名如果以__開頭,就變成了一個私有變數(private),只有內部可以訪問,外部不能訪問
class Student(object):

    def __init__(self, name, score):
        self.__name = name
        self.__score = score

    def print_score(self):
        print('%s: %s' % (self.__name, self.__score))
>> bart = Student('Bart Simpson', 98)
>>> bart.__name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'
    外部程式碼要獲取私有屬性name和score,給Student類增加get_name和get_score方法,修改score增加set_score方法,增加方法的原因是在方法中,可以對引數做檢查,避免傳入無效的引數:
class Student(object):
    ...
    def get_name(self):
        return self.__name
    def get_score(self):
        return self.__score
    def set_score(self, score):
        if 0 <= score <= 100:
            self.__score = score
        else:
            raise ValueError('bad score')
    在Python中,變數名類似__xxx__的,也就是以雙下劃線開頭,並且以雙下劃線結尾的,是特殊變數,特殊變數是可以直接訪問的,不是private變數,所以,不能用__name__、__score__這樣的變數名。

    以一個下劃線開頭的例項變數名,比如_name,這樣的例項變數外部是可以訪問的,但是,按照約定,當看到這樣的變數時,意思就是,“雖然我可以被訪問,但是,請把我視為私有變數,不要隨意訪問”。

2.例項屬性(Instance Atrribute)

class CocaCola:
    formula = ['caffeine','sugar','water','soda']
coke_for_China = CocaCola()
coke_for_China.local_logo = '可口可樂' #建立例項屬性
print(coke_for_China.local_logo) #列印例項屬性引用結果

    在建立了類之後,通過object.new_attr的形式進行一個賦值,即可得到新的例項的變數,專有術語即例項屬性,用方式上,引用例項屬性和引用類屬性完全一樣,但是二者卻有本質上的差異

3.例項方法(Instance Method):

    類的例項可以引用屬性,也可以使用方法(函式)

class CocaCola:
    formula = ['caffeine','sugar','water','soda']
    def drink(self):
        print('Energy!')
coke = CocaCola()
coke.drink()
>>> Energy!

    self這個引數名稱實際上是可以任意修改的,但是按照Python的規矩,還是統一使用self

給屬性指定預設值

類中的每個屬性都必須有初始值,哪怕這個值是0或空字串。在有些情況下,如設定預設值時,在方法__init__() 內指定這種初始值是可行的;如果你對某個屬性這樣做了,就無需包含為它提供初始值的形參。

class Car():
      """一次模擬汽車的簡單嘗試"""
❶     def __init__(self, make, model, year):
          """初始化描述汽車的屬性"""
          self.make = make
          self.model = model
          self.year = year
          self.odometer_reading = 0
❷     def get_descriptive_name(self):
           """返回整潔的描述性資訊"""
          long_name = str(self.year) + ' ' + self.make + ' ' + self.model
          return long_name.title()
      def read_odometer(self):
          """列印一條指出汽車裡程的訊息"""
          print("This car has " + str(self.odometer_reading) + " miles on it.")

  my_new_car = Car('audi', 'a4', 2016)
  print(my_new_car.get_descriptive_name())
  my_new_car.read_odometer()

修改屬性的值

可以以三種不同的方式修改屬性的值:直接通過例項進行修改;通過方法進行設定;通過方法進行遞增(增加特定的值)

1.直接修改屬性的值

要修改屬性的值,最簡單的方式是通過例項直接訪問它

class Car():
      --snip--

  my_new_car = Car('audi', 'a4', 2016)
  print(my_new_car.get_descriptive_name())

❶ my_new_car.odometer_reading = 23
  my_new_car.read_odometer()

2. 通過方法修改屬性的值

如果有更新屬性的方法,就無需直接訪問屬性,而可將值傳遞給一個方法,由它在內部進行更新

class Car():
      --snip--

❶     def update_odometer(self, mileage):
          """將里程錶讀數設定為指定的值"""
          self.odometer_reading = mileage

  my_new_car = Car('audi', 'a4', 2016)
  print(my_new_car.get_descriptive_name())

❷ my_new_car.update_odometer(23)
  my_new_car.read_odometer()

3. 通過方法對屬性的值進行遞增

有時候需要將屬性值遞增特定的量,而不是將其設定為全新的值。假設我們購買了一輛二手車,且從購買到登記期間增加了100英里的里程,下面的方法讓我們能夠傳遞這個增量,並相應地增加里程表讀數:

 class Car():
      --snip--

      def update_odometer(self, mileage):
          --snip--

❶     def increment_odometer(self, miles):
          """將里程錶讀數增加指定的量"""
          self.odometer_reading += miles

❷ my_used_car = Car('subaru', 'outback', 2013)
  print(my_used_car.get_descriptive_name())

❸ my_used_car.update_odometer(23500)
  my_used_car.read_odometer()

❹ my_used_car.increment_odometer(100)
  my_used_car.read_odometer()

三、繼承和多型

1.類的繼承(Inheritance)

    在OOP程式設計中,當定義一個class的時候,可以從某個現有的class繼承,新的class稱為子類(Subclass),而被繼承的class稱為基類、父類或超類(Base class、Super class)。

class Animal(object):
    def run(self):
        print('Animal is running...')
class Dog(Animal):
    pass
class Cat(Animal):
    pass

    新的類Dog後面的括號中放入Animal,表示這個類繼承於Animal這個父類,Dog成為Animal子類。類中的變數和方法可以完全被子類繼承

dog = Dog()
dog.run()
>>>Animal is running...

    但如需有特殊的改動也可以進行覆蓋(Override)

class Dog(Animal):
    def run(self):
        print('Dog is running...')
    def eat(self):
        print('Eating meat...')
    當子類和父類都存在相同的run()方法時,我們說,子類的run()覆蓋了父類的run(),在程式碼執行的時候,總是會呼叫子類的run()。這樣,我們就獲得了繼承的另一個好處:多型。

    多型真正的威力:呼叫方只管呼叫,不管細節,而當我們新增一種Animal的子類時,只要確保run()方法編寫正確,不用管原來的程式碼是如何呼叫的。這就是著名的“開閉”原則:
    對擴充套件開放:允許新增Animal子類;

    對修改封閉:不需要修改依賴Animal型別的函式。(即函式作引數)

練習:

❶ class Car():
      """一次模擬汽車的簡單嘗試"""

      def __init__(self, make, model, year):
          self.make = make
          self.model = model
          self.year = year
          self.odometer_reading = 0

      def get_descriptive_name(self):
          long_name = str(self.year) + ' ' + self.make + ' ' + self.model
          return long_name.title()

      def read_odometer(self):
          print("This car has " + str(self.odometer_reading) + " miles on it.")

      def update_odometer(self, mileage):

          if mileage >= self.odometer_reading:
              self.odometer_reading = mileage
          else:
              print("You can't roll back an odometer!")

      def increment_odometer(self, miles):
          self.odometer_reading += miles

❷ class ElectricCar(Car):
      """電動汽車的獨特之處"""


❸     def __init__(self, make, model, year):
          """初始化父類的屬性"""
❹         super().__init__(make, model, year)


❺ my_tesla = ElectricCar('tesla', 'model s', 2016)
  print(my_tesla.get_descriptive_name())

    首先是Car 類的程式碼(見❶)。建立子類時,父類必須包含在當前檔案中,且位於子類前面。在❷處,我們定義了子類ElectricCar 。定義子類時,必須在括號內指定父類的名稱。方法__init__() 接受建立Car 例項所需的資訊(見❸)。❹處的super()是一個特殊函式,幫助Python將父類和子類關聯起來。這行程式碼讓Python呼叫ElectricCar 的父類的方法__init__() ,讓ElectricCar 例項包含父類的所有屬性。父類也稱為超類 (superclass),名稱super因此而得名。

Python 2.7中的繼承

    在Python 2.7中,繼承語法稍有不同,ElectricCar 類的定義類似於下面這樣:
class Car(object):
    def __init__(self, make, model, year):
        --snip--


class ElectricCar(Car):
    def __init__(self, make, model, year):
        super(ElectricCar, self).__init__(make, model, year)

    函式super() 需要兩個實參:子類名和物件self 。為幫助Python將父類和子類關聯起來,這些實參必不可少。另外,在Python 2.7中使用繼承時,務必在定義父類時在括號內指定object 。

2.給子類定義屬性和方法

    讓一個類繼承另一個類後,可新增區分子類和父類所需的新屬性和方法。

  class Car():
      --snip--

  class ElectricCar(Car):
      """Represent aspects of a car, specific to electric vehicles."""

      def __init__(self, make, model, year):
          """
          電動汽車的獨特之處
          初始化父類的屬性,再初始化電動汽車特有的屬性
          """
          super().__init__(make, model, year)
❶         self.battery_size = 70

❷     def describe_battery(self):
          """列印一條描述電瓶容量的訊息"""
          print("This car has a " + str(self.battery_size) + "-kWh battery.")

3.重寫父類的方法

    對於父類的方法,只要不符合子類模擬的實物的行為,都可對其進行重寫。為此,可在子類中定義一個方法,與要重寫的父類方法同名。這樣,Python將不會考慮這個父類方法,而只關注你在子類中定義的相應方法。假設Car 類有一個名為fill_gas_tank() 的方法,它對全電動汽車來說毫無意義,因此你可能想重寫它。下面演示了一種重寫方式:

class ElectricCar(Car):
    --snip--


    def fill_gas_tank():
        """電動汽車沒有油箱"""
        print("This car doesn't need a gas tank!")

4.將例項用作屬性

使用程式碼模擬實物時,你可能會發現自己給類新增的細節越來越多:屬性和方法清單以及檔案都越來越長。在這種情況下,可能需要將類的一部分作為一個獨立的類提取出來。你可以將大型類拆分成多個協同工作的小類。不斷給ElectricCar類新增細節時,我們可能會發現其中包含很多專門針對汽車電瓶的屬性和方法。在這種情況下,我們可將這些屬性和方法提取出來,放到另一個名為Battery的類中,並將一個Battery例項用作ElectricCar 類的一個屬性:

class Car():
      --snip--

❶ class Battery():
      """一次模擬電動汽車電瓶的簡單嘗試"""

❷     def __init__(self, battery_size=70):
          """初始化電瓶的屬性"""
          self.battery_size = battery_size

❸     def describe_battery(self):
          """列印一條描述電瓶容量的訊息"""
          print("This car has a " + str(self.battery_size) + "-kWh battery.")


  class ElectricCar(Car):
      """電動汽車的獨特之處"""

      def __init__(self, make, model, year):
          """
          初始化父類的屬性,再初始化電動汽車特有的屬性
          """
          super().__init__(make, model, year)
❹         self.battery = Battery()

四、獲取物件資訊

1.使用type()

判斷一個物件是否是函式可以使用types模組中定義的常量:

import types
>>> def fn():
...     pass
...
>>> type(fn)==types.FunctionType
True
>>> type(abs)==types.BuiltinFunctionType
True
>>> type(lambda x: x)==types.LambdaType
True
>>> type((x for x in range(10)))==types.GeneratorType
True

2.使用isinstance()

    isinstance()判斷的是一個物件是否是該型別本身,或者位於該型別的父繼承鏈上。

    繼承關係是:object -> Animal -> Dog -> Husky

>>> a = Animal()
>>> d = Dog()
>>> h = Husky()
>>> isinstance(h, Husky)
True
>>> isinstance(h, Dog)
True
>>> isinstance(d, Husky)
False
3.使用dir()
    獲得一個物件的所有屬性和方法,可以使用dir()函式,它返回一個包含字串的list,比如,獲得一個str物件的所有屬性和方法:
>>> dir('ABC')
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 
    類似__xxx__的屬性和方法在Python中都有特殊用途,比如__len__方法返回長度。在Python中,呼叫len()函式獲取一個物件的長度,實際上,在len()函式內部,會自動呼叫該物件的__len__()方法,下面的程式碼是等價的:
>>> len('ABC')
3
>>> 'ABC'.__len__()
3
    配合getattr()、setattr()以及hasattr(),我們可以直接操作一個物件的狀態:
>>> class MyObject(object):
...     def __init__(self):
...         self.x = 9
...     def power(self):
...         return self.x * self.x
...
>>> obj = MyObject()
    測試該物件的屬性:
>>> hasattr(obj, 'x') # obj例項有屬性'x'嗎?
True
>>> obj.x
9
>>> hasattr(obj, 'y') # 有屬性'y'嗎?
False
>>> setattr(obj, 'y', 19) # 設定一個屬性'y'
>>> hasattr(obj, 'y') # 有屬性'y'嗎?
True
>>> getattr(obj, 'y') # 獲取屬性'y'
19
>>> obj.y # 獲取屬性'y'
19
    如果試圖獲取不存在的屬性,會丟擲AttributeError的錯誤:
>>> getattr(obj, 'z') # 獲取屬性'z'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'z'
    可以傳入一個default引數,如果屬性不存在,就返回預設值:
>>> getattr(obj, 'z', 404) # 獲取屬性'z',如果不存在,返回預設值404
404

   小結:通過內建的一系列函式,我們可以對任意一個Python物件進行剖析,拿到其內部的資料。要注意的是,只有在不知道物件資訊的時候,我們才會去獲取物件資訊。

    如果可以直接寫:

sum = obj.x + obj.y

    就不要寫:

sum = getattr(obj, 'x') + getattr(obj, 'y')

    正確的用法的例子如下:

def readImage(fp):
    if hasattr(fp, 'read'):
        return readData(fp)
    return None
    假設我們希望從檔案流fp中讀取影象,我們首先要判斷該fp物件是否存在read方法,如果存在,則該物件是一個流,如果不存在,則無法讀取。hasattr()就派上了用場。

五、從模組匯入類

1從一個模組中匯入一個或多個類,一個模組可以儲存多個類

    可根據需要在程式檔案中匯入任意數量的類。如果我們要在同一個程式中建立普通汽車和電動汽車,就需要將Car 和ElectricCar 類都匯入:

❶ from car import Car, ElectricCar #car模組是car.py,Car、ElectriCar類

❷ my_beetle = Car('volkswagen', 'beetle', 2016)#例項化
  print(my_beetle.get_descriptive_name())

❸ my_tesla = ElectricCar('tesla', 'roadster', 2016)
  print(my_tesla.get_descriptive_name())

2.匯入整個模組

    你還可以匯入整個模組,再使用句點表示法訪問需要的類。這種匯入方法很簡單,程式碼也易於閱讀。由於建立類例項的程式碼都包含模組名,因此不會與當前檔案使用的任何名稱發生衝突。

❶ import car

❷ my_beetle = car.Car('volkswagen', 'beetle', 2016)
  print(my_beetle.get_descriptive_name())

❸ my_tesla = car.ElectricCar('tesla', 'roadster', 2016)
  print(my_tesla.get_descriptive_name())

3不推薦使用匯入模組中所有類

from module_name import *

    其原因有二。首先,如果只要看一下檔案開頭的import 語句,就能清楚地知道程式使用了哪些類,將大有裨益;但這種匯入方式沒有明確地指出你使用了模組中的哪些類。這種匯入方式還可能引發名稱方面的困惑。如果你不小心匯入了一個與程式檔案中其他東西同名的類,將引發難以診斷的錯誤。

    需要從一個模組中匯入很多類時,最好匯入整個模組,並使用

module_name.class_name

語法來訪問類。這樣做時,雖然檔案開頭並沒有列出用到的所有類,但你清楚地知道在程式的哪些地方使用了匯入的模組;你還避免了匯入模組中的每個類可能引發的名稱衝突。

4在一個模組中匯入另一個模組

有時候,需要將類分散到多個模組中,以免模組太大,或在同一個模組中儲存不相關的類。將類儲存在多個模組中時,你可能會發現一個模組中的類依賴於另一個模組中的類。在這種情況下,可在前一個模組中匯入必要的類。

❶ from car import Car
  from electric_car import ElectricCar

  my_beetle = Car('volkswagen', 'beetle', 2016)
  print(my_beetle.get_descriptive_name())

  my_tesla = ElectricCar('tesla', 'roadster', 2016)
  print(my_tesla.get_descriptive_name())

類編碼風格

1.類名應採用駝峰命名法 ,即將類名中的每個單詞的首字母都大寫,而不使用下劃線。例項名和模組名都採用小寫格式,並在單詞之間加上下劃線。

2.對於每個類,都應緊跟在類定義後面包含一個文件字串。這種文件字串簡要地描述類的功能,並遵循編寫函數的文件字串時採用的格式約定。每個模組也都應包含一個文件字串,對其中的類可用於做什麼進行描述。

3.可使用空行來組織程式碼,但不要濫用。在類中,可使用一個空行來分隔方法;而在模組中,可使用兩個空行來分隔類。

4.需要同時匯入標準庫中的模組和你編寫的模組時,先編寫匯入標準庫模組的import 語句,再新增一個空行,然後編寫匯入你自己編寫的模組的import 語句。在包含多條import 語句的程式中,這種做法讓人更容易明白程式使用的各個模組都來自何方。

疑問:

1、類屬性被重新賦值,是否會影響到類屬性的引用?<<<42

class TestA:
attr = 1
obj_a = TestA()
TestA.attr = 42
print(obj_a.attr)

2、例項屬性被重新賦值,是否會影響到類屬性的引用?<<<1

class TestA:
attr = 1
obj_a = TestA()
obj_b = TestA()
obj_a.attr = 42
print(obj_b.attr)

3、類屬性例項屬性具有相同名稱,那麼 . 後面引用的將會是什麼?<<<42

class TestA:
attr = 1
def __init__(self):
    self.attr = 42
obj_a = TestA()
print(obj_a.attr)

__dict__ 是一個類的特殊屬性,它是一個字典,用於儲存類或者例項的屬性。即使不去定義,也會存在於每一個類中,是預設隱藏的。在問題3中新增以下兩行程式碼:

print(TestA.__dict__)
print(obj_a.__dict__)
>>> {‘__module__': '__main__', '__doc__':
None, '__dict__': <attribute '__dict__' of
'TestA' objects>, '__init__': <function
TestA.__init__ at 0x1007fc7b8>, 'attr': 1,
'__weakref__': <attribute '__weakref__' of
'TestA' objects>}
>>> {'attr': 42}

類TestA和類的例項obj_a各自擁有各自的attr屬性,是完全獨立的Python中屬性的引用機制是自外而內的,當你建立了一個例項之後,準備開始引用屬性,這時候編譯器會先搜尋該例項是否擁有該屬性,如果有,則引用;如果沒有,將搜尋這個例項所屬的類是否有這個屬性,如果有,則引用,沒有則報錯


類的擴充套件理解

obj1 = 1
obj2 = 'String!'
obj3 = []
obj4 = {}
print(type(obj1),type(obj2),type(obj3),type(obj4))

Python中任何種類的物件都是類的例項,上面的型別被稱作 內建型別,它們並不需要像上面一樣例項化如果你安裝了Beautifulsoup4第三方庫,可以試著這樣:

from bs4 import Beautifulsoup
soup = BeautifulSoup
print(type(soup))

然後按住ctrl點選Beautifulsoup檢視soup物件的完整型定義。