程序员Python

[译]Python提高:Python类和面向对象编程

2017-09-12  本文已影响760人  lakerszhy

原文作者: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 ClassAbstract 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>,并让其中一个方法为虚拟的,就能让类成为一个ABCABC规定,虚拟方法必须在子类中存在,但不是必须要实现。例如,<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和类的解释,如果本文对你不合适,搜索会让你找到更适合你的。

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

上一篇下一篇

猜你喜欢

热点阅读