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對象的完整型定義。







發佈了35 篇原創文章 · 獲贊 21 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章