Python面向对象

通过几个实例来了解python怎么面向对象编程

面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模板,比如Student类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同

定义一个类

定义

假如我们想定义一个名为Student的类,定义类要通过class关键字

1
2
class Student(object):
pass

class后面紧接着是类名,类名通常首字母要大写,然后是(object),表示该类是从哪个类继承下来的,如果没有合适的继承类,用object类就行,它是python中一切类的基类

类的实例化

类可以看作是一个模板,对象便是类的实例,类的实例化可以使用类名+(),例如

1
student = Student()

这样就完成了类的实例化,student对象便是Student类的一个实例,我们可以自由给它添加一些相应的属性,例如

1
student.name = "Lisa"

由于类可以起到模板的作用,我们可以在创建实例时,把一些我们认为必须绑定的属性强制填写进去,以Student类为例,通过定义一个特殊的__init__方法,可以将name与score等属性绑定

1
2
3
4
class Student(object):
def __init__(self, name, score):
self.name = name
self,score = score

这时候再使用student = Student()便会报错

1
TypeError: __init__() missing 2 required positional arguments: 'name' and 'score'

必须在实例化时将name与grade参数传入

1
2
3
4
5
6
7
8
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score

student = Student("Lisa", 90)
print(student.name)
print(student.score)

运行结果为

1
2
Lisa
90

我们也可以使用hasattr()函数来判断某个对象是否包含对应属性

hasattr语法:

1
hasattr(object, name)

实例:

1
2
3
4
5
6
7
8
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score

