原文作者:Jeff Knupp

class是Python的基础构建快。它是很多流行的程序和库,以及Python标准库的基础依托。理解类是什么,什么时候使用,以及它们如何有用至关重要,这也是本文的目的。在这个过程中,我们会探讨“面向对象编程”的含义,以及它与Python类之间的联系。

一切都是对象…

class关键字究竟是什么?跟它基于函数的def表兄弟类似,它用于定义事物。def用来定义函数,class用来定义。什么是类?就是一个数据和函数(在类中定义时,通常叫做“方法”)的逻辑分组。

“逻辑分组”是什么意思?一个类可以包含我们希望的任何数据和函数(方法)。我们尝试创建事物之间有逻辑联系的类,而不是把随机的事物放在“类”名下面。很多时候,类都是基于真实世界的物体(比如CustomerProduct)。其它时候,类基于系统中的概念,比如HTTPRequestOwner

不管怎么样,类是一种建模技术,一种思考程序的方式。当你用这种方式思考和实现你的系统时,被称为使用面向对象编程。“类”和“对象”经常互换使用,但实际上它们并不相同。理解它们是什么和它们是如何工作的关键是理解它们之间的区别。

..所以一切都有一个类?

类可以看做是创建对象的蓝图。当我使用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)取钱。selfCustomer实例,在它上面调用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上调用depositwithdrawjeff是一个完全初始化的对象。

我们定义了另外一个稍微不同的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

不管makemodel是什么,一辆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类等等,但我们肯定有一个CarTruckMotorcycle类。

这些类应该是什么样的?用我们已经学会的知识,以下是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_pricepurchase_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类只有一个字符不同(除了注释)。

出什么事了?我们哪里做错了?我们的主要问题是我们直奔概念:CarTruck是真实的事物,直觉让有形的对象成为类。但是它们共享这么多数据和功能,似乎我们可以在这里引入一个抽象。没错,它就是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,我们可以让CarTruck继承Vehicle类。括号中的类表示从哪个类继承(object实际上是“没有继承”。我们一会儿讨论为什么这么写)。

现在我们可以直截了当的定义CarTruck

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对象(而不是CarTruck)?Vehicle仅仅是一个概念,不是真实的事物,所以下面代码的意义是:

v = Vehicle(4, 0, 'Honda', 'Accord', 2014, None)
print v.purchase_price()

Vehicle没有base_sale_price,只有各个子类(比如CarTruck)有。问题在于Vehicle应该是一个Abstract Base ClassAbstract Base Class是只可以被继承的类;不能创建ABC的实例。这意味着如果Vehicle是一个ABC,那么下面的代码就是非法的:

v = Vehicle(4, 0, 'Honda', 'Accord', 2014, None)

禁止这一点是有意义的,因为我们从来不会直接使用Vehicle。我们只想用它抽取一些通用的数据和行为。我们如何让一个类成为ABC?很简单!abc模块包括一个称为ABCMeta的元类。设置一个类的元类为ABCMeta,并让其中一个方法为虚拟的,就能让类成为一个ABCABC规定,虚拟方法必须在子类中存在,但不是必须要实现。例如,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实例。只要CarTruckVehicle继承,定义了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

现在CarTruck类变成:

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

尽管看起来我们用继承处理了重复,但我们真正做的是简单的提供适当级别的抽象。抽象是理解继承的关键。我们已经看到使用继承的一个附带作用是减少重复的代码,但从调用者的角度来看呢?使用继承如何改变代码?

事实证明有一点。想象我们有两个类:DogPerson,我们想写一个函数,它接收任何两种对象类型,并打印该实例是否可以说话(狗不能,人可以)。我们可能这么编写代码:

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()

这是因为PersonDog(或者其它任何从Animal继承的类)遵循Liskov Substitution Principle。这表示我们可以在希望父类(Animal)的地方,使用子类(比如PersonDog)替换。这听起来很简单,但它是interface的基础。

总结

希望你们学会了什么是Python类,为什么它们很有用,以及如何使用。类和面向对象编程很深奥。确实,它涉及计算机科学的核心。本文不是对类的详细研究,也不应该是你的唯一参考。网络上有数以千计的OOP和类的解释,如果本文对你不合适,搜索会让你找到更适合你的。

一如既往,欢迎在评论中更正和讨论。只要保持礼貌就行。