改进您的 Python: Python 类和面向对象编程
改进您的 Python: Python 类和面向对象编程
注:本周的每一天,我都会重新发布我最受欢迎的帖子之一。我希望第一次错过它们的人现在会发现它们很有用。这篇关于“类”和面向对象编程的帖子一直是我最受欢迎的帖子。光是这个帖子,我就收到了上百封邮件。许多人发现这是对 Python 和 OOP 的简单介绍。
class是 Python 中的一个基本构件。它不仅是许多流行程序和库的基础,也是 Python 标准库的基础。理解什么是类,何时使用它们,以及它们如何有用是至关重要的,也是本文的目标。在这个过程中,我们将探索术语面向对象 编程 的含义以及它如何与 Python 类联系在一起。
一切都是物体…
class关键字到底是用来做什么的?就像它基于功能的表亲def一样,它关注事物的定义。def用于定义一个函数,class用于定义一个类。什么是阶级?只是数据和函数的逻辑分组(当在类中定义时,后者经常被称为“方法”)。
我们所说的“逻辑分组”是什么意思?嗯,一个类可以包含我们想要的任何数据,并且可以有我们想要的任何函数(方法)附加到它上面。我们试图创建事物之间有逻辑联系的类,而不是简单地将随机的事物放在一起命名为“类”。很多时候,类是基于现实世界中的对象的(比如Customer或者Product)。其他时候,类是基于我们系统中的概念,比如HTTPRequest或Owner。
不管怎样,类是一种建模技术;一种思考程序的方式。当你以这种方式思考和实现你的系统时,你被认为是在执行面向对象的编程。“类”和“对象”是经常互换使用的词,但它们实际上不是一回事。理解它们的不同之处是理解它们是什么以及它们如何工作的关键。
..所以万物都有类?
类可以被认为是创建对象的蓝图。当我使用class关键字定义一个客户类时,我实际上并没有创建一个客户。相反,我创建的是一种构造“客户”对象的指导手册。让我们看看下面的示例代码:
**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对象的蓝图。为此,我们用适当数量的参数调用该类的__init__方法(减去self,我们稍后会讲到)。
因此,为了使用我们通过定义class Customer(用于创建Customer对象)创建的“蓝图”,我们几乎像调用函数一样调用类名:jeff = Customer('Jeff Knupp', 1000.0)。这一行简单地说“使用Customer蓝图为我创建一个新对象,我称之为jeff”
被称为实例的jeff 对象,是Customer 类的实现版本。在我们调用Customer()之前,没有Customer对象存在。当然,我们可以创建尽可能多的Customer对象。然而,仍然只有一个Customer 类,不管我们创建了多少个类的实例。
self?
那么所有Customer方法的self参数是怎么回事呢?这是什么?为什么,这是实例,当然!换句话说,像withdraw这样的方法定义了从某个抽象客户账户中取钱的指令。调用jeff.withdraw(100.0)将那些使用的指令放到 *jeff* 实例上。
所以当我们说def withdraw(self, amount):时,我们是在说,“这是你如何从一个客户对象(我们称之为self)和一个美元数字(我们称之为amount)中取钱。self是被调用withdraw的Customer的实例。这也不是我在做类比。jeff.withdraw(100.0)只是Customer.withdraw(jeff, 100.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__
在__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.set_balance之前jeff实例甚至都没有一个 balance 属性,这意味着对象还没有被“完全”初始化。
经验法则是,不要在__init__方法之外给引入新的属性,否则你已经给了调用者一个没有完全初始化的对象。当然,也有例外,但这是一个需要牢记的好原则。这是更大的对象一致性概念的一部分:不应该有任何一系列的方法调用会导致对象进入没有意义的状态。
不变量(比如,“balance 应该总是一个非负数”)应该在进入和退出方法时都成立。一个对象不可能仅仅通过调用它的方法就进入无效状态。不言而喻,一个对象也应该在有效状态下启动,这就是为什么在方法中初始化所有东西是很重要的。
实例属性和方法
在类中定义的函数称为“方法”。方法可以访问对象实例中包含的所有数据;他们可以访问和修改之前在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*
一个Car总是有四个wheels,不管是make还是model。实例方法可以像访问常规属性一样访问这些属性:通过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 美元 x 一辆车的车轮数量。我们也喜欢回购我们的汽车。我们提供统一费率——车辆行驶里程的 10%。对于卡车,费率为 10,000 美元。对于汽车,8000 美元。对于摩托车,4000 美元。
如果我们想用面向对象的技术为我们的经销商创建一个销售系统,我们该怎么做呢?对象会是什么?我们可能有一个Sale类、一个Customer类、一个Inventory类等等,但是我们几乎肯定会有一个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类?让我们遵循我们为 car 所做的相同模式:
**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)
...
哇哦。那就是几乎和一模一样的汽车类。编程最重要的规则之一(一般来说,不仅仅是在处理对象时)是“干”或“T21”on tREPE atYyourself。我们肯定在这里重复了一遍。事实上,Car和Truck类的区别仅仅在于一个字符(除了注释)。
那么是什么原因呢?我们哪里出错了?我们的主要问题是我们直接进入了具体的领域:Car s 和Truck s 是真实的东西,作为类具有直观意义的有形对象。然而,它们共享如此多的共同数据和功能,似乎必须有一个我们可以在这里引入的抽象。的确有:s 的概念。
抽象类
一个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 s)?一个Vehicle只是一个概念,不是一个真实的东西,那么下面说的是什么意思:
v = Vehicle(4, 0, 'Honda', 'Accord', 2014, None)
print v.purchase_price()
一个Vehicle没有一个base_sale_price,只有像Car和Truck这样的单个子类有。问题是Vehicle实际上应该是一个抽象基类。抽象基类是只能从其继承的类;您不能创建 ABC 的实例。这意味着,如果Vehicle是一个 ABC,以下是非法的:
v = Vehicle(4, 0, 'Honda', 'Accord', 2014, None)
不允许这样做是有意义的,因为我们从来没有打算让车辆被直接使用。我们只是想用它来抽象出一些常见的数据和行为。那么我们如何让一个类成为 ABC 呢?简单!abc模块包含一个名为ABCMeta的元类(元类有点超出了本文的范围)。将一个类的元类设置为ABCMeta并使其方法之一为虚拟的会使其成为一个 ABC。虚拟方法是 ABC 说必须存在于子类中的方法,但不一定要实际实现。例如,车辆类别可以定义如下:
**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'
这完全符合我们的直觉:就我们的系统而言,轿车和卡车之间的唯一区别是基本销售价格。定义一个Motorcycle类也同样简单:
**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'
继承和长期服务协议
尽管看起来我们使用继承来消除重复,但我们真正做的只是提供适当的抽象层次。而抽象是理解继承的关键。我们已经看到了使用继承的一个副作用是我们减少了重复的代码,但是从调用者的角度来看呢?使用继承如何改变代码?
事实证明,相当多。假设我们有两个类,Dog和Person,我们想要编写一个函数,它接受任一类型的对象,并打印出所讨论的实例是否能说话(狗不能,人可以)。我们可以编写如下代码:
**def** can_speak(animal):
**if** isinstance(animal, Person):
**return** True
**elif** isinstance(animal, Dog):
**return** False
**else**:
**raise** **RuntimeError**('Unknown animal!')
当我们只有两种动物时,这是可行的,但是如果我们有二十种,或者两百种动物呢?这个链条会变得很长。
这里的关键见解是can_speak不应该关心它在处理什么类型的动物,动物类本身应该告诉我们它是否会说话。通过引入一个定义了can_speak的公共基类Animal,我们减轻了它的类型检查负担。现在,只要它知道传入的是一个Animal,确定它是否能说话就很简单了:
**def** can_speak(animal):
**return** animal.can_speak()
这是因为Person和Dog(以及我们从Animal派生的任何其他类)遵循利斯科夫替换原则。这表明我们应该能够在任何需要父类(Animal)的地方使用子类(比如Person或Dog),一切都会很好。这听起来很简单,但是它是我们将在以后的文章中讨论的强大概念的基础:接口。
摘要
希望您已经学习了很多关于 Python 类是什么、为什么它们有用以及如何使用它们的知识。类和面向对象编程的主题非常深奥。事实上,它们触及了计算机科学的核心。这篇文章并不是对类的详尽研究,也不应该是你唯一的参考。网上有数以千计的关于 OOP 和类的解释,所以如果你没有找到一个合适的,搜索一下肯定会发现一个更适合你的。
一如既往,欢迎评论中的更正和争论。尽量保持文明。
Jeff Knupp 于 2017 年 3 月 27 日发布
原载于 2017 年 3 月 27 日 jeffknupp.com。
黑客中午是黑客如何开始他们的下午。我们是 T21 家庭的一员。我们现在接受投稿并乐意讨论广告&赞助机会。
要了解更多信息,请阅读我们的“关于”页面、喜欢/在脸书上给我们发消息,或者简单地发送 tweet/DM @HackerNoon。