原文作者: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和类的解释,如果本文对你不合适,搜索会让你找到更适合你的。
一如既往,欢迎在评论中更正和讨论。只要保持礼貌就行。