面向对象编程

到目前为止,在我们编写的所有程序中,主要使用函数---也就是处理数据的代码块来设计我们的程序,这叫做面向过程的编程方式。还有一种方式来组织你的程序,是将数据和函数组合起来打包到称为对象的东西里面,这叫做面向对象编程技术。在大多数情况下,你可以使用面向过程的编程,但是当写大型程序或者遇到了一些更加适合这种方法的时候,你可以使用面向对象的编程技术。

类和对象是面向对象编程的两个主要概念。一个创建一个新的类型,而对象就是类的一个实例。例如,你可以有一个int的类型(类),而所有的存储整数的变量是int类的一个实例(对象)。

静态语言的程序员应该注意

注意整型被看待为一个int类的对象。这一点与 C++ 和 Java ( 早于 1.5 版本)不同。在这些语言中,整型被看成一种基本数据类型。

关于int类的更多细节,请看help(int)。

C#和Java 1.5程序员将发现这和装箱和拆封的概念相似。

对象可以使用属于对象的普通变量存储数据。属于一个对象或类的变量被称为字段。对象也可以通过拥有属于类的函数实现一定的功能。这样的函数被称为类的方法,这个术语是很重要的,因为它帮助我们区分函数和变量哪些是独立的,那些是属于一个类或对象的。总体而言,这些字段和方法可以被称为类的属性

字段有两种类型,他们可以属于每一个类的实例(也就是对象),也可以属于类本身。它们分别被称为实例变量类变量

要创建一个类使用class的关键字,类的字段和方法在一个缩进的代码块中。

self

类的方法与普通的函数相比只有一个区别 - 他们在入口参数表的第一个位置必须有一个额外的形式参数。但是当你调用这个方法的时候,你不需要为这个参数赋予任何一个值,Python 会提供给它。这个特别的参数指向对象本身,约定它的名字叫做self.

尽管你可以给这个参数起任何一个名字,但是这里强烈推荐使用self —— 任何其他的名字绝对会引起歧义。使用一个标准的名字有许多优点 - 如果你使用 self ,任何人阅读你的程序都会马上理解它,甚至一些特定的集成开发环境(IDE,Integrated Development Environments)还可以给你提供额外的帮助。

C++/Java/C#程序员要注意

在Python中,self相当于C++中的指针this、Java和C#中的this引用。

你一定很想知道Python怎样给self赋值,为什么你不需要给它一个值。一个例子会使这个清楚。假设,你有一个称为MyClass的类和这个类的实例称为myobject。当你调用这个对象的方法myobject.method(arg1, arg2)时,Python将自动转换成MyClass.method(myobject, arg1, arg2)--这是关于self的所有特殊之处。

你一定好奇Python是如何给self赋值的,以及为什么你不必给它赋值。一个例子将会把这些问题说明清楚。假设你有一个类叫做MyClass以及这个类的一个对象叫做myobject。当你需要这样调用这个对象的方法的时候:myobject.method(arg1, arg2),这个语句会被Python自动的转换成MyClass.method(myobject, arg1, arg2)这样的形式 —— 这就是self特殊的地方。

这也意味着如果你有一个不声明任何形式参数的方法,却仍然有一个入口参数 —— self

最简单的类可能如下列代码所示(保存为文件oop_simplestclass.py)。

class Person:
    pass # 一个空的代码块

p = Person()
print(p)

输出:

C:\> python3 simplestclass.py
<__main__.Person object at 0x000001DEE25BC2C8>

它是如何工作的:

我们使用class语句和类名创建了一个类。在这之后跟着一个代码块形成了类的主体。在这个例子中,我们使用pass语句声明了一个空的代码块。

之后,我们使用类的名字和一对括号创建了一个类的对象/实例(我们将在下一节学习更多的例子)。我们通过简单地打印变量p的方法确认这个变量类型。结果证明这是__main__模块中Person类的一个对象。

注意这个对象在内存中的地址也被显示出来。这个地址可能在你的电脑上有一个不同的值,这是由于Python只要找到空闲的内存空间就会在此处存储这个对象。

方法

我们已经讨论过了,类和对象可以拥有一些成员函数,它们都有一个额外的self参数。现在我们来看一个例子(保存为文件oop_method.py)。

