面向对象编程
在我们迄今为止编写的所有程序中,我们都是围绕函数来设计程序的,即操作数据的语句块。这被称为面向过程的编程方式。还有另一种组织程序的方式,即将数据和功能组合在一起,包裹在一种叫做对象的东西中。这就是面向对象的编程范式。大多数时候你可以使用面向过程编程,但在编写大型程序或遇到更适合这种方法的问题时,你可以使用面向对象编程技术。
类(class)和对象(object)是面向对象编程的两个主要方面。一个类创建了一种新的类型,而对象是该类的实例。打个比方,你可以有类型为 int 的变量,这意味着存储整数的变量是 int 类的实例(对象)。
静态语言程序员注意
请注意,即使是整数也被视为对象(
int类的对象)。这与 C++ 和 Java(1.5 版本之前)不同,在那些语言中整数是原始的本地类型。参见
help(int)了解关于该类的更多详细信息。C# 和 Java 1.5 程序员会发现这与装箱和拆箱(boxing and unboxing)概念类似。
对象可以使用属于对象的普通变量来存储数据。属于对象或类的变量被称为字段(field)。对象也可以通过使用属于类的函数来拥有功能。这样的函数被称为类的方法(method)。这个术语很重要,因为它帮助我们区分独立的函数和变量与属于类或对象的函数和变量。 总而言之,字段和方法可以被称为该类的属性(attribute)。
字段有两种类型——它们可以属于类的每个实例/对象,也可以属于类本身。它们分别被称为实例变量和类变量。
类使用 class 关键字创建。类的字段和方法列在一个缩进的代码块中。
self
类方法与普通函数只有一个特定的区别——它们必须有一个额外的第一个参数名称,需要添加到参数列表的开头,但你在调用方法时不需要为这个参数提供值,Python 会自动提供它。这个特定的变量引用对象本身,按照惯例,它被命名为 self。
虽然你可以给这个参数取任何名字,但强烈建议使用 self 这个名字——使用其他名字是不被赞成的。使用标准名称有很多好处——任何阅读你程序的人都能立即认出它,甚至专门的 IDE(集成开发环境)也能在你使用 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。
类
最简单的类如以下示例所示(保存为 oop_simplestclass.py)。
class Person:
pass # An empty block
p = Person()
print(p)
输出:
$ python oop_simplestclass.py
<__main__.Person instance at 0x10171f518>
工作原理
我们使用 class 语句和类的名称创建一个新类。后面跟着一个缩进的语句块,形成类的主体。在这个例子中,我们有一个空的代码块,用 pass 语句表示。
接下来,我们使用类名后跟一对括号来创建这个类的对象/实例。(我们将在下一节中了解更多关于实例化的内容)。为了验证,我们简单地通过打印来确认变量的类型。它告诉我们我们在 __main__ 模块中有一个 Person 类的实例。
注意,你的对象存储在计算机内存中的地址也被打印出来了。在你的计算机上地址会有不同的值,因为 Python 可以将对象存储在它找到空间的任何地方。
方法
我们已经讨论过类/对象可以有方法,就像函数一样,只是多了一个额外的 self 变量。我们现在来看一个例子(保存为 oop_method.py)。
class Person:
def say_hi(self):
print('Hello, how are you?')
p = Person()
p.say_hi()
# The previous 2 lines can also be written as
# Person().say_hi()
输出:
$ python oop_method.py
Hello, how are you?
工作原理
在这里我们看到了 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('Hello, my name is', self.name)
p = Person('Swaroop')
p.say_hi()
# The previous 2 lines can also be written as
# Person('Swaroop').say_hi()
输出:
$ python oop_init.py
Hello, my name is Swaroop
工作原理
在这里,我们将 __init__ 方法定义为接受一个参数 name(以及通常的 self)。在这里,我们只是创建了一个也叫 name 的新字段。注意这是两个不同的变量,即使它们都叫 'name'。这没有问题,因为点号表示法 self.name 意味着有一个叫 "name" 的东西是属于名为 "self" 的对象的一部分,而另一个 name 是一个局部变量。由于我们明确指出了引用的是哪个 name,所以不会产生混淆。
创建 Person 类的新实例 p 时,我们使用类名后跟括号中的参数:p = Person('Swaroop')。
我们没有显式调用 __init__ 方法。
这就是这个方法的特殊之处。
现在,我们可以在方法中使用 self.name 字段了,这在 say_hi 方法中得到了演示。
类变量与对象变量
我们已经讨论了类和对象的功能部分(即方法),现在让我们学习数据部分。数据部分,即字段,不过是绑定到类和对象的命名空间(namespace)的普通变量。这意味着这些名称仅在这些类和对象的上下文中有效。这就是为什么它们被称为命名空间。
有两种类型的字段——类变量和对象变量,它们根据类或对象是否拥有该变量来分类。
类变量是共享的——它们可以被该类的所有实例访问。类变量只有一个副本,当任何一个对象修改了类变量时,所有其他实例都会看到这个变化。
对象变量由类的每个单独的对象/实例拥有。在这种情况下,每个对象都有自己的字段副本,即它们不共享,并且与不同实例中同名字段没有任何关系。一个例子会让你很容易理解(保存为 oop_objvar.py):
class Robot:
"""Represents a robot, with a name."""
# A class variable, counting the number of robots
population = 0
def __init__(self, name):
"""Initializes the data."""
self.name = name
print("(Initializing {})".format(self.name))
# When this person is created, the robot
# adds to the population
Robot.population += 1
def die(self):
"""I am dying."""
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):
"""Greeting by the robot.
Yeah, they can do that."""
print("Greetings, my masters call me {}.".format(self.name))
@classmethod
def how_many(cls):
"""Prints the current population."""
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()
输出:
$ 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 赋值),因此是一个对象变量。
因此,我们将 population 类变量称为 Robot.population,而不是 self.population。我们在该对象的方法中使用 self.name 表示法来引用对象变量 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 来引用同一对象的变量和方法。这被称为属性引用。
在这个程序中,我们还看到了类和方法的文档字符串(docstring)的使用。我们可以在运行时使用 Robot.__doc__ 访问类文档字符串,使用 Robot.say_hi.__doc__ 访问方法文档字符串。
在 die 方法中,我们只是将 Robot.population 计数减少 1。
所有类成员都是公开的。一个例外是:如果你使用双下划线前缀命名数据成员,如 __privatevar,Python 会使用名称修饰(name-mangling)来有效地使其成为私有变量。
因此,遵循的惯例是,任何仅在类或对象内部使用的变量应该以下划线开头,所有其他名称都是公开的,可以被其他类/对象使用。记住这只是一个惯例,Python 不会强制执行(双下划线前缀除外)。
C++/Java/C# 程序员注意
在 Python 中,所有类成员(包括数据成员)都是公开的,所有方法都是虚方法(virtual)。
继承
面向对象编程的主要好处之一是代码的重用,实现这一目标的方式之一是通过继承(inheritance)机制。继承最好理解为在类之间实现一种类型和子类型的关系。
假设你想编写一个程序来跟踪大学里的老师和学生。他们有一些共同的特征,如姓名、年龄和地址。他们也有各自的特征,如老师的薪水、课程和假期,以及学生的成绩和学费。
你可以为每种类型创建两个独立的类来处理它们,但添加一个新的共同特征意味着要在这两个独立的类中都添加。这很快就会变得难以管理。
更好的方法是创建一个名为 SchoolMember 的公共类,然后让老师和学生类从该类继承,即它们将成为该类型(类)的子类型,然后我们可以为这些子类型添加特定的特征。
这种方法有很多好处。如果我们在 SchoolMember 中添加/更改任何功能,这些变化也会自动反映在子类型中。例如,你可以通过简单地将新的 ID 卡字段添加到 SchoolMember 类中,为老师和学生们都添加一个新的 ID 卡字段。但是,子类型中的更改不会影响其他子类型。另一个好处是你可以将老师或学生对象作为 SchoolMember 对象来引用,这在某些情况下可能很有用,比如统计学校成员的数量。这就是多态(polymorphism),即子类型可以在任何期望父类型的地方被替换,也就是说,对象可以被视为父类的实例。
还要注意我们重用了父类的代码,不需要在不同的类中重复它,如果我们使用独立的类的话就需要重复。
在这种情况下,SchoolMember 类被称为基类(base class)或超类(superclass)。Teacher 和 Student 类被称为派生类(derived class)或子类(subclass)。
我们现在将这个例子作为一个程序来看(保存为 oop_subclass.py):
class SchoolMember:
'''Represents any school member.'''
def __init__(self, name, age):
self.name = name
self.age = age
print('(Initialized SchoolMember: {})'.format(self.name))
def tell(self):
'''Tell my details.'''
print('Name:"{}" Age:"{}"'.format(self.name, self.age), end=" ")
class Teacher(SchoolMember):
'''Represents a teacher.'''
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):
'''Represents a student.'''
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)
# prints a blank line
print()
members = [t, s]
for member in members:
# Works for both Teachers and Students
member.tell()
输出:
$ 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"
工作原理
要使用继承,我们在类定义中类名后面的元组中指定基类名称(例如,class Teacher(SchoolMember))。接下来,我们观察到基类的 __init__ 方法是使用 self 变量显式调用的,以便我们可以在子类中初始化实例的基类部分。这非常重要,要记住——由于我们在 Teacher 和 Student 子类中定义了 __init__ 方法,Python 不会自动调用基类 SchoolMember 的构造函数,你必须自己显式调用它。
相反,如果我们没有在子类中定义 __init__ 方法,Python 会自动调用基类的构造函数。
虽然我们可以像对待 SchoolMember 实例一样对待 Teacher 或 Student 的实例,并简单地通过输入 Teacher.tell 或 Student.tell 来访问 SchoolMember 的 tell 方法,但我们在每个子类中定义了另一个 tell 方法(使用 SchoolMember 的 tell 方法作为其中一部分)来为该子类量身定制。因为我们这样做了,当我们写 Teacher.tell 时,Python 使用该子类的 tell 方法而不是超类的。但是,如果子类中没有 tell 方法,Python 会使用超类中的 tell 方法。Python 总是先在实际的子类类型中查找方法,如果没有找到任何东西,就开始逐个在子类的基类中查找方法,按照类定义中元组中指定的顺序(这里我们只有 1 个基类,但你可以有多个基类)。
关于术语的说明——如果在继承元组中列出了多个类,那么它被称为多重继承(multiple inheritance)。
在超类的 tell() 方法中,print 函数使用了 end 参数来打印一行并允许下一个 print 继续在同一行上。这是一个让 print 不在打印末尾输出 \n(换行符)的技巧。
小结
我们现在已经探索了类和对象的各个方面以及与之相关的各种术语。我们还看到了面向对象编程的好处和陷阱。Python 是高度面向对象的语言,仔细理解这些概念从长远来看会对你有很大帮助。
接下来,我们将学习如何处理输入/输出以及如何在 Python 中访问文件。