面向对象编程
简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数(方法)。
面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行。
面向对象的三大特点:数据封装、继承和多态。
面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。
在Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。
数据封装
面向对象编程的一个重要特点就是数据封装。
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
Student实例本身就拥有这些数据,要访问这些数据,就没有必要从外面的函数去访问,可以直接在类的内部定义访问数据**的函数,这样,就把“数据”给封装起来了。这些封装数据的函数是和类本身是关联起来的,我们称之为类的方法:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def print_score(self):
print('%s: %s' % (self.name, self.score))
def get_grade(self):
if self.score >= 90:
return 'A'
elif self.score >= 60:
return 'B'
else:
return 'C'
bart = Student('Bart Simpson', 59)
bart.print_score()
bart.get_grade()
#Bart Simpson: 59
#'C'
这样一来,我们从外部看Student类,就只需要知道,创建实例需要给出name和score,而如何打印,都是在Student类的内部定义的,这些数据和逻辑被“封装”起来了,调用很容易,但却不用知道内部实现的细节。
访问限制
在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。
但是,从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的name、score属性。如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__
,在Python中,实例的变量名如果以__
开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问。
所以,我们把Student类改一改:
class Student(object):
def __init__(self, name, score):
self.__name = name
self.__score = score
def print_score(self):
print('%s: %s' % (self.__name, self.__score))
改完后,对于外部代码来说,没什么变动,但是已经无法从外部访问实例变量.__name
和实例变量.__score
了,这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。
有些时候,你会看到以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问
__name
是因为Python解释器对外把__name
变量改成了_Student__name
,所以,仍然可以通过_Student__name
来访问__name
变量
继承和多态
Python的继承和多态与Java一致,不赘述。
静态语言(如Java) vs 动态语言(如Python)
对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。
对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了。
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
获取对象信息
通过内置的一系列函数type()/isinstance()/dir()等方法,我们可以对任意一个Python对象进行剖析,拿到其内部的数据。要注意的是,只有在不知道对象信息的时候,我们才会去获取对象信息。
type()
首先,我们来判断对象类型,使用type()函数,基本类型都可以用type()判断:
type(123)
<class 'int'>
type('str')
<class 'str'>
type(None)
<type(None) 'NoneType'>
如果一个变量指向函数或者类,也可以用type()判断。
type(abs)
<class 'builtin_function_or_method'>
type()函数返回的是对应的Class类型。
isinstance()
要判断class的类型,可以使用isinstance()函数。isinstance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上。
isinstance('a', str)
还可以判断一个变量是否是某些类型中的一种,比如下面的代码就可以判断是否是list或者tuple:
isinstance([1, 2, 3], (list, tuple))
dir()
如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法。
dir('ABC') ['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
类似
__xxx__
的属性和方法在Python中都是有特殊用途的,比如__len__
方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的len()方法,所以,下面的代码是等价的:
len('ABC') == 'ABC'.__len__()
我们自己写的类,如果也想用len(myObj)的话,就自己写一个
__len__()
方法:
class MyDog(object):
def __len__(self):
return 100
dog = MyDog()
len(dog)
仅仅把属性和方法列出来是不够的,配合getattr()、setattr()以及hasattr(),我们可以直接操作一个对象的状态。
实例属性和类属性
由于Python是动态语言,根据类创建的实例可以任意绑定属性。给实例绑定属性的方法是通过实例变量,或者通过self变量:
class Student(object):
def __init__(self, name):
self.name = name
s = Student('Bob')
s.score = 90 #这是实例属性
如果Student类本身需要绑定一个属性,可以直接在class中定义属性,这种属性是类属性,归Student类所有:
class Student(object):
name = 'Student'
当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。
由于实例属性优先级比类属性高,千万不要把实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。