例子(保存为 oop_method.py):

class Person:
    def say_hi(self):
        print('嗨,你好吗?')

p = Person()
p.say_hi()
# 上面这两行也可写成Person().say_hi()

输出:

C:\> python oop_method.py
嗨,你好吗?

它是如何工作的:

现在我们具体的看一下self是如何工作的。注意到在say_hi方法中不包含任何参数,却在方法定义的时候仍然有一个self参数。

__init__ 方法

对Python类来说,许多方法名有特殊的含义。现在,我们来考察一个重要的__init__方法。

__init__方法将在类的对象被初始化(也就是创建)的时候自动被调用。这个方法将按照你的要求初始化对象(例如:给对象传递初始值)。请注意这个名字的开头和结束都是双下划线。

例子 (保存为 oop_init.py):

class Person:
    def __init__(self, name):
        self.name = name

    def say_hi(self):
        print('嗨,我的名字是', self.name)

p = Person('Swaroop')
p.say_hi()
# 以上两行也可以写成 Person('Swaroop').sayHi()

输出:

C:\> python class_init.py
嗨,我的名字是 Swaroop

它是如何工作的:

最重要的是。请注意。我们没有显式地调用 __init__ 方法,而是当创建类的一个实例时,通过在类名称后的括号内传递参数,这是该方法的特殊意义。

现在,我们可以在我们的方法中使用self.name字段了,在say_hi方法中已经做了演示。

这里,我们定义了__init__方法。这个方法除了通常的self变量之外,还有一个参数name。 这里我们创建了一个新的名为name的字段。注意这里有两个不同的变量却都被叫做 name。这是没有问题的,因为带点的标记self.name表示有一个叫做“name”的字段是这个类的一部分,而另外一个name是一个局部变量。这里我们显式地指出使用哪个变量,因此没有任何冲突。

当新建一个新的Person类的实例p的时候,我们通过调用类名的方式来创建这个新的实例,在紧跟着的括号中填入初始化参数: p = Person('Swaroop') 。

我们没有显式的调用__init__这个方法,这是这个方法特殊之处。

正如say_hi方法所示的,现在在我们的方法中可以使用self.name这个字段了。

类变量和对象变量

我们已经讨论了关于类和对象中函数的部分(即方法),现在让我们来学习关于数据的部分。数据的部分(即字段)并不是什么特别的东西,只是一些绑定到类或者对象命名空间的普通的变量。这意味着这些变量只在和这些类和对象有关的上下文中有效。这就是为什么他们被称作命名空间

有两种类型的字段–类变量和对象变量。这是通过他们是属于类还是属于对象这一点来区分的。

类变量是共享的 – 他们可以通过所有这个类的对象来访问。类变量只有一份拷贝,这意味着当一个对象改变了一个类变量的时候,改变将发生在所有这个类的对象中。

