原文作者:Jeff Knupp
原文鏈接:這里
<font color=red>class
</font>是Python的基礎構建快。它是很多流行的程序和庫,以及Python標準庫的基礎依托。理解類是什么,什么時候使用,以及它們如何有用至關重要,這也是本文的目的。在這個過程中,我們會探討“面向對象編程”的含義,以及它與Python類之間的聯系。
一切都是對象...
<font color=red>class
</font>關鍵字究竟是什么?跟它基于函數的<font color=red>def
</font>表兄弟類似,它用于定義事物。<font color=red>def
</font>用來定義函數,<font color=red>class
</font>用來定義類。什么是類?就是一個數據和函數(在類中定義時,通常叫做“方法”)的邏輯分組。
“邏輯分組”是什么意思?一個類可以包含我們希望的任何數據和函數(方法)。我們嘗試創建事物之間有邏輯聯系的類,而不是把隨機的事物放在“類”名下面。很多時候,類都是基于真實世界的物體(比如<font color=red>Customer
</font>和<font color=red>Product
</font>)。其它時候,類基于系統中的概念,比如<font color=red>HTTPRequest
</font>和<font color=red>Owner
</font>。
不管怎么樣,類是一種建模技術,一種思考程序的方式。當你用這種方式思考和實現你的系統時,被稱為使用面向對象編程。“類”和“對象”經常互換使用,但實際上它們并不相同。理解它們是什么和它們是如何工作的關鍵是理解它們之間的區別。
..所以一切都有一個類?
類可以看做是創建對象的藍圖。當我使用<font color=red>class
</font>關鍵字定義一個Customer
類時,我并沒有真正創建一個顧客。相反,我創建的是構建顧客
對象的說明手冊。讓我們看以下示例代碼:
class Customer(object):
"""A customer of ABC Bank with a checking account. Customers have the
following properties:
Attributes:
name: A string representing the customer's name.
balance: A float tracking the current balance of the customer's account.
"""
def __init__(self, name, balance=0.0):
"""Return a Customer object whose name is *name* and starting
balance is *balance*."""
self.name = name
self.balance = balance
def withdraw(self, amount):
"""Return the balance remaining after withdrawing *amount*
dollars."""
if amount > self.balance:
raise RuntimeError('Amount greater than available balance.')
self.balance -= amount
return self.balance
def deposit(self, amount):
"""Return the balance remaining after depositing *amount*
dollars."""
self.balance += amount
return self.balance
<font color=red>class Customer(object)
</font>并沒有創建一個新的顧客。我們只是定義了一個<font color=red>Customer
</font>,并不意味著創建
了一個顧客;我們僅僅勾勒出創建<font color=red>Customer
</font>對象的藍圖
。用正確的參數數量(去掉<font color=red>self
</font>,我們馬上會討論)調用類的<font color=red>__init__
</font>方法可以創建一個顧客。
因此,要使用通過<font color=red>class Customer
</font>(用于創建<font color=red>Customer
</font>對象)定義的“藍圖”,可以把類名看做一個函數來調用:<font color=red>jeff = Customer('Jeff Knupp', 1000.0)
</font>。這行代碼表示:使用<font color=red>Customer
</font>藍圖創建一個新對象,并把它指向<font color=red>jeff
</font>。
被稱為實例的<font color=red>jeff
</font>對象是<font color=red>Customer
</font>類的實現版本。我們調用<font color=red>Customer()
</font>之前,不存在<font color=red>Customer
</font>對象。當然,我們可以創建任意多個<font color=red>Customer
</font>對象。但是不管我們創建多少<font color=red>Customer
</font>實例,仍然只有一個<font color=red>Customer
</font>類。
<font color=red>self
</font>?
對應所有<font color=red>Customer
</font>方法來說,<font color=red>self
</font>參數是什么?當然,它是實例。換一種方式,像<font color=red>withdraw
</font>這樣的方法,定義了從某些抽象顧客的賬戶中取錢的指令。調用<font color=red>jeff.withdraw(1000.0)
</font>把這些指令用在<font color=red>jeff
</font>實例上。
所以,當我們說:<font color=red>def withdraw(self, amount):
</font>,我們的意思是:這是你如何從一個顧客對象(我們稱為<font color=red>self
</font>)和一個美元數字(我們稱為<font color=red>amount
</font>)取錢。<font color=red>self
</font>是<font color=red>Customer
</font>的實例,在它上面調用<font color=red>withdraw
</font>。這也不是我做類比。<font color=red>jeff.withdraw(1000.0)
</font>只是<font color=red>Customer.withdraw(jeff, 1000.0)
</font>的簡寫,也是完全有限的代碼。
<font color=red>__init__
</font>
<font color=red>self
</font>可能對其它方法也有意義,但是<font color=red>__init__
</font>呢?當我們調用<font color=red>__init__
</font>時,我們在創建一個對象的過程中,為什么已經有了<font color=red>self
</font>?盡管不完全適合,Python還是允許我們擴展<font color=red>self
</font>模式到對象的構造。想象<font color=red>jeff = Customer('Jeff Knupp', 1000.0)
</font>等價于<font color=red>jeff = Customer(jeff, 'Jeff Knupp', 1000.0)
</font>;傳入的<font color=red>jeff
</font>也是同樣的結果。
這就是為什么調用<font color=red>__init__
</font>時,我們通過<font color=red>self.name = name
</font>來初始化對象。記住,因為<font color=red>self
</font>是實例,所以它等價于<font color=red>jeff.name = name
</font>,它等價于<font color=red>jeff.name = 'Jeff Knupp'
</font>。同樣的,<font color=red>self.balance = balance
</font>等價于<font color=red>jeff.balance = 1000.0
</font>。這兩行代碼之后,我們認為<font color=red>Customer
</font>對象已經“初始化”,可以被使用。
完成<font color=red>__init__
</font>后,調用者可以假設對象已經可以使用。也就是,調用<font color=red>jeff = Customer('Jeff Knupp', 1000.0)
</font>后,我們可以在<font color=red>jeff
</font>上調用<font color=red>deposit
</font>和<font color=red>withdraw
</font>;<font color=red>jeff
</font>是一個完全初始化的對象。
我們定義了另外一個稍微不同的<font color=red>Customer
</font>類:
class Customer(object):
"""A customer of ABC Bank with a checking account. Customers have the
following properties:
Attributes:
name: A string representing the customer's name.
balance: A float tracking the current balance of the customer's account.
"""
def __init__(self, name):
"""Return a Customer object whose name is *name*."""
self.name = name
def set_balance(self, balance=0.0):
"""Set the customer's starting balance."""
self.balance = balance
def withdraw(self, amount):
"""Return the balance remaining after withdrawing *amount*
dollars."""
if amount > self.balance:
raise RuntimeError('Amount greater than available balance.')
self.balance -= amount
return self.balance
def deposit(self, amount):
"""Return the balance remaining after depositing *amount*
dollars."""
self.balance += amount
return self.balance
它看起來是一個合理的替代者;在使用實例之前,只需要簡單的調用<font color=red>set_balance
</font>。但是,沒有一種方式可以告訴調用者這么做。即使我們在文檔中說明了,也不能強制調用者在調用<font color=red>jeff.withdraw(100.0)
</font>之前調用<font color=red>jeff.set_balance(1000.0)
</font>。<font color=red>jeff
</font>實例在調用<font color=red>jeff.set_balance
</font>之前沒有<font color=red>balance
</font>屬性,這意味著對象沒有“完全”初始化。
簡單來說,不要在<font color=red>__init__
</font>方法之外引入新的屬性,否則你會給調用一個沒有完全初始化的對象。當然也有例外,但這是一個需要記住的原則。這是對象一致性這個大概念的一部分:不應該有任何一系列的方法調用可能導致對象進入沒有意義的狀態。
不變性(比如“賬戶余額總是非負數”)應該在方法進入和退出時都保留。對象不可能通過調用它的方法進入無效狀態。不用說,一個對象也應該從一個有效的狀態開始,這就是為什么在<font color=red>__init__
</font>方法中初始化所有內容是很重要的。
實例屬性和方法
定義在類中的函數稱為“方法”。方法可以訪問包含在對象實例中的所有數據;它們可以訪問和修改之前在<font color=red>self
</font>上設置的任何內容。因為它們使用<font color=red>self
</font>,所以需要使用類的一個實例。基于這個原因,它們通常被稱為“實例方法”。
如果有“實例方法”,一定也會有其它類型的方法,對吧?是的,確實有,但這些方法有些深奧。我們會在這里簡略的介紹一下,但是可以更深入的研究這些主題。
靜態方法
類屬性是在類級別上設置的屬性,相對的是實例級別。普通屬性在<font color=red>__init__
</font>方法中引入,但有些屬性適用于所有實例。例如,思考下面<font color=red>Car
</font>對象的定義:
class Car(object):
wheels = 4
def __init__(self, make, model):
self.make = make
self.model = model
mustang = Car('Ford', 'Mustang')
print mustang.wheels
# 4
print Car.wheels
# 4
不管<font color=red>make
</font>和<font color=red>model
</font>是什么,一輛<font color=red>Car
</font>總是有四個<font color=red>Wheels
</font>。實例方法可以通過跟訪問普通屬性一樣訪問這些屬性:通過<font color=red>self
</font>(比如,<font color=red>self.wheels
</font>)。
有一種稱為靜態方法的方法,它們不能訪問<font color=red>self
</font>。跟類屬性類似,它們不需要實例就能工作。因為實例總是通過<font color=red>self
</font>引用,所以靜態方法沒有<font color=red>self
</font>參數。
下面是<font color=red>Car
</font>類的一個有效的靜態方法:
class Car(object):
...
def make_car_sound():
print 'VRooooommmm!'
不管我們擁有什么類型的汽車,它總是發出相同的聲音。為了說明這個方法不應該接收實例作為第一個參數(比如“普通”方法的<font color=red>self
</font>),可以使用<font color=red>@staticmethod
</font>裝飾器,把我們的定義變成:
class Car(object):
...
@staticmethod
def make_car_sound():
print 'VRooooommmm!'
類方法
靜態方法的一個變種是類方法。它傳遞類,而不是實例作為第一個參數。它也使用裝飾器定義:
class Vehicle(object):
...
@classmethod
def is_motorcycle(cls):
return cls.wheels == 2
現在類方法可能沒有太大的意義,但它通常與下一個主題聯系在一起:繼承。
繼承
面向對象編程作為建模工具非常有用,引入繼承的概念后,它真正變強大了。
繼承是“子”類衍生“父”類的數據和行為的過程。有一個實例可以明確的幫助我們理解。
想象我們經營了一家汽車銷售店。我們銷售所有類型的車輛,從摩托車到卡車。我們通過價格與競爭對手區分開來。特別是我們如何確定車輛的價格:$5000 * 一臺車輛擁有的車輪數。我們也喜歡回購車輛。我們提供統一的價格 - 車輛行駛里程的10%。卡車的價格是$10,000,汽車是$8,000,摩托車是$4,000。
如果我們想用面對對象技術為汽車銷售店創建一個銷售系統,應該怎么做?對象是什么?我們可能有一個<font color=red>Sale
</font>類,一個<font color=red>Customer
</font>類,一個<font color=red>Inventor
</font>類等等,但我們肯定有一個<font color=red>Car
</font>,<font color=red>Truck
</font>和<font color=red>Motorcycle
</font>類。
這些類應該是什么樣的?用我們已經學會的知識,以下是<font color=red>Car
</font>類的一種實現:
class Car(object):
"""A car for sale by Jeffco Car Dealership.
Attributes:
wheels: An integer representing the number of wheels the car has.
miles: The integral number of miles driven on the car.
make: The make of the car as a string.
model: The model of the car as a string.
year: The integral year the car was built.
sold_on: The date the vehicle was sold.
"""
def __init__(self, wheels, miles, make, model, year, sold_on):
"""Return a new Car object."""
self.wheels = wheels
self.miles = miles
self.make = make
self.model = model
self.year = year
self.sold_on = sold_on
def sale_price(self):
"""Return the sale price for this car as a float amount."""
if self.sold_on is not None:
return 0.0 # Already sold
return 5000.0 * self.wheels
def purchase_price(self):
"""Return the price for which we would pay to purchase the car."""
if self.sold_on is None:
return 0.0 # Not yet sold
return 8000 - (.10 * self.miles)
...
看起來非常合理。當然,類中可能還有其它方法,但我已經展示了兩個我們感興趣的方法:<font color=red>sale_price
</font>和<font color=red>purchase_price
</font>。我們之后會看到為什么這些很重要。
我們已經有了<font color=red>Car
</font>類,也許我們應該創建<font color=red>Truck
</font>類。我們按同樣的方式創建:
class Truck(object):
"""A truck for sale by Jeffco Car Dealership.
Attributes:
wheels: An integer representing the number of wheels the truck has.
miles: The integral number of miles driven on the truck.
make: The make of the truck as a string.
model: The model of the truck as a string.
year: The integral year the truck was built.
sold_on: The date the vehicle was sold.
"""
def __init__(self, wheels, miles, make, model, year, sold_on):
"""Return a new Truck object."""
self.wheels = wheels
self.miles = miles
self.make = make
self.model = model
self.year = year
self.sold_on = sold_on
def sale_price(self):
"""Return the sale price for this truck as a float amount."""
if self.sold_on is not None:
return 0.0 # Already sold
return 5000.0 * self.wheels
def purchase_price(self):
"""Return the price for which we would pay to purchase the truck."""
if self.sold_on is None:
return 0.0 # Not yet sold
return 10000 - (.10 * self.miles)
...
幾乎跟<font color=red>Car
</font>類一模一樣。編程中最重要的原則之一(通常不只是處理對象時)是“DRY”或者“Don't Repeat Yourself”。確定無疑,我們在這里重復了。實際上,<font color=red>Car
</font>類和<font color=red>Truck
</font>類只有一個字符不同(除了注釋)。
出什么事了?我們哪里做錯了?我們的主要問題是我們直奔概念:<font color=red>Car
</font>和<font color=red>Truck
</font>是真實的事物,直覺讓有形的對象成為類。但是它們共享這么多數據和功能,似乎我們可以在這里引入一個抽象。沒錯,它就是<font color=red>Vehicle
</font>。
抽象類
<font color=red>Vehicle
</font>不是真實世界的對象。而是一個概念,它包含某些真實世界中的對象(比如汽車,卡車和摩托車)。我們可以用這個事實來移除重復代碼,即每個對象都被看做是一臺車輛。通過定義<font color=red>Vehicle
</font>類達到目的:
class Vehicle(object):
"""A vehicle for sale by Jeffco Car Dealership.
Attributes:
wheels: An integer representing the number of wheels the vehicle has.
miles: The integral number of miles driven on the vehicle.
make: The make of the vehicle as a string.
model: The model of the vehicle as a string.
year: The integral year the vehicle was built.
sold_on: The date the vehicle was sold.
"""
base_sale_price = 0
def __init__(self, wheels, miles, make, model, year, sold_on):
"""Return a new Vehicle object."""
self.wheels = wheels
self.miles = miles
self.make = make
self.model = model
self.year = year
self.sold_on = sold_on
def sale_price(self):
"""Return the sale price for this vehicle as a float amount."""
if self.sold_on is not None:
return 0.0 # Already sold
return 5000.0 * self.wheels
def purchase_price(self):
"""Return the price for which we would pay to purchase the vehicle."""
if self.sold_on is None:
return 0.0 # Not yet sold
return self.base_sale_price - (.10 * self.miles)
通過替換<font color=red>class Car(object)
</font>中的<font color=red>object
</font>,我們可以讓<font color=red>Car
</font>和<font color=red>Truck
</font>類繼承<font color=red>Vehicle
</font>類。括號中的類表示從哪個類繼承(<font color=red>object
</font>實際上是“沒有繼承”。我們一會兒討論為什么這么寫)。
現在我們可以直截了當的定義<font color=red>Car
</font>和<font color=red>Truck
</font>:
class Car(Vehicle):
def __init__(self, wheels, miles, make, model, year, sold_on):
"""Return a new Car object."""
self.wheels = wheels
self.miles = miles
self.make = make
self.model = model
self.year = year
self.sold_on = sold_on
self.base_sale_price = 8000
class Truck(Vehicle):
def __init__(self, wheels, miles, make, model, year, sold_on):
"""Return a new Truck object."""
self.wheels = wheels
self.miles = miles
self.make = make
self.model = model
self.year = year
self.sold_on = sold_on
self.base_sale_price = 10000
這樣可以工作了,但還有一些問題。首先我們仍然有很多重復的代碼。最終我們會處理完所有重復的代碼。其次,更大的問題是,我們引入了<font color=red>Vehicle
</font>類,但我們真的允許調用者創建<font color=red>Vehicle
</font>對象(而不是<font color=red>Car
</font>和<font color=red>Truck
</font>)?<font color=red>Vehicle
</font>僅僅是一個概念,不是真實的事物,所以下面代碼的意義是:
v = Vehicle(4, 0, 'Honda', 'Accord', 2014, None)
print v.purchase_price()
<font color=red>Vehicle
</font>沒有<font color=red>base_sale_price
</font>,只有各個子類(比如<font color=red>Car
</font>和<font color=red>Truck
</font>)有。問題在于<font color=red>Vehicle
</font>應該是一個Abstract Base Class。Abstract Base Class是只可以被繼承的類;不能創建ABC的實例。這意味著如果<font color=red>Vehicle
</font>是一個ABC,那么下面的代碼就是非法的:
v = Vehicle(4, 0, 'Honda', 'Accord', 2014, None)
禁止這一點是有意義的,因為我們從來不會直接使用<font color=red>Vehicle
</font>。我們只想用它抽取一些通用的數據和行為。我們如何讓一個類成為ABC?很簡單!<font color=red>abc
</font>模塊包括一個稱為<font color=red>ABCMeta
</font>的元類。設置一個類的元類為<font color=red>ABCMeta
</font>,并讓其中一個方法為虛擬的,就能讓類成為一個ABC。ABC規定,虛擬方法必須在子類中存在,但不是必須要實現。例如,<font color=red>Vehicle
</font>類可以如下定義:
from abc import ABCMeta, abstractmethod
class Vehicle(object):
"""A vehicle for sale by Jeffco Car Dealership.
Attributes:
wheels: An integer representing the number of wheels the vehicle has.
miles: The integral number of miles driven on the vehicle.
make: The make of the vehicle as a string.
model: The model of the vehicle as a string.
year: The integral year the vehicle was built.
sold_on: The date the vehicle was sold.
"""
__metaclass__ = ABCMeta
base_sale_price = 0
def sale_price(self):
"""Return the sale price for this vehicle as a float amount."""
if self.sold_on is not None:
return 0.0 # Already sold
return 5000.0 * self.wheels
def purchase_price(self):
"""Return the price for which we would pay to purchase the vehicle."""
if self.sold_on is None:
return 0.0 # Not yet sold
return self.base_sale_price - (.10 * self.miles)
@abstractmethod
def vehicle_type():
""""Return a string representing the type of vehicle this is."""
pass
因為<font color=red>vehicle_type
</font>是一個<font color=red>abstractmethod
</font>,所以我們不能直接創建<font color=red>Vehicle
</font>實例。只要<font color=red>Car
</font>和<font color=red>Truck
</font>從<font color=red>Vehicle
</font>繼承,并定義了<font color=red>vehicle_type
</font>,我們就能實例化這些類。
返回<font color=red>Car
</font>類和<font color=red>Truck
</font>類中的重復代碼,看看我們是否可以把通用的功能提升到基類<font color=red>Vehicle
</font>中:
from abc import ABCMeta, abstractmethod
class Vehicle(object):
"""A vehicle for sale by Jeffco Car Dealership.
Attributes:
wheels: An integer representing the number of wheels the vehicle has.
miles: The integral number of miles driven on the vehicle.
make: The make of the vehicle as a string.
model: The model of the vehicle as a string.
year: The integral year the vehicle was built.
sold_on: The date the vehicle was sold.
"""
__metaclass__ = ABCMeta
base_sale_price = 0
wheels = 0
def __init__(self, miles, make, model, year, sold_on):
self.miles = miles
self.make = make
self.model = model
self.year = year
self.sold_on = sold_on
def sale_price(self):
"""Return the sale price for this vehicle as a float amount."""
if self.sold_on is not None:
return 0.0 # Already sold
return 5000.0 * self.wheels
def purchase_price(self):
"""Return the price for which we would pay to purchase the vehicle."""
if self.sold_on is None:
return 0.0 # Not yet sold
return self.base_sale_price - (.10 * self.miles)
@abstractmethod
def vehicle_type(self):
""""Return a string representing the type of vehicle this is."""
pass
現在<font color=red>Car
</font>和<font color=red>Truck
</font>類變成:
class Car(Vehicle):
"""A car for sale by Jeffco Car Dealership."""
base_sale_price = 8000
wheels = 4
def vehicle_type(self):
""""Return a string representing the type of vehicle this is."""
return 'car'
class Truck(Vehicle):
"""A truck for sale by Jeffco Car Dealership."""
base_sale_price = 10000
wheels = 4
def vehicle_type(self):
""""Return a string representing the type of vehicle this is."""
return 'truck'
這完全符合我們的直覺:就我們的系統而言,汽車和卡車之間的唯一區別是基礎售價。
定義一個<font color=red>Motocycle
</font>類非常簡單:
class Motorcycle(Vehicle):
"""A motorcycle for sale by Jeffco Car Dealership."""
base_sale_price = 4000
wheels = 2
def vehicle_type(self):
""""Return a string representing the type of vehicle this is."""
return 'motorcycle'
繼承和LSP
盡管看起來我們用繼承處理了重復,但我們真正做的是簡單的提供適當級別的抽象。抽象是理解繼承的關鍵。我們已經看到使用繼承的一個附帶作用是減少重復的代碼,但從調用者的角度來看呢?使用繼承如何改變代碼?
事實證明有一點。想象我們有兩個類:<font color=red>Dog
</font>和<font color=red>Person
</font>,我們想寫一個函數,它接收任何兩種對象類型,并打印該實例是否可以說話(狗不能,人可以)。我們可能這么編寫代碼:
def can_speak(animal):
if isinstance(animal, Person):
return True
elif isinstance(animal, Dog):
return False
else:
raise RuntimeError('Unknown animal!')
只有兩種類型的動物時沒問題,但是如何有20種呢,或者200種?那么<font color=red>if...elif
</font>會相當長。
這里關鍵是<font color=red>can_speak
</font>不應該關心處理的動物類型,動物類本身應該告訴我們它能否說話。通過引入基類<font color=red>Animal
</font>,其中定義<font color=red>can_speak
</font>,可以避免函數的類型檢查。只要知道是傳進來的是<font color=red>Animal
</font>,確定能否說話很簡單:
def can_speak(animal):
return animal.can_speak()
這是因為<font color=red>Person
</font>和<font color=red>Dog
</font>(或者其它任何從<font color=red>Animal
</font>繼承的類)遵循Liskov Substitution Principle。這表示我們可以在希望父類(<font color=red>Animal
</font>)的地方,使用子類(比如<font color=red>Person
</font>或<font color=red>Dog
</font>)替換。這聽起來很簡單,但它是interface的基礎。
總結
希望你們學會了什么是Python類,為什么它們很有用,以及如何使用。類和面向對象編程很深奧。確實,它涉及計算機科學的核心。本文不是對類的詳細研究,也不應該是你的唯一參考。網絡上有數以千計的OOP和類的解釋,如果本文對你不合適,搜索會讓你找到更適合你的。
一如既往,歡迎在評論中更正和討論。只要保持禮貌就行。