student = Student("Lisa", 90)
print(hasattr(student, "score"))
print(hasattr(student, "age")

输出结果为

1
2
True
False

想要判断一个对象是从哪个类创建而来的话,可以使用__class__属性,例如

1
2
3
4
5
class Student(object):
pass

student = Student()
print(student.__class__)

输出结果为

1
<class '__main__.Student'>

注意:__init__方法两边各有两个下划线,方法的第一个参数必须为self,代表这个实例本身,因此在__init__方法内部,可以将各种属性绑定到self上

定义类的方法

除了特殊的__init__方法外,也可以定义其他函数,以Student类为例,可以定义一个打印出自身成绩的函数

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

def print_score(self):
print("%s的成绩为%d" % (self.name, self.score))

student = Student("Lisa", 90)
student.print_score()

可以看到我们在类的内部定义了一个print_score函数,用于打印自身成绩,该函数有一个参数self,该参数是类内部的每个函数都需要添加的,代表该实例本身,不过实际调用类内部函数时self这个参数不需要传入

运行结果为

1
Lisa的成绩为90

在类内部定义的函数与其他函数仅有一点区别,就是第一个参数永远是实例变量self,并且,在调用时不需要传入该参数,除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、可选参数等


封装、继承与多态

封装

封装就是将类内部的信息对外界隐藏起来,从外部无法看见,最典型的一点就是将类的属性设置为私有,在属性名前面加两个下划线便代表将改属性设置为私有,从外面无法直接调用

1
2
3
4
class Student(object):
def __init__(self, name, score):
self.__name = name
self.__score = score

这时候再输出__name属性与__grade属性

1
2
3
student = Student("Lisa", 90)
print(student.__name)
print(student.__score)

可以看见出现错误

1
AttributeError: 'Student' object has no attribute '__name'

值得注意的一点是在外部为类添加属性,是无法设置为私有的

1
2
3
student = Student("Lisa", 90)
student.__age = 12
print(student.__age)

结果仍然会直接输出该属性

1
12

继承

继承通俗的来说,就比如A类继承自B类,则A是B的子类,B是A的父类,子类会获得父类的全部属性和方法,即A获得了B的全部属性和方法

继承又分为单继承与多继承,多继承就是一个子类继承自多个父类,会获得多个父类的属性和方法

想使用继承,只需在定义类时,类名后的括号内填入继承的父类即可

下面以一个家庭为例,一个家庭中有一个父亲,一个母亲,一个儿子与一个女儿,父亲可以说话,母亲可以写作,假设儿子继承自父亲,女儿继承自母亲,那么可以这样定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 父亲类(基类)
class Father(object):
def speak(self):
print("I can speak!")

# 母亲类
class Mother(object):
def write(self):
print("I can write!")

# 单继承: class 子类(父类)
class Son(Father):
pass

# 多继承: class 女儿类(父类,母亲类)
class Daughter(Father, Mother):
pass

son = Son()
daughter = Daughter()
son.speak()
daughter.speak()
daughter.write()

可以看到son和daughet可以直接调用他们父类的方法,结果为

1
2
3
I can speak!
I can speak!
I can write!

我们可以再做下修改,在Son这个类中定义自己的speak方法,看看输出结果

1
2
3
4
5
6
7
8
9
10
11
12
# 父亲类(基类)
class Father(object):
def speak(self):
print("I can speak!")

# 单继承: class 子类(父类)
class Son(Father):
def speak(self):
print("I can speak2!")

son = Son()
son.speak()

结果为

1
I can speak2!

从结果可以看出,当子类和父类都拥有speak方法时,子类的speak方法覆盖了父类的speak方法,在代码运行的时候,总是会调用子类的speak(),这样就引出了多态

多态

要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个class的时候,我们实际上就定义了一种数据类型,判断一个变量是否是某种类型可以用isinstance()判断

1
2
3
4
son = Son()
daughter = Daughter()
print(isinstance(son, Son))
print(isinstance(daughter, Daughter))

结果为

1
2
True
True

看来son,daughter分别对应着Son,Daughter这两种类型

但是再试一试

1
2
3
4
5
son = Son()
daughter = Daughter()
print(isinstance(son, Father))
print(isinstance(daughter, Father))
print(isinstance(daughter, Mother))

结果为

1
2
3
True
True
True

看来son还是Father类型,daughter还是Father与Mother类型

所以,在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是,反过来就不行

1
2
father = Father()
print(isinstance(father, Father))

结果为

1
False

Son可以看作Father,但Father不可以看作Son

要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Father类型的变量

1
2
def say(father):
father.speak()

当我们传入Father的实例时

1
2
father = Father()
say(father)

say()就打印出

1
I can speak!

当我们传入Son的实例时

1
2
son = Son()
say(son)

say()就打印出

1
I can speak2!

看上去没啥意思,但是仔细想想,现在,如果我们再定义一个Son2类型,也从Father派生:

1
2
3
class Son2(Father):
def speak(self):
print("I can speak3!")

当我们调用say()时,传入Son2的实例:

1
2
son2 = Son2()
say(son2)

say()就打印出

1
I can speak3!

你会发现,新增一个Father的子类,不必对say()做任何修改,实际上,任何依赖Father作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态

多态的好处就是,当我们需要传入Son、Son2、Daughter……时,我们只需要接收Father类型就可以了,因为Son、Son2、Daughter……都是Father类型,然后,按照Father类型进行操作即可。由于Father类型有speak()方法,因此,传入的任意类型,只要是Father类或者子类,就会自动调用实际类型的say()方法,这就是多态的意思:

对于一个变量,我们只需要知道它是Father类型,无需确切地知道它的子类型,就可以放心地调用speak()方法,而具体调用的speak()方法是作用在哪个对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Father的子类时,只要确保speak()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:

对扩展开放:允许新增Father子类;

对修改封闭:不需要修改依赖Father类型的say()等函数

静态语言 vs 动态语言

对于静态语言(例如Java)来说,如果需要传入Father类型,则传入的对象必须是Father类型或者它的子类,否则,将无法调用say()方法。

对于Python这样的动态语言来说,则不一定需要传入Father类型。我们只需要保证传入的对象有一个say()方法就可以了:

1
2
3
class Timer(object):
def say(self):
print('dida...')

这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子

Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个read()方法,返回其内容。但是,许多对象,只要有read()方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象