原文作者:Jeff Knupp
class
是Python的基础构建快。它是很多流行的程序和库,以及Python标准库的基础依托。理解类是什么,什么时候使用,以及它们如何有用至关重要,这也是本文的目的。在这个过程中,我们会探讨“面向对象编程”的含义,以及它与Python类之间的联系。
一切都是对象…
class
关键字究竟是什么?跟它基于函数的def
表兄弟类似,它用于定义事物。def
用来定义函数,class
用来定义类。什么是类?就是一个数据和函数(在类中定义时,通常叫做“方法”)的逻辑分组。
“逻辑分组”是什么意思?一个类可以包含我们希望的任何数据和函数(方法)。我们尝试创建事物之间有逻辑联系的类,而不是把随机的事物放在“类”名下面。很多时候,类都是基于真实世界的物体(比如Customer
和Product
)。其它时候,类基于系统中的概念,比如HTTPRequest
和Owner
。
不管怎么样,类是一种建模技术,一种思考程序的方式。当你用这种方式思考和实现你的系统时,被称为使用面向对象编程。“类”和“对象”经常互换使用,但实际上它们并不相同。理解它们是什么和它们是如何工作的关键是理解它们之间的区别。
..所以一切都有一个类?
类可以看做是创建对象的蓝图。当我使用class
关键字定义一个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
class Customer(object)
并没有创建一个新的顾客。我们只是定义了一个Customer
,并不意味着创建
了一个顾客;我们仅仅勾勒出创建Customer
对象的蓝图
。用正确的参数数量(去掉self
,我们马上会讨论)调用类的__init__
方法可以创建一个顾客。
因此,要使用通过class Customer
(用于创建Customer
对象)定义的“蓝图”,可以把类名看做一个函数来调用:jeff = Customer('Jeff Knupp', 1000.0)
。这行代码表示:使用Customer
蓝图创建一个新对象,并把它指向jeff
。
被称为实例的jeff
对象是Customer
类的实现版本。我们调用Customer()
之前,不存在Customer
对象。当然,我们可以创建任意多个Customer
对象。但是不管我们创建多少Customer
实例,仍然只有一个Customer
类。
self
?
对应所有Customer
方法来说,self
参数是什么?当然,它是实例。换一种方式,像withdraw
这样的方法,定义了从某些抽象顾客的账户中取钱的指令。调用jeff.withdraw(1000.0)
把这些指令用在jeff
实例上。
所以,当我们说:def withdraw(self, amount):
,我们的意思是:这是你如何从一个顾客对象(我们称为self
)和一个美元数字(我们称为amount
)取钱。self
是Customer
的实例,在它上面调用withdraw
。这也不是我做类比。jeff.withdraw(1000.0)
只是Customer.withdraw(jeff, 1000.0)
的简写,也是完全有限的代码。
__init__
self
可能对其它方法也有意义,但是__init__
呢?当我们调用__init__
时,我们在创建一个对象的过程中,为什么已经有了self
?尽管不完全适合,Python还是允许我们扩展self
模式到对象的构造。想象jeff = Customer('Jeff Knupp', 1000.0)
等价于jeff = Customer(jeff, 'Jeff Knupp', 1000.0)
;传入的jeff
也是同样的结果。
这就是为什么调用__init__
时,我们通过self.name = name
来初始化对象。记住,因为self
是实例,所以它等价于jeff.name = name
,它等价于jeff.name = 'Jeff Knupp'
。同样的,self.balance = balance
等价于jeff.balance = 1000.0
。这两行代码之后,我们认为Customer
对象已经“初始化”,可以被使用。
完成__init__
后,调用者可以假设对象已经可以使用。也就是,调用jeff = Customer('Jeff Knupp', 1000.0)
后,我们可以在jeff
上调用deposit
和withdraw
;jeff
是一个完全初始化的对象。
我们定义了另外一个稍微不同的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):
"""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
它看起来是一个合理的替代者;在使用实例之前,只需要简单的调用set_balance
。但是,没有一种方式可以告诉调用者这么做。即使我们在文档中说明了,也不能强制调用者在调用jeff.withdraw(100.0)
之前调用jeff.set_balance(1000.0)
。jeff
实例在调用jeff.set_balance
之前没有balance
属性,这意味着对象没有“完全”初始化。
简单来说,不要在__init__
方法之外引入新的属性,否则你会给调用一个没有完全初始化的对象。当然也有例外,但这是一个需要记住的原则。这是对象一致性这个大概念的一部分:不应该有任何一系列的方法调用可能导致对象进入没有意义的状态。
不变性(比如“账户余额总是非负数”)应该在方法进入和退出时都保留。对象不可能通过调用它的方法进入无效状态。不用说,一个对象也应该从一个有效的状态开始,这就是为什么在__init__
方法中初始化所有内容是很重要的。
实例属性和方法
定义在类中的函数称为“方法”。方法可以访问包含在对象实例中的所有数据;它们可以访问和修改之前在self
上设置的任何内容。因为它们使用self
,所以需要使用类的一个实例。基于这个原因,它们通常被称为“实例方法”。
如果有“实例方法”,一定也会有其它类型的方法,对吧?是的,确实有,但这些方法有些深奥。我们会在这里简略的介绍一下,但是可以更深入的研究这些主题。
静态方法
类属性是在类级别上设置的属性,相对的是实例级别。普通属性在__init__
方法中引入,但有些属性适用于所有实例。例如,思考下面Car
对象的定义:
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
不管make
和model
是什么,一辆Car
总是有四个Wheels
。实例方法可以通过跟访问普通属性一样访问这些属性:通过self
(比如,self.wheels
)。
有一种称为静态方法的方法,它们不能访问self
。跟类属性类似,它们不需要实例就能工作。因为实例总是通过self
引用,所以静态方法没有self
参数。
下面是Car
类的一个有效的静态方法:
class Car(object):
...
def make_car_sound():
print 'VRooooommmm!'
不管我们拥有什么类型的汽车,它总是发出相同的声音。为了说明这个方法不应该接收实例作为第一个参数(比如“普通”方法的self
),可以使用@staticmethod
装饰器,把我们的定义变成:
class Car(object):
...
@staticmethod
def make_car_sound():
print 'VRooooommmm!'
类方法
静态方法的一个变种是类方法。它传递类,而不是实例作为第一个参数。它也使用装饰器定义:
class Vehicle(object):
...
@classmethod
def is_motorcycle(cls):
return cls.wheels == 2
现在类方法可能没有太大的意义,但它通常与下一个主题联系在一起:继承。
继承
面向对象编程作为建模工具非常有用,引入继承的概念后,它真正变强大了。
继承是“子”类衍生“父”类的数据和行为的过程。有一个实例可以明确的帮助我们理解。
想象我们经营了一家汽车销售店。我们销售所有类型的车辆,从摩托车到卡车。我们通过价格与竞争对手区分开来。特别是我们如何确定车辆的价格:5000∗一台车辆拥有的车轮数。我们也喜欢回购车辆。我们提供统一的价格−车辆行驶里程的1010,000,汽车是8,000,摩托车是4,000。
如果我们想用面对对象技术为汽车销售店创建一个销售系统,应该怎么做?对象是什么?我们可能有一个Sale
类,一个Customer
类,一个Inventor
类等等,但我们肯定有一个Car
,Truck
和Motorcycle
类。
这些类应该是什么样的?用我们已经学会的知识,以下是Car
类的一种实现:
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)
...
看起来非常合理。当然,类中可能还有其它方法,但我已经展示了两个我们感兴趣的方法:sale_price
和purchase_price
。我们之后会看到为什么这些很重要。
我们已经有了Car
类,也许我们应该创建Truck
类。我们按同样的方式创建:
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)
...
几乎跟Car
类一模一样。编程中最重要的原则之一(通常不只是处理对象时)是“DRY”或者“Don’t Repeat Yourself”。确定无疑,我们在这里重复了。实际上,Car
类和Truck
类只有一个字符不同(除了注释)。
出什么事了?我们哪里做错了?我们的主要问题是我们直奔概念:Car
和Truck
是真实的事物,直觉让有形的对象成为类。但是它们共享这么多数据和功能,似乎我们可以在这里引入一个抽象。没错,它就是Vehicle
。
抽象类
Vehicle
不是真实世界的对象。而是一个概念,它包含某些真实世界中的对象(比如汽车,卡车和摩托车)。我们可以用这个事实来移除重复代码,即每个对象都被看做是一台车辆。通过定义Vehicle
类达到目的:
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)
通过替换class Car(object)
中的object
,我们可以让Car
和Truck
类继承Vehicle
类。括号中的类表示从哪个类继承(object
实际上是“没有继承”。我们一会儿讨论为什么这么写)。
现在我们可以直截了当的定义Car
和Truck
:
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
这样可以工作了,但还有一些问题。首先我们仍然有很多重复的代码。最终我们会处理完所有重复的代码。其次,更大的问题是,我们引入了Vehicle
类,但我们真的允许调用者创建Vehicle
对象(而不是Car
和Truck
)?Vehicle
仅仅是一个概念,不是真实的事物,所以下面代码的意义是:
v = Vehicle(4, 0, 'Honda', 'Accord', 2014, None)
print v.purchase_price()
Vehicle
没有base_sale_price
,只有各个子类(比如Car
和Truck
)有。问题在于Vehicle
应该是一个Abstract Base Class。Abstract Base Class是只可以被继承的类;不能创建ABC的实例。这意味着如果Vehicle
是一个ABC,那么下面的代码就是非法的:
v = Vehicle(4, 0, 'Honda', 'Accord', 2014, None)
禁止这一点是有意义的,因为我们从来不会直接使用Vehicle
。我们只想用它抽取一些通用的数据和行为。我们如何让一个类成为ABC?很简单!abc
模块包括一个称为ABCMeta
的元类。设置一个类的元类为ABCMeta
,并让其中一个方法为虚拟的,就能让类成为一个ABC。ABC规定,虚拟方法必须在子类中存在,但不是必须要实现。例如,Vehicle
类可以如下定义:
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
因为vehicle_type
是一个abstractmethod
,所以我们不能直接创建Vehicle
实例。只要Car
和Truck
从Vehicle
继承,并定义了vehicle_type
,我们就能实例化这些类。
返回Car
类和Truck
类中的重复代码,看看我们是否可以把通用的功能提升到基类Vehicle
中:
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
现在Car
和Truck
类变成:
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'
这完全符合我们的直觉:就我们的系统而言,汽车和卡车之间的唯一区别是基础售价。
定义一个Motocycle
类非常简单:
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
尽管看起来我们用继承处理了重复,但我们真正做的是简单的提供适当级别的抽象。抽象是理解继承的关键。我们已经看到使用继承的一个附带作用是减少重复的代码,但从调用者的角度来看呢?使用继承如何改变代码?
事实证明有一点。想象我们有两个类:Dog
和Person
,我们想写一个函数,它接收任何两种对象类型,并打印该实例是否可以说话(狗不能,人可以)。我们可能这么编写代码:
def can_speak(animal):
if isinstance(animal, Person):
return True
elif isinstance(animal, Dog):
return False
else:
raise RuntimeError('Unknown animal!')
只有两种类型的动物时没问题,但是如何有20种呢,或者200种?那么if...elif
会相当长。
这里关键是can_speak
不应该关心处理的动物类型,动物类本身应该告诉我们它能否说话。通过引入基类Animal
,其中定义can_speak
,可以避免函数的类型检查。只要知道是传进来的是Animal
,确定能否说话很简单:
def can_speak(animal):
return animal.can_speak()
这是因为Person
和Dog
(或者其它任何从Animal
继承的类)遵循Liskov Substitution Principle。这表示我们可以在希望父类(Animal
)的地方,使用子类(比如Person
或Dog
)替换。这听起来很简单,但它是interface的基础。
总结
希望你们学会了什么是Python类,为什么它们很有用,以及如何使用。类和面向对象编程很深奥。确实,它涉及计算机科学的核心。本文不是对类的详细研究,也不应该是你的唯一参考。网络上有数以千计的OOP和类的解释,如果本文对你不合适,搜索会让你找到更适合你的。
一如既往,欢迎在评论中更正和讨论。只要保持礼貌就行。