对象变量属于每一个对象(实例)自身。在这种情况下,每一个对象都有属于它自己的字段(在不同的对象中,这些变量不是共享的,它们也并不相关,仅仅是名称相同。为了便于理解我们举个例子(保存到文件oop_objvar.py):

class Robot:
    """表示人一机器人,有一个名字。"""

    # 一个类变量,数机器人的数量
    population = 0

    def __init__(self, name):
        """初始化数据。"""
        self.name = name
        print("(初始化 {})".format(self.name))

        # 当创建一个人时,机器人人口加1
        Robot.population += 1

    def __del__(self):
        """我将要死了。"""
        print("{0} 正在被毁!".format(self.name))

        Robot.population -= 1

        if Robot.population == 0:
            print("{}是最后一个。".format(self.name))
        else:
            print("还有{:d}机器人在工作。".format(Robot.population))

    def say_hi(self):
        """机器人问候。

        是的,它们能做作那个。"""
        print("你好,我的主人叫我".format(self.name))

    @classmethod
    def how_many(cls):
        """打印当前人口。"""
        print("我们有{:d}个机器人。".format(cls.population))

droid1 = Robot('R2-D2')
droid1.say_hi()
Robot.how_many()

droid2 = Robot('C-3PO')
droid2.say_hi()
Robot.how_many()

print("\n机器人在这能做一些工作。\n")

print("机器人已经完成了它们的工作,因此,让我们销毁它们。")
droid1.die()
droid2.die()

Robot.how_many()

输出:

C:\> python objvar.py
(初始化 R2-D2)
你好,我的主人叫我
我们有1个机器人。
(初始化 C-3PO)
你好,我的主人叫我
我们有2个机器人。

机器人在这能做一些工作。

机器人已经完成了它们的工作,因此,让我们销毁它们。
R2-D2 正在被毁!
还有1机器人在工作。
C-3PO 正在被毁!
C-3PO是最后一个。
我们有0个机器人。

它是如何工作的:

这是一个很长的例子,但有助于展示类和对象变量的特性。在这里,population 属于Robot类,因此是一个类变量。name变量属于对象(使用self分配),因此是一个对象变量。

由此,我们可以推测出population类变量应当用Robot.population来访问,而非self.population;可以推测在对象的方法之中,对象变量name应当使用self.name来访问。请记住这个类变量和对象变量之间这一个简单的区别。也请记住一个对象变量与一个类变量名字相同时,类变量将被隐藏。

除了Robot.population之外,我们还可以通过self.__class__.population来访问这个类变量,因为每一个对象都通过self.__class__属性指向自己的类。

how_many实际上是一个属于类的方法,而非属于对象的方法,这意味着我们可以使用classmethod或者staticmethod来定义它。这取决于我们是否需要知道是哪个类。因此既然我们想要声明一个类变量,让我们使用classmethod吧。

我们使用一个装饰器来标记how_many方法,并将其作为一个类方法。

我们可以把装饰器想象成为一个包装函数的快捷方式(一个包裹着另外一个函数的函数,因此可以在内部函数调用之前及之后做一些事情),因此使用@classmethod装饰器和如下调用等价:

how_many = classmethod(how_many)

我们注意到__init__方法被用作初始化一个Robot实例,并给这个机器人取一个名字。在这个方法之中,我们每获得一个新的机器人,就使得population增加 1 。此外,还注意到self.name变量的值会因对象的不同而不同,这就是对象变量的特征。

请记住,你只能通过self来指向同一个对象的变量和方法。这被称为属性引用(attribute reference) 。

在这个程序中,我们还可以看到文档字符串(docstrings)在类和方法值中的使用。在运行时我们可以通过Robot.__doc__来访问类的文档字符串以及通过Robot.say_hi.__doc__来访问方法的文档字符串。

die方法中,我们简单的将Robot.population减少 1 。

所有的类成员都是公共的。只有一种情况除外:如果你使用双下划线前缀(例如__privatevar)时,Python会使用命名修饰(name-mangling) 作用于这个变量,并使其变为私有变量。

因此,只在对象和类中使用的任何变量,首先应该以一个下划线开始,其他所有的名字都是公共的,可以被其他类和对象访问。请记住这只是约定而非Python强制规定(使用双下划线除外)。

C++/Java/C#程序员要注意 在Python中,所有类成员(包括数据成员)是公共有,所有的方法是虚拟。

继承

面向对象编程的主要优势之一就是代码的重用,一种方式是通过继承机制实现。继承可以被想象成为类之间的一种类型和子类型的关系的实现。

假设你想要写一个程序来跟踪一所大学之中的老师和同学。他们有一些共同的特征,比如名字、年龄、地址等。他们还有一些独有的特征,比如对老师来说有薪水、课程、离开等,对学生来说有成绩和学费。

你当然可以为这两种类型构建两种独立的类,并且处理它们。但是当需要添加一个共同的属性的时候,意味着需要在这两个独立的类中同时添加。这很快就会变得非常笨拙。

一个更好的办法就是构造一个共同的类SchoolMember,然后在让老师和学生分别继承这个类。换句话说,他们都是这个类型(类)的子类型,之后我们也可以为这些子类型添加独有的属性。

这种方式有很多优点,如果我们在SchoolMember中添加/更改任何功能,在子类中会自动反映出来。举个例子,你可以通过简单的修改SchoolMember类的方式来为学生和老师添加新的 ID 卡的字段。然而,子类中的变化不影响其他子类。另外一个好处就是你可以使用一个SchoolMember对象来指向任意一个老师或者学生的对象。这将会在某些情况下非常有用,比如统计学校中人的总数。这被称作多态:如果程序的某个地方期望出现的是父类型的对象,那么可以用它的子类型的对象来替代。也就是说,一个子类型的对象可以被当作父类型的对象。

此外,我们还重用了父类的代码。我们不需要在不同的类中重复这些代码,除非我们使用独立类的方式来实现。

SchoolMember类在这种情况下被称为基类或者超类。而TeacherStudent类被成为派生类或者子类

我们来看看这个例子(保存为oop_subclass.py):

class SchoolMember:
    '''代表学校的任何成员。'''
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print('(初始化学校成员:{})'.format(self.name))

    def tell(self):
        '''告诉我细节。'''
        print('Name:"{}" Age:"{}"'.format(self.name, self.age), end=' ')


class Teacher(SchoolMember):
    '''代表老师。'''
    def __init__(self, name, age, salary):
        SchoolMember.__init__(self, name, age)
        self.salary = salary
        print('(初始化老师:{})'.format(self.name))

    def tell(self):
        SchoolMember.tell(self)
        print('Salary: "{0:d}"'.format(self.salary))


class Student(SchoolMember):
    '''代表学生。'''
    def __init__(self, name, age, marks):
        SchoolMember.__init__(self, name, age)
        self.marks = marks
        print('(初始化学生:{})'.format(self.name))

    def tell(self):
        SchoolMember.tell(self)
        print('Marks: "{:d}"'.format(self.marks))


t = Teacher('Mrs. Shrividya', 40, 30000)
s = Student('Swaroop', 25, 75)

# 打印一个空行
print()

members = [t, s]
for member in members:
    # 所有的老师和学生都可用
    member.tell()

输出:

C:\> python oop_subclass.py
(初始化学校成员:Mrs. Shrividya)
(初始化老师:Mrs. Shrividya)
(初始化学校成员:Swaroop)
(初始化学生:Swaroop)

Name:"Mrs. Shrividya" Age:"40" Salary: "30000"
Name:"Swaroop" Age:"25" Marks: "75"

它是如何工作的:

为了使用继承,我们在类名之后的元祖中指明父类的类名。例如:class Teacher(SchoolMember)。之后我们可以看到在__init__方法中,通过self变量显式的调用了父类的__init__方法来初始化子类对象中属于父类的部分。这非常重要,请记住 -- 既然我们在TeacherStudent子类中定义了__init__方法,Python不会自动的调用父类SchoolMember中的构造方法,你必须显式的调用。

相反的,如果我们不定义子类的__init__方法,Python 将会自动地调用父类中的构造方法。

我们可以把Teacher或者Student的实例当作SchoolMember的实例,当我们想调用父类SchoolMembertell方法的时候,只需要简单的输入Teacher.tell或者Student.tell即可。本例中我们没有这么做,我们在每个子类之中定义了另一个新的tell方法( 父类SchoolMembertell方法作为其中的一部分)来定制子类的功能。因为我们已经做了这样的工作,当我们调用Teacher.tell的时候, Python 将会使用子类中tell方法,而非父类的。然而,如果我们没有在子类中定义tell方法,Python 将使用父类中的方法。Python 总是首先在子类中寻找方法,如果不存在,将会按照子类声明语句中的顺序,依次在父类之中寻找(在这里我们只有一个父类,但是你可以声明多个父类)。

注意术语 -- 如果有多个类被列在继承元组之中,这就叫做多重继承

在父类tell()方法中的print函数中我们使用了end参数,这样在打印完一句话之后,下一次打印紧接在第一句话之后,而不换行。这个技巧可以使得print函数在输出结束时不打印\n符号(换行)。

小结

我们已经探讨了类和对象的各个方面以及相关的术语。我们也已经领略到了面向对象编程的优势和陷阱。Python是高度面向对象,从长远看仔细理解这些概念将对你很有帮助。

接下,我们将学习如何处理输入/输出和如何在Python中访问文件。

results matching ""

    No results matching ""