不论单身与否,也要面向对象编程。


基本概念

类与对象的关系

一个类(Class)能够创建一种新的类型(Type),其中对象(Object)就是类的实例(Instance)

类的属性

对象可以使用属于它的普通变量来存储数据。这种从属于对象或者类的变量叫做字段(Field)。对象还可以使用属于类的函数来实现某些功能,这种函数叫做类的方法(Method)。字段与方法通称类的属性(Attribute)。 字段由两种类型:它们属于某一类的各个实例或对象,称为实例变量(Instance Variables);或者从属于某一类本身,称为类变量(Class Variables)。

self

类方法与普通函数的区别在于,类方法必须有一个额外的名字,且这个名字必须添加到参数列表的开头,但是不用在调用这个功能时为这个参数赋值,这种特定的变量引用对象本身,习惯上赋予self这一名称(类似于C/C++语言中的指针)。

举例来说,假设我们由一个MyClass的类,这个类下有一个实例myobject,当我们调用一个这个对象的方法时,如myobject.method(arg1, arg2)时,Python将会自动将其转换成MyClass.method(myobject, arg1, arg2),这就是self的意义所在。

示例程序块如下:

1
2
3
4
5
class Person:
pass # 空的代码块

p = Person()
print(p)

运行结果如下:

1
<__main__.Person object at 0x000001B7CF28C048>

注意:
在上述示例中,首先使用class来创建一个新类,缩进的语句块代表这个类的主体,此处空代码块用pass语句标明。接着,通过采用类的名称后面跟着一对括号的方法,给这个类创建了一个对象。为了验证操作是否成功,我们通过直接将它们打印出来来确认变量类型。结果显示我们在Person类的__main__模块中拥有了一个实例。

方法

类与对象一般都可以带有方法(Method),唯一的不同在于需要一个额外的self变量。
示例程序块如下:

1
2
3
4
5
6
class Person:
def say_hi(self):
print('Hello, how are you?')

p = Person()
p.say_hi()

运行结果如下:

1
Hello, how are you?

注意:

  1. 代码块中的p = Person()p.say_hi()等价于Person().sayhi()
  2. sayhi()这一方法不需要参数,但是在函数定义中依然有self变量。

__init__方法

__init__方法在类的对象被实例化(Instantiated)时立即运行,此外这一方法可以对任何想要进行操作的目标对象进行初始化(Initialization)操作。

示例程序块如下:

1
2
3
4
5
6
7
8
9
class Person:
def __init__(self, name):
self.name = name

def say_hi(self):
print('Hello, my name is', self.name)

p = Person('lzwang')
p.say_hi()

运行结果如下:

1
Hello, my name is lzwang

注意:

  1. 上述实例中,我们定义__init__方法来接收name参数,同时我们也创建了一个名为name的字段;
  2. 虽然有着两个name,但它们是不同的变量;
  3. self.name表示这个nameself对象的一部分,另一个name是一个局部变量。

类变量与对象变量

前面几个小节谈论了类与对象的功能部分(方法),现在讨论数据部分,即字段,只不过是绑定(Bound)到类与对象的命名空间(Namespace)的普通变量。

字段(Field)由两种类型——类变量与对象变量,它们根据究竟是类还是对象拥有这些变量来进行分类。

  • 类变量(Class Variable)是共享的(Shared)——它们可以被属于该类的所有实例访问;
  • 对象变量(Object Variable)由类的每一个独立的对象或者实例所拥有,不会被分享。

示例程序块如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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()

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(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.

注意:

  1. population属于Robot类,属于类变量,而name属于一个对象(使用self分配),属于对象变量;
  2. 我们对于name对象变量采用self.name标记法加以称呼,这是这个对象中所具有的方法;
  3. 当一个对象变量与一个类变量名称相同时,类变量将会被隐藏;
  4. how_many是一个属于类而非属于对象的方法,这就是说,我们可以把它定义为一个classmethod(类方法)或者是staticmethod(静态方法),这取决于我们是否知道我们属于哪个类,上例中由于已经引用了一个类变量,因此使用classmehod(类方法);
  5. 上例中,我们使用了装饰器(Decorator)how_many方法标记为类方法,启用@classmethod装饰器等价于调用how_many = classmethod(how_many)
  6. 只能使用self来引用同一对象的变量与方法,称为属性引用(Attribute Reference)
  7. 所有的类成员都是公开的,但有一个例外:若使用数据成员并在其名字中使用了双下划线作为前缀,形成了诸如__privatevar的形式,Python会使用名称调整(Name-mangling)来使其称为一个私有变量。

所有类成员(包括数据成员)都是公开的,并且Python中所有的方法都是虚拟的(Virtual)

继承

面向对象编程的一大优点就是对于代码的重用(Reuse),重用的一种实现方式是通过继承(Inheritance)机制,继承是在类之间实现类型与子类型(Type and Subtype)关系的工具。
举一个学校的例子:教师和学生都是学校的成员,他们有着公共的特征(姓名,年龄,地址),但是教师有独自的特征(薪水,课程,假期),学生也有独自的特征(成绩,学费)。如果创建两个独立的类,会显得太啰嗦,所以我们选择先创建一个公共类叫做SchoolMember,然后让教师和学生从这个类中继承(Inherit),即他们成为子类,接着我们就可以向子类型中添加某些类独有的特征。
这样做有以下的优点:

  • 如果我们增加或者修改了SchoolMember的任何功能,那么它将自动反映在子类型中;
  • 我们可以将某一老师或者学生对象看作是SchoolMember的对象并加以引用(这在诸如清点学校成员数量时候很有用);
    以上被称为多态性(Polymorphism),在任何情况下,只要父类型希望,子类型都可以被替换,即该对象可以看作是父类的实例。
    在上文中,SchoolMemmber类被称为基类(Base Class)或者超类(Superclass)TeacherStudent被称为派生类(Derived Classes)或者子类(Subclass)

示例程序块如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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('lzwang', 25, 75)

print() #打印空行

members = [t,s]
for member in members:
member.tell()

运行结果如下:

1
2
3
4
5
6
7
(Initialized SchoolMember: Mrs. Shrividya)
(Initialized Teacher: Mrs. Shrividya)
(Initialized SchoolMember: lzwang)
(Initialized Student: lzwang)

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

注意:

  1. 若要使用继承,在定义类时我们需要在类后面跟一个包含基类名称的元祖;
  2. 注意到基类的__init__方法是通过self变量被显式调用,因而我们可以初始化对象的基类的部分;
  3. 由于我们在TeacherStudent子类中定义了__init__方法,所以Python不会自动调用基类SchoolMember的构造函数,若果要使用的话,必须要显式地调用它;
  4. 另一方面,如果我们没有在一个子类中定义一个__init__方法,Python将会自动调用基类的构造函数;
  5. 当我们使用SchoolMember类的tell方法时,我们可以将Teacher或者Student的实例看作是SchoolMember的实例;
  6. 当我们调用tell方法时,会调用子类的而不是基类的。原因在于Python总会从当前的世纪类型中开始寻找方法。如果它找不到对应的方法,它就会在该类所属的基类中按顺序逐个寻找属于基类的方法,而这个基类是定义子类时,后面所跟的元祖所指定的;
  7. 如果继承元祖(Inheritance Tuple)中有超过一个类,这种情况被称为多重继承(Multiple Inheritance)
  8. end参数用在超累的tell()方法的print函数中,目的在于打印一行并允许下一次打印在同一行继续(这是一个让print能够不在打印的末尾打印出\n的方法)。

Python是高度面向对象的。