一、数据封装
面向对象编程的一个重要特点就是数据封装,封装是指将数据与具体操作的实现代码放在某个对象内部,使这些代码的实现细节不被外界发现,外界只能通过接口使用该对象,而不能通过任何形式修改对象内部实现,正是由于封装机制,程序在使用某一对象时不需要关心该对象的数据结构细节及实现操作的方法。使用封装能隐藏对象实现细节,使代码更易维护,同时因为不能直接调用、修改对象内部的私有信息,在一定程度上保证了系统安全性。类通过将函数和变量封装在内部,实现了比函数更高一级的封装。
比如 Student 类:
1 2 3 4 |
class Student(object): def __init__(self, name, score): self.name = name self.score = score |
上面的 Student 类中,每个实例就拥有各自的 name 和 score 这些数据。我们可以通过函数来访问这些数据,比如打印一个学生的成绩:
1 2 3 4 5 6 |
>>> A = Student('jerry', 59) >>> def p_score(s): ... print('%s: %s' % (s.name, s.score)) ... >>> p_score(A) jerry: 59 |
但是,既然 Student 实例本身就拥有这些数据,要访问这些数据,就没有必要从外面的函数去访问,可以直接在 Student 类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。这些封装数据的函数是和 Student 类本身是关联起来的,我们称之为类的方法:
1 2 3 4 5 6 |
class Student(object): def __init__(self, name, score): self.name = name self.score = score def p_score(self): print('%s: %s' % (self.name, self.score)) |
要定义一个方法,除了第一个参数是 self 外,其他和普通函数一样。要调用一个方法,只需要在实例变量上直接调用,除了 self 不用传递,其他参数正常传入:
1 2 3 |
>>> A = Student('jerry', 50) >>> A.p_score() jerry: 50 |
这样一来,我们从外部看 Student 类,就只需要知道,创建实例需要给出 name 和 score,而如何打印,都是在 Student 类的内部定义的,这些数据和逻辑被“封装”起来了,调用很容易,但却不用知道内部实现的细节。
封装的另一个好处是可以给 Student 类增加新的方法,比如 g_grade:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Student(object): def __init__(self, name, score): self.name = name self.score = score def p_score(self): print('%s: %s' % (self.name, self.score)) def g_grade(self): if self.score >= 90: return 'A' elif self.score >= 60: return 'B' else: return 'C' |
同样的,g_grade 方法可以直接在实例变量上调用,不需要知道内部实现细节:
1 2 3 |
>>> A = Student('jerry', 50) >>> A.g_grade() 'C' |
二、访问限制
在类内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。
但是,从前面 Student 类的定义来看,外部代码还是可以自由地修改一个实例的 name、score 属性:
1 2 3 4 5 6 |
>>> A = Student('jerry', 50) >>> A.score 50 >>> A.score = 90 >>> A.score 90 |
如果要让内部属性(类属性和实例属性)不被外部访问,可以把属性的名称前加上两个下划线__
,在 Python 中,实例的变量名如果以__
开头,就变成了一个私有变量(private),私有变量只有内部可以访问,无论是类方法,还是实例方法,但外部不能访问,所以,我们把 Student 类改一改:
1 2 3 4 5 6 |
class Student(object): def __init__(self, name, score): self.__name = name self.__score = score def score(self): print('%s: %s' % (self.__name, self.__score)) |
改完后,对于外部代码来说,没什么变动,但是已经无法从外部访问实例变量.__name
和实例变量.__score
了:
1 2 3 4 5 |
>>> A = Student('jerry', 50) >>> A.__name Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Student' object has no attribute '__name' |
这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。
而访问数据直接通过封装过的方法调用访问,如下:
1 2 |
>>> A.score() jerry: 50 |
如果说我想像访问属性一样正常访问私有属性,可以通过内置 @property 装饰器来装饰 score 方法(可以 help(property) 一下看看),如下:
1 2 3 4 5 6 7 |
class Student(object): def __init__(self, name, score): self.__name = name self.__score = score @property def score(self): return self.__score |
然后就可以以属性方式访问 score 变量:
1 2 3 |
>>> A = Student('jerry', 50) >>> A.score '50' |
此时就不能以方法的方式访问了:
1 2 3 4 |
>>> A.score() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'str' object is not callable |
如果又要允许外部代码修改score怎么办?可以再给Student类增加set_score方法:
1 2 3 4 |
class Student(object): .... def set_score(self, score): self.__score = score |
然后就可以调用函数来修改这个变量了:
1 2 3 |
>>> A.set_score(90) >>> A.score() jerry: 90 |
这样大费周章之后,通过方法来修改参数,就可以对参数做检查,避免传入无效的参数:
1 2 3 4 5 6 7 |
class Student(object): .... def set_score(self, score): if 0 <= score <= 100: self.__score = score else: raise ValueError('bad score') |
同正常访问私有属性那样,修改和删除私有属性可以基于@property装饰器进行装饰,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Student(object): def __init__(self, name, score): self.__name = name self.__score = score @property #使用@property装饰器把方法装饰成了一个同名属性; def score(self): return self.__score @score.setter #x.setter中x代表被@property装饰的属性名,当对此属性赋值时会调用此方法; def score(self, score): if 0 <= score <= 100: self.__score = score else: raise ValueError('bad score') @score.deleter #x.deleter中x代表被@property装饰的属性名,当删除此属性时会调用此方法; def score(self): raise NotImplementedError('you can not delete score of Student') |
这里对于删除予以拒绝,然后调用一下相关方法:
1 2 3 4 5 6 7 8 9 10 11 |
>>> A = Student('jerry', 50) #创建实例A; >>> A.score #访问私有属性; 50 >>> A.score = 90 #私有属性赋值; >>> A.score 90 >>> del A.score #删除私有属性,报预定错误; Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 16, in score NotImplementedError: you can not delete score of Student |
需要注意的是,在Python中,变量名类似__xxx__
的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用__name__
、__score__
这样的变量名。
有些时候,你会看到以一个下划线开头的实例变量名,比如_name
,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__name
是因为Python解释器对外把__name
变量改成了_Student__name
,所以,仍然可以通过_Student__name
来访问__name
变量:
1 2 |
>>> A._Student__name 'jerry' |
但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把__name
改成不同的变量名,可以通过__dict__
改查看相关变量:
1 2 |
>>> A.__dict__ {'_Student__score': 50, '_Student__name': 'jerry', '__name': 'dkey'} |
总的来说就是,Python本身没有任何机制阻止你干坏事,一切全靠自觉。
最后注意下面的这种错误写法:
1 2 3 4 5 6 |
>>> A = Student('jerry', 50) >>> A.p_score() jerry: 50 >>> A.__name = 'dkey' >>> A.__name #设置__name变量; 'dkey' |
表面上看,外部代码“成功”地设置了__name
变量,但实际上这个__name
变量和class内部的__name
变量不是一个变量!内部的__name
变量已经被Python解释器自动改成了_Student__name
,而外部代码给bart
新增了一个__name
变量。不信试试:
1 2 |
>>> A.p_score() jerry: 50 |
完结。。。