进阶:面向对象编程 ===================== 在至今我们编写的所有程序中,我们曾围绕函数设计我们的程序,也就是那些能够处理数据的代码块。这被称作*面向过程(Procedure-oriented)*的编程方式。还有另外一种组织起你的程序的方式,它将数据与功能进行组合,并将其包装在被称作“对象”的东西内。在大多数情况下,你可以使用过程式编程,但是当你需要编写一个大型程序或面对某一更适合此方法的问题时,你可以考虑使用面向对象式的编程技术。 类与对象是面向对象编程的两个主要方面。一个\ **类(Class)**\ 能够创建一种新的*类型(Type)*,其中\ **对象(Object)**\ 就是类的\ **实例(Instance)**\ 。可以这样来类比:你可以拥有类型 ``int`` 的变量,也就是说存储整数的变量是 ``int`` 类的实例(对象)。 **针对静态编程语言程序员的提示** 请注意,即使是整数也会被视为对象(\ ``int`` 类的对象)。这不同于 C++ 与 Java(1.5 版之前),在它们那儿整数是原始内置类型。 有关类的更多详细信息,请参阅 ``help(int)``\ 。 C# 与 Java 1.5 程序员会发现这与 *装箱与拆箱(Boxing and Unboxing)* 概念颇有相似之处。 对象可以使用 *属于* 它的普通变量来存储数据。这种从属于对象或类的变量叫作\ **字段(Field)**\ 。对象还可以使用属于类的函数来实现某些功能,这种函数叫作类的\ **方法(Method)**\ 。这两个术语很重要,它有助于我们区分函数与变量,哪些是独立的,哪些又是属于类或对象的。总之,字段与方法通称类的\ **属性(Attribute)**\ 。 字段有两种类型——它们属于某一类的各个实例或对象,或是从属于某一类本身。它们被分别称作\ **实例变量(Instance Variables)**\ 与\ **类变量(Class Variables)**\ 。 通过 ``class`` 关键字可以创建一个类。这个类的字段与方法可以在缩进代码块中予以列出。 ``self`` -------- 类方法与普通函数只有一种特定的区别——前者必须多加一个参数在参数列表开头,这个名字必须添加到参数列表的开头,但是你不用在你调用这个功能时为这个参数赋值,Python 会为它提供。这种特定的变量引用的是对象本身,按照惯例,它被赋予 ``self`` 这一名称。 尽管你可以为这一参数赋予任何名称,但是_强烈推荐_你使用 ``self`` 这一名称——其它的任何一种名称绝对会引人皱眉。使用一个标准名称能带来诸多好处——任何一位你的程序的读者能够立即认出它,甚至是专门的 IDE(Integrated Development Environments,集成开发环境)也可以为你提供帮助,只要你使用了 ``self`` 这一名称。 **针对 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`` 的全部特殊之处所在。 这同时意味着,如果你有一个没有参数的方法,你依旧必须拥有一个参数——\ ``self``\ 。 类 -- 最简单的类(Class)可以通过下面的案例来展示(保存为 ``oop_simplestclass.py``\ ): .. code:: python class Person: pass # 一个空的代码块 p = Person() print(p) 输出: .. code:: text $ python oop_simplestclass.py <__main__.Person instance at 0x10171f518> **它是如何工作的** 我们通过使用 ``class`` 语句与这个类的名称来创建一个新类。在它之后是一个缩进的语句块,代表这个类的主体。在本案例中,我们创建的是一个空代码块,使用 ``pass`` 语句予以标明。 然后,我们通过采用类的名称后跟一对括号的方法,给这个类创建一个对象(或是实例,我们将在\ `后面的章节 <13.object_oriented_programming.md#init>`__\ 中了解有关实例的更多内容)。为了验证我们的操作是否成功,我们通过直接将它们打印出来来确认变量的类型。结果告诉我们我们在 ``Person`` 类的 ``__main__`` 模块中拥有了一个实例。 要注意到在本例中还会打印出计算机内存中存储你的对象的地址。案例中给出的地址会与你在你的电脑上所能看见的地址不相同,因为 Python 会在它找到的任何空间来存储对象。 方法 ---- 我们已经在前面讨论过类与对象一如函数那般都可以带有方法(Method),唯一的不同在于我们还拥有一个额外的 ``self`` 变量。现在让我们来看看下面的例子(保存为 ``oop_method.py``\ )。 .. code:: python class Person: def say_hi(self): print('Hello, how are you?') p = Person() p.say_hi() # 前面两行同样可以写作 # Person().say_hi() 输出: .. code:: text $ python oop_method.py Hello, how are you? **它是如何工作的** 这里我们就能看见 ``self`` 是如何行动的了。要注意到 ``say_hi`` 这一方法不需要参数,但是依旧在函数定义中拥有 ``self`` 变量。 ``__init__`` 方法 ----------------- 在 Python 的类中,有不少方法的名称具有着特殊的意义。现在我们要了解的就是 ``__init__`` 方法的意义。 ``__init__`` 方法会在类的对象被实例化(Instantiated)时立即运行。这一方法可以对任何你想进行操作的目标对象进行 *初始化(Initialization)* 操作。这里你要注意在 init 前后加上的双下划线。 案例(保存为 ``oop_init.py``\ ): .. code:: python class Person: def __init__(self, name): self.name = name def say_hi(self): print('Hello, my name is', self.name) p = Person('Swaroop') p.say_hi() # 前面两行同时也能写作 # Person('Swaroop').say_hi() 输出: .. code:: text $ python oop_init.py Hello, my name is Swaroop **它是如何工作的** 在本例中,我们定义一个接受 ``name`` 参数(当然还有 ``self`` 参数)的 ``__init__`` 方法。在这里,我们创建了一个字段,同样称为 ``name``\ 。要注意到尽管它们的名字都是“name”,但这是两个不相同的变量。虽说如此,但这并不会造成任何问题,因为 ``self.name`` 中的点号意味着这个叫作“name”的东西是某个叫作“self”的对象的一部分,而另一个 ``name`` 则是一个局部变量。由于我们已经如上这般明确指出了我们所指的是哪一个名字,所以它不会引发混乱。 当我们在 ``Person`` 类下创建新的实例 ``p`` 时,我们采用的方法是先写下类的名称,后跟括在括号中的参数,形如:\ ``p = Person('Swaroop')``\ 。 我们不会显式地调用 ``__init__`` 方法。 这正是这个方法的特殊之处所在。 现在,我们可以使用我们方法中的 ``self.name`` 字段了,使用的方法在 ``say_hi`` 方法中已经作过说明。 类变量与对象变量 ---------------- 我们已经讨论过了类与对象的功能部分(即方法),现在让我们来学习它们的数据部分。数据部分——也就是字段——只不过是 *绑定(Bound)* 到类与对象的\ *\ **命名空间(Namespace)**\ *\ 的普通变量。这就代表着这些名称仅在这些类与对象所存在的上下文中有效。这就是它们被称作*\ “命名空间”\_的原因。 *字段(Field)* 有两种类型——类变量与对象变量,它们根据究竟是类还是对象_拥有_这些变量来进行分类。 **类变量(Class Variable)**\ 是共享的(Shared)——它们可以被属于该类的所有实例访问。该类变量只拥有一个副本,当任何一个对象对类变量作出改变时,发生的变动将在其它所有实例中都会得到体现。 **对象变量(Object variable)**\ 由类的每一个独立的对象或实例所拥有。在这种情况下,每个对象都拥有属于它自己的字段的副本,也就是说,它们不会被共享,也不会以任何方式与其它不同实例中的相同名称的字段产生关联。下面一个例子可以帮助你理解(保存为 ``oop_objvar.py``\ ): .. code:: python # coding=UTF-8 class Robot: """表示有一个带有名字的机器人。""" # 一个类变量,用来计数机器人的数量 population = 0 def __init__(self, name): """初始化数据""" self.name = name print("(Initializing {})".format(self.name)) # 当有人被创建时,机器人 # 将会增加人口数量 Robot.population += 1 def die(self): """我挂了。""" print("{} is being destroyed!".format(self.name)) Robot.population -= 1 if Robot.population == 0: print("{} was the last one.".format(self.name)) else: print("There are still {:d} robots working.".format( Robot.population)) def say_hi(self): """来自机器人的诚挚问候 没问题,你做得到。""" print("Greetings, my masters call me {}.".format(self.name)) @classmethod def how_many(cls): """打印出当前的人口数量""" print("We have {:d} robots.".format(cls.population)) droid1 = Robot("R2-D2") droid1.say_hi() Robot.how_many() droid2 = Robot("C-3PO") droid2.say_hi() Robot.how_many() print("\nRobots can do some work here.\n") print("Robots have finished their work. So let's destroy them.") droid1.die() droid2.die() Robot.how_many() 输出: .. code:: text $ python oop_objvar.py (Initializing R2-D2) Greetings, my masters call me R2-D2. We have 1 robots. (Initializing C-3PO) Greetings, my masters call me C-3PO. We have 2 robots. Robots can do some work here. Robots have finished their work. So let's destroy them. R2-D2 is being destroyed! There are still 1 robots working. C-3PO is being destroyed! C-3PO was the last one. We have 0 robots. **它是如何工作的** 这是一个比较长的案例,但是它有助于展现类与对象变量的本质。在本例中,\ ``population`` 属于 ``Robot`` 类,因此它是一个类变量。\ ``name`` 变量属于一个对象(通过使用 ``self`` 分配),因此它是一个对象变量。 因此,我们通过 ``Robot.population`` 而非 ``self.population`` 引用 ``population`` 类变量。我们对于 ``name`` 对象变量采用 ``self.name`` 标记法加以称呼,这是这个对象中所具有的方法。要记住这个类变量与对象变量之间的简单区别。同时你还要注意当一个对象变量与一个类变量名称相同时,类变量将会被隐藏。 除了 ``Robot.popluation``\ ,我们还可以使用 ``self.__class__.population``\ ,因为每个对象都通过 ``self.__class__`` 属性来引用它的类。 ``how_many`` 实际上是一个属于类而非属于对象的方法。这就意味着我们可以将它定义为一个 ``classmethod(类方法)`` 或是一个 ``staticmethod(静态方法)``\ ,这取决于我们是否需要知道这一方法属于哪个类。由于我们已经引用了一个类变量,因此我们使用 ``classmethod(类方法)``\ 。 我们使用\ :doc:`装饰器(Decorator) <./more>` \ 将 ``how_many`` 方法标记为类方法。 你可以将装饰器想象为调用一个包装器(Wrapper)函数的快捷方式,因此启用 ``@classmethod`` 装饰器等价于调用: .. code:: python 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 中所有的方法都是虚拟的(Virtual)。 继承 ---- 面向对象编程的一大优点是对代码的\ **重用(Reuse)**\ ,重用的一种实现方法就是通过\ **继承(Inheritance)**\ 机制。继承最好是想象成在类之间实现\ **类型与子类型(Type and Subtype)**\ 关系的工具。 现在假设你希望编写一款程序来追踪一所大学里的老师和学生。有一些特征是他们都具有的,例如姓名、年龄和地址。另外一些特征是他们独有的,一如教师的薪水、课程与假期,学生的成绩和学费。 你可以为每一种类型创建两个独立的类,并对它们进行处理。但增添一条共有特征就意味着将其添加进两个独立的类。这很快就会使程序变得笨重。 一个更好的方法是创建一个公共类叫作 ``SchoolMember``\ ,然后让教师和学生从这个类中继承(Inherit),也就是说他们将成为这一类型(类)的子类型,而我们就可以向这些子类型中添加某些该类独有的特征。 这种方法有诸多优点。如果我们增加或修改了 ``SchoolMember`` 的任何功能,它将自动反映在子类型中。举个例子,你可以通过简单地向 SchoolMember 类进行操作,来为所有老师与学生添加一条新的 ID 卡字段。不过,对某一子类型作出的改动并不会影响到其它子类型。另一大优点是你可以将某一老师或学生对象看作 ``SchoolMember`` 的对象并加以引用,这在某些情况下会大为有用,例如清点学校中的成员数量。这被称作\ **多态性(Polymorphism)**\ ,在任何情况下,如果父类型希望,子类型都可以被替换,也就是说,该对象可以被看作父类的实例。 同时还需要注意的是我们重用父类的代码,但我们不需要再在其它类中重复它们,当我们使用独立类型时才会必要地重复这些代码。 在上文设想的情况中,\ ``SchoolMember`` 类会被称作\ **基类(Base Class)**\ 或是\ **超类(Superclass)**\ 。\ ``Teacher`` 和 ``Student`` 类会被称作\ **派生类(Derived Classes)**\ 或是\ **子类(Subclass)**\ 。 我们将通过下面的程序作为案例来进行了解(保存为 ``oop_subclass.py``\ ): .. code:: python # coding=UTF-8 class SchoolMember: '''代表任何学校里的成员。''' def __init__(self, name, age): self.name = name self.age = age print('(Initialized SchoolMember: {})'.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('(Initialized Teacher: {})'.format(self.name)) def tell(self): SchoolMember.tell(self) print('Salary: "{:d}"'.format(self.salary)) class Student(SchoolMember): '''代表一位学生。''' def __init__(self, name, age, marks): SchoolMember.__init__(self, name, age) self.marks = marks print('(Initialized Student: {})'.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() 输出: .. code:: text $ python oop_subclass.py (Initialized SchoolMember: Mrs. Shrividya) (Initialized Teacher: Mrs. Shrividya) (Initialized SchoolMember: Swaroop) (Initialized Student: Swaroop) Name:"Mrs. Shrividya" Age:"40" Salary: "30000" Name:"Swaroop" Age:"25" Marks: "75" **它是如何工作的** 要想使用继承,在定义类时我们需要在类后面跟一个包含基类名称的元组。然后,我们会注意到基类的 ``__init__`` 方法是通过 ``self`` 变量被显式调用的,因此我们可以初始化对象的基类部分。下面这一点很重要,需要牢记——因为我们在 ``Teacher`` 和 ``Student`` 子类中定义了 ``__init__`` 方法,Python 不会自动调用基类 ``SchoolMember`` 的构造函数,你必须自己显式地调用它。 相反,如果我们_没有_在一个子类中定义一个 ``__init__`` 方法,Python 将会自动调用基类的构造函数。 我们会观察到,我们可以通过在方法名前面加上基类名作为前缀,再传入 ``self`` 和其余变量,来调用基类的方法。 在这里你需要注意,当我们使用 ``SchoolMember`` 类的 ``tell`` 方法时,我们可以将 ``Teacher`` 或 ``Student`` 的实例看作 ``SchoolMember`` 的实例。 同时,你会发现被调用的是子类型的 ``tell`` 方法,而不是 ``SchoolMember`` 的 ``tell`` 方法。理解这一问题的一种思路是 Python *总会* 从当前的实际类型中开始寻找方法,在本例中即是如此。如果它找不到对应的方法,它就会在该类所属的基本类中依顺序逐个寻找属于基本类的方法,这个基本类是在定义子类时后跟的元组指定的。 这里有一条有关术语的注释——如果继承元组(Inheritance Tuple)中有超过一个类,这种情况就会被称作\ **多重继承(Multiple Inheritance)**\ 。 ``end`` 参数用在超类的 ``tell()`` 方法的 ``print`` 函数中,目的是打印一行并允许下一次打印在同一行继续。这是一个让 ``print`` 能够不在打印的末尾打印出 ``\n`` (新行换行符)符号的小窍门。 总结 ---- 在这一章我们探索了有关类和对象的各个方面,还有与它们相关的各类术语。我们还了解了面向对象编程的益处与陷阱。Python 是高度面向对象的,从长远来看,了解这些概念对你大有帮助。