Python学习笔记(12)-黑马教程-面向对象之封装

面向对象相关知识背景

面向对象即Object Oriented,OO,这是一种软件开发方法,具体的定义也说不清楚,根据书中的描述,对象大致意味着一系列数据或属性以及一套访问这些数据的方法。在《Python无师自通》这本书中提到:

在 Python 中,每一个数据值,如 2 或”Hello, World!”,被称为对象(object)。可以把对象看作拥有 3 个属性的数据值:唯一标识(identity)、数据类型和值。

对象的唯一标识,指的是其在计算机内存中的地址,该地址不会变化。

对象的数据类型是对象所属的数据类别,这决定了对象的属性,也不会变化。

对象的值是其表示的数据,例如数字 2 的值即为 2。

“Hello, World!”这个对象的数据类型为字符串(str,string 的缩写),值为”Hello, World!”。如果提及数据类型为 str 的对象,可以称其为字符串。

与面向对象有关的一些术语有多态、封装、方法、属性、超类和继承。

多态:可对不同类型的对象执行相同的损伤;;

封装:对外部隐藏有关对象工作原理的细节。

继承:可基于通用类创建出专用类。

在后面的笔记中会详细说明这是什么意思。

什么是类

类是一种对象,每个对象都属于特定的类,并被称为该类的实例。例如,如果你在窗外看到一只鸟,这只鸟就是“鸟类”的一个实例,鸟类是一个非常通用(抽象)的类,它有许多个子类:你看到的那只鸟可能属于子类”云雀“。你可以将“鸟类”视为由所有鸟组成的集合,而“云雀”是其一个子集,一个类的对象为另一个类的对象的子集时,前者就是后者的子类。因此“云雀”为“鸟类”的子类,而“鸟类”为“云雀”的超类。并且在面向对象的编程中,是先定义类,然后再由类生成一个实例。

通过上面的比喻,我们大致就理解了类,子类,超类。但在面向对象的编程中,子类可能还要更复些,因为类是由其支持的方法定义的。类的所有实例都有该类的所有方法,因此子类的所有实例都有超类的所有方法。因此,要定义子类,只需要定义多出来的方法(或者是重写某个方法)即可。例如鸟类(我们用Bird替代)可能提供方法fly,而Penguin类(Bird的一个子类)可能新增方法eat_fish。创建Penguin类时,我们还可能要重写超类方法,即方法fly,企鹅Penguin不会飞,我们要在Penguin的实例中,方法fly应什么都不做或引发异常。

创建类

在使用面向对象开发之前,应该首先分析一下需要,确定程序中需要包括哪些类。在程度开发中,要设计一个类,通常需要满足以下三个要求:

  1. 类名。类名的命名要以驼峰式命名法进行命名,也就是每个单词的首字母要大写,例如CapWords
  2. 属性。属性赋予事物具有什么样的特性;
  3. 方法。方法赋予事物具有什么样的行为。

封装(encapsulation)

封装指的是向外部隐藏不必要的细节。这听起来有点像多态(无需知道对象的内部细节就可使用它)。这两个概念很像,因为它们都是抽象的原则。它们都像函数一样,可帮助你处理程序的组成部分,让你无需关心不必要的细节。

但封装与多态又有所不同,多态让你无需知道对象所属的类(对象的类型)就能调用其方法,而封闭则是让你需知道对象的构造就能使用它,下面展示一个案例,在这个案例中,使用了多态,但是没有使用封闭,发吭你有一个名为OpenObject的类,如下所示:

1
2
3
4
>>>o = OpenObject()
>>>o.set_name('Sir Lancelot')
>>>o.get_names()
>>>'Sir Lancelot'

在上面的案例中,我们像调用函数一样调用了类,创建了一个对象,并将其关联到变量o,然后使用了set_nameget_name这两个方法(假设OpenObject支持这些方法),但是,如果o将其名称储存在全局变量global_name中,如下所示:

1
2
>>>gloabl_name
'Sir Lancelot'

这就意味着使用OpenObject类的实例(对象)时,就需要考虑global_name的内容,事实上,必须确保无人能修改它。

1
2
3
>>>global_name = 'Sir Gumby'
>>>o.get_name()
'Sir Gumby'

d如果浓度创建多个OpenObject对象,则会出现问题,因为它们共用同一个变量,如下所示:

1
2
3
4
5
>>> o1 = OpenObject()
>>> o2 = OpenObject()
>>> o1.set_name('Robin Hood')
>>> o2.get_name()
'Robin Hood'

在上面的这个案例中,设置一个对象的名称时,将自动设置另一个对象的名称,这可能不是我们想要的结果。基本上,你希望对象是抽象的:当调用方法时,无需操心其他的事情,如避免干扰全局变量。如何将名称“封装”在对象中呢,将其作为一种属性即可,属性是归属于对象的变量,就像方法一样,实际上,方法差不多就是与函数相关联的属性,如果你使用属性而非全局变量重新编写前面的类,并将其重命名为ClosedObject,就可以像下面这样使用它,如下所示:

1
2
3
4
>>> c = ClosedObject()
>>> c.set_name('Sir Lancelot')
>>> c.get_name()
'Sir Lancelot'

到目前为止,一切顺利,但这并不能证明名称不是存储在全局变量中的,现在再来创建一个对象,如下所示:

1
2
3
4
>>> r = ClosedObject()
>>> r.set_name('Sir Robin')
r.get_name()
'Sir Robin'

从中可知正确地设置了新对象的名称,此时再来看一下第一个对象,如下所示:

1
2
>>> c.get_name()
'Sir Lancelot'

其名称还在,因为这个对象有自己的状态,对象的状态由其属性(如名称)描述,对象的方法可能修改这些属性,因此对象将一系列函数(方法)组合起来,并赋予它们访问一些变量(属性)的权限,而属性可用于在两次函数调用之间存储值。

属性和方法的确定

描述对象特征的内容可以定义为属性。对象具有的某些行为(动词),就可以定义为方法

例如我们看下面一个案例:

  • 小明今年18岁,身高1.75,每天早上完步,会去东西。
  • 小美今年17岁,身高1.65,小美不跑步,小美喜吃东西。

从上面的描述我们可以这么思考,我们可以设计一个人类,例如persion,这个类中需要包含3个属性,即名字name,年龄age,和身高height,此外,还要包括2个动作,分别为是跑步run和吃东西eat

再看一个案例:

我们有以下需求:

  • 一只黄颜色的狗,叫大黄
  • 看见生人
  • 看见家人摇尾巴

那么我们就会设计这样一个狗类(Dog),这个类中含有2个属性,分别是名字(name)用于记录狗的名字,颜色(color)用于记录狗的颜色;含有2个方法,分别是叫(shout)和摇尾巴(shake)。

以上就是类是如何设计的,总之就是一句话,先考虑需求,然后考虑类,描述性的文字是属性,动作性的文字是方法。

面向对象基础语法

在Python中,对象无所不在,变量、数据、函数都是对象。在Python中,可能通过两种方法来验证以对象:

  1. 在标记符/数据后输入一个.,然后按下TAB键,ipython就会提示该对象能够调用的方法列表
  2. 使用内置函数dir传入标识符/数据,可以查看对象内的所有属性及方法

先看第一种方法,在ipython中定义一个列表变量,即gl_list=[],然后输入gl_list.(后面有一个点),按下TAB键,后面就会出现这个对象能使用的方法,如下所示:

1
2
3
4
5
6
In [3]: gl_list = []
In [4]: gl_list.
append() count() insert() reverse()
clear() extend() pop() sort()
copy() index() remove()

现在定义一个函数,如下所示:

1
2
3
4
5
6
7
8
9
In [6]: def demo():
...: """这是一个测试函数"""
...: print("Hello python")
...:
In [7]: demo()
Hello python
In [8]: demo.

此时输入demo后面再加一个点.,没有任何信息,此时就需要用另外一种方法来验证它是一个对象,也就是使用dir,如下所示:

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
In [10]: dir(demo)
Out[10]:
['__annotations__',
'__call__',
'__class__',
'__closure__',
'__code__',
'__defaults__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__get__',
'__getattribute__',
'__globals__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__kwdefaults__',
'__le__',
'__lt__',
'__module__',
'__name__',
'__ne__',
'__new__',
'__qualname__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__']

从上面的我们可以看出来,这里面是一个列表,它列出了很多方法,这些方法都是双下划线开头,双下划线结尾,这些东西都是Python内置的方法(__方法名__),有些可以直接使用,例如__doc__,如下所示:

1
2
In [11]: demo.__doc__
Out[11]: '这是一个测试函数'

此时我们就可以看到,使用__doc__这个内置方法就能查看函数的文档说明,常用的一些内置方法/属性有以下这些:

序号 方法名 类型 作用
1 __new__ 方法 创建对象时,会被自动调用
2 __init__ 方法 对象被初始化时,会被自动调用
3 __del__ 方法 对象被从内存中销毁之前,会被自动调用
4 __str__ 方法 返回对象的工描述信息,print函数输出使用

因此,dir()这个函数很有用,例如当我想调用某个函数的方法时,一时想不想来,就可以使用dir()这个函数。

定义简单的类

格式

在Python中要定义一个只包含方法的类,格式如下:

1
2
3
4
5
6
class 类名
def 方法1(self, 参数列表):
pass
def 方法2(self, 参数列表):
pass

先看下面的一个案例:

1
2
3
4
5
6
7
8
9
10
class Person:
def set_name(self, name):
self.name = name
def get_name(self):
return self.name
def greet(self):
print("Hello, wolrd! I'm {}.".format(self.name))

在上面的这个案例中,一共定义了3种方法,它们类似于函数定义,但都位于class语句语,Person是类的名称。class语句创建独立的命名空间,用于在其中定义方法(其实就是定义函数)。self它指向对象本身,具体是哪个对象呢,我们再来看下面的案例:

1
2
3
4
5
6
foo = Person()
bar = Person()
foo.set_name('Luke Skywalker')
bar.set_name('Anakin Skywalker')
foo.greet()
bar.greet()

运行结果如下所示:

1
2
Hello, wolrd! I'm Luke Skywalker.
Hello, wolrd! I'm Anakin Skywalker.

这个案例主要是用来说明self是什么,对foo调用set_name和greet时,foo都会做为第一个参数自动传递给它们,也就是命名为self的原因(其实可以任意命名),如果没有self,所有的方法都无法访问对象本身,也就是要损伤的属性所属的对象。此外,也可以从外部访问这些属性,如下所示:

1
2
3
foo.name
bar.name = 'Yoda'
bar.greet()

运行结果如下所示:

1
2
3
Hello, wolrd! I'm Luke Skywalker.
Hello, wolrd! I'm Anakin Skywalker.
Hello, wolrd! I'm Yoda.

属性、函数和方法

方法和函数的区别表现在前面提到的参数self上,方法更准确地说是关联的方法,是将第一个参数关联到它所属的实例中,因此无需提供这个参数。也可以将属性关联到一个普通的函数,但这样就没有特殊的self参数了,如下所示:

1
2
3
4
5
6
7
8
9
10
11
class Class:
def method(self):
print("I have a self")
def function():
print("I don't...")
instance = Class()
instance.method()
instance.method= function
instance.method()

运行结果如下所示:

1
2
I have a self
I don't...

从上面可以知道,有没有参数self并不取决于是否以刚才使用的方式(如instance.method)调用方法。实际上,完全可以让另一个变量指向同一个方法,如下所示:

1
2
3
4
5
6
7
8
9
class Bird:
song = 'Squaawk!'
def sing(self):
print(self.song)
bird = Bird()
bird.sing()
birdsong = bird.sing
birdsong()

运行结果如下所示:

1
2
Squaawk!
Squaawk!

最后一个方法调用看起来很像函数调用,但变量birdsong指向的是关联的方法bird.song,这意味着它也能够访问参数self(即使它被关联到类的实例)。

隐藏

默认情况下,我们可以从外部访问对象的属性,看下面的案例:

1
2
3
4
5
>>>c.name
'Sir Lancelot'
>>>c.name = 'Sir Gumby'
>>>c.get_name()
'Sir Gumby'

如果我们不让其他人从外部访问属性,就可能将属性定义为私有,私有属性不能从对象外部访问,而只能通过存取器(例如get_name和set_name)来访问。

要让方法或属性成为私有的(不能从外部访问),只需要让其名称以两个下划线打头即可,如下所示:

1
2
3
4
5
6
7
8
9
10
11
class Secrective:
def __inaccessible(self):
print("Bet you can't see me...")
def accessible(self):
print("The secret message is: ")
self.__inaccessible()
s = Secrective()
s.accessible()
s.__inaccessible()

运行结果如下所示:

1
2
3
4
5
6
7
8
9
s.accessible()
The secret message is:
Bet you can't see me...
s.__inaccessible()
Traceback (most recent call last):
File "C:/Users/20161111/PycharmProjects/untitled2/test.py", line 11, in <module>
s.__inaccessible()
AttributeError: 'Secrective' object has no attribute '__inaccessible'

从上面的案例我们可以看出来,无法从外部访问__inaccessible,但是在类中,例如accessible中我们仍然可以使用它。虽然以两个下划线打头有点奇怪,但这样的方法类似于其他语言中的标准私有方法。然后背后的处理手法并不标准:在类定义中,对所有以两个下划线打头的名称都进行转换,即在开头加上一个下划线和类名,如下所示:

1
2
>>> Secretive._Secretive__inaccessible
<unbound method Secretive.__inaccessible>

上面的代码在我的编辑器中无法运行,可以直接跳过,但知道了这种幕后处理手法,就能从类外部访问私有方法,但是不建议这么做,如下所示:

1
2
s=Secrective()
s._Secrective__inaccessible()

运行结果如下所示:

1
Bet you can't see me...

总之,你无法禁止别人访问对象的私有方法和属性,但这种名称修改方式发出了强烈的信号,让他们不要这样做。如果你不希望名称被悠,又想发出不要从外部修改属性或方法的信号,可以用一个下划线打头,这虽然只是一种约定,但也有些作用,例如使用from module import *就不会导入以一个下划线打头的名称。

类的命名空间

下面的两条语句大致等价:

1
2
def foo(x):return x*x
foo = lambda x: x*x

它们𨝌会创建一个返回参数平方的函数,并将这个函数关联到变量foo,可以在全局(模块)作用域内定义名称foo,也可以在函数或方法内定义。定义类时情况也是如此,在class语句中定义的代码都是在一个特殊的命名空间(类的命名空间)内执行的,而类的所有成员都可以访问这个命名空间。类定义其实就是要执行的代码段。例如,在类定义中,并非只能包括def语句,如下所示:

1
2
3
4
>>> class C:
... print('Class C being defined...')
...
Class C being defined...

这个案例非常简单,再看下一个案例:

1
2
3
4
5
6
7
8
9
10
11
class MemberCounter:
members = 0
def init(self):
MemberCounter.members += 1
m1 = MemberCounter()
m1.init()
MemberCounter.members
m2 = MemberCounter()
m2.init()
MemberCounter.members

运行结果如下所示:

1
2
1
2

上述代码在类作用域内定义了一个变量,所有的成员(实例)都可以访问它,这里使用它来计算类实例的数量,注意到这里使用了init来初始化所有的实例,也就是将init转换为合适的构造函数,每个实例都可以访问这个类作用域内的变化,就像方法一样,如下所示:

1
2
3
4
>>> m1.members
2
>>> m2.members
2

如果在一个实例中给属性members赋值,结果如下所示:

1
2
3
4
5
>>> m1.members='two'
>>> m1.members
'two'
>>> m2.members
2

新值被写入m1的一个属性中,这个属性遮住了类的变量。

创建对象

当一个类定义完全成,要使用这个类来创建对象,语法格式如下:

1
对象变量=类名()

创建对象

先看一个案例:例如小猫爱吃鱼,小猫要喝水。

分析:

  1. 定义一个猫类,Cat()
  2. 定义两个方法eatdrink
  3. 此时并没有涉及属性,因此我们不用定义属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Cat:
def eat(self):
print("小猫爱吃鱼")
def drink(self):
print("小猫要喝水")
# 创建猫对象
tom = Cat()
tom.eat()
tom.drink()

运行结果如下所示:

1
2
小猫爱吃鱼
小猫要喝水

在这个案例中,我们先定义了一个猫类(Cat),然后创建了一个猫对象,再后,使这个对象赋予了吃与喝这两个方法,这两个方法已经封装到了类中,并不需要知道它的执行细节。

如果要用print函数直接输出对象,那么输出结果就是此对象创建的内存地址,如下所示:

1
2
print(tom)
<__main__.Cat object at 0x0000017F72807A90>

创建多个对象

类只有一个,类只是一个模板,使用这个模板可以创建多个对象,如下所示:

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
40
class Cat:
def eat(self):
print("小猫爱吃鱼")
def drink(self):
print("小猫要喝水")
# 创建猫对象
tom = Cat()
tom.eat()
tom.drink()
# 再创建一个猫对象
lazy_cat = Cat()
lazy_cat.drink()
lazy_cat.eat()
print(tom)
print(lazy_cat)
class Cat:
def eat(self):
print("小猫爱吃鱼")
def drink(self):
print("小猫要喝水")
# 创建猫对象
tom = Cat()
# 再创建一个猫对象
lazy_cat = Cat()
print(tom)
print(lazy_cat)

我们来看一下结果:

1
2
<__main__.Cat object at 0x0000020AF8E47A90>
<__main__.Cat object at 0x0000020AF8E5F4E0>

从结果可以看出来,tom与lazy_cat这两个对象的内存地址不一样,它们是不一样的对象,再看一下以下代码:

1
2
3
lazy_cat2 = lazy_cat
print(lazy_cat2)
print(lazy_cat)

结果如下:

1
2
<__main__.Cat object at 0x000001C0424B7A90>
<__main__.Cat object at 0x000001C0424B7A90>

可以发现,lazy_cat和lazy_cat2这两个对象是一样的。

方法中的self参数

给对象添加属性

在Python中很容易给对象添加属性,但是这种做法并不推荐,因为对象的属性通常都已经封装到类中了,没必要再单独给对象添加属性,虽然不推荐,但还是要看一下案例,如下所示:

1
tom.name = "Tom"

输出对象属性

前面是找到,在类中定义的方法中的一个参数self,这个self是指:哪一个对象调用的方法,self就是哪一个对象的引用。

例如代码中的tom = Cat(),tom指向的对象就是由Cat这个类创建的,此时这个对象调用了Cat中的eat这个方法时,self指的也是这个对象,如果要想访问这个对象的属性,那么就是self加一个点(.)即可,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Cat:
def eat(self):
print("%s 爱吃鱼"% self.name)#这一步我们可以使用self.name来访问对象的属性
def drink(self):
print(""%s 要喝水"% self.name)
# 创建猫对象
tom = Cat()
# 可能使用 .属性名 利用赋值语句就可以为对象添加属性
tom.name = "Tom"
tom.eat()
# 再创建一个猫对象
lazy_cat = Cat()
lazy_cat.name = "大懒猫"
lazy_cat.eat()

结果如下所示:

1
2
Tom 爱吃鱼
大懒猫 爱吃鱼

我们可以看到,原来的“小猫”就被替换成了“Tom”和“大懒猫”。也就是说,由哪一个对象调用的方法,方法内的self就是哪一个对象的引用。在类封装的方法内部,self就表示当前调用方法的对象自己。调用方法时,程序员不需要传递self参数。在方法内部,可以通过self.访问对象的属性,也可以通过self.调用其它的对象方法。

初始化方法

当使用类名()创建对象时,会自动执行以下操作:

  1. 为对象在内存中分配空间——创建对象
  2. 为对象的属性设置初始值——初始化方法(init)

这个初始化方法就是__init__方法,__init__是对象的内置的方法,此方法是专门用来定义一个类具有哪些属性的方法,这个方法是固定的。我们在Cat中增加__init__方法,验证此方法在创建对象时会被自动调用,如下所示:

1
2
3
4
5
6
7
8
class Cat:
def __init__(self):
print("这是一个初始化方法")
tom = Cat()

运行结果如下所示:

1
这是一个初始化方法

从结果我们可以发现,使用类名()来创建对象的时候,会自动调用初始化方法__init__

在初始化方法内部定义属性

__init__方法内部使用self.属性名 = 属性的初始值就可以定义属性

定义属性之后,再使用Cat类创建的对象都会拥有该属性,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Cat:
def __init__(self):
print("这是一个初始化方法")
# self.属性名 = 属性的初始值
self.name = "Tom"
tom = Cat()
print(tom.name)

运行结果,如下所示:

1
2
这是一个初始化方法
Tom

现在解释一下上面的代码,代码是从上到下执行的:

  1. 当Python解释器遇到class Cat:时,这段代码是不执行的,直接跳过去,跳到tom=Cat()处;
  2. 我们的代码运行到tom=Cat()处时,Python解释器此时会做两件事情:①在内存中为Cat对象分配一块空间,假设就是0x1234这块空间(16进制);②然后执行class Cat这块的代码,类代码中的self也会指向这块空间(即0x1234),此时也是从上到下执行的,执行到self.name = "Tom"时,这块空间就被命名为了Tom,因此在使用print(tom.name)时,就输出了空间的名字。

初始化方法的改造

在前面代码的基础上,我们再创建一个对象,lazy_cat,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Cat:
def __init__(self):
print("这是一个初始化方法")
# self.属性名 = 属性的初始值
self.name = "Tom"
def eat(self):
print("%s爱吃鱼"%self.name)
tom = Cat()
print(tom.name)
lazy_cat = Cat()
lazy_cat.eat()

从上面代码我们可知,lazy_cat这个对象的名称还是Tom,如下所示:

1
2
3
4
这是一个初始化方法
Tom
这是一个初始化方法
Tom爱吃鱼

运行后,发现果然如此。因为我们在初始化时,已经把对象的名称固定了,就是self.name = "Tom"这句代码。现在我们要解决这个问题。

如果要解决这个问题,我们需要在初始化方法中再定义一个形参,用于输入不同对象的名称,现在我们在def __init__(self)中添加一个参数,new_name,即def __init__(self, new_name),当我们已经添加了这个形参后,在原来代码创建一个新对象时,也要添加相应的实参,例如tom = Cat()就需要写为tom= Cat("Tom"),完整代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Cat:
def __init__(self, new_name):
print("这是一个初始化方法")
# self.属性名 = 属性的初始值
self.name = new_name
def eat(self):
print("%s爱吃鱼"%self.name)
tom = Cat("Tom")
print(tom.name)
lazy_cat = Cat("大懒猫")
lazy_cat.eat()

运行结果如下所示:

1
2
3
4
这是一个初始化方法
Tom
这是一个初始化方法
大懒猫爱吃鱼

此时我们就发现了,一个对象对应一个名称。

总结一下就是,在开发中,如果希望在创建对象的同时就设置对象的属性,就可以对__init__方法进行改造:

  1. 把希望设置的属性值,定义成__init__方法的参数;
  2. 在方法内部使用self.属性 = 形参接收外部 传递的参数;
  3. 在创建对象时,使用类名(属性1,属性2...)调用。

内置方法和属性

这一部分介绍两个内置方法,如下所示:

序号 方法名 类型 作用
01 __del__ 方法 对象被从内存中销毁之前,会被自动调用
02 __str__ 方法 返回对象的描述信息,print函数输出使用

__del__方法

  • 在Python中,当使用类名()创建对象时,为对象分配完空间后,自动调用__init__方法。
  • 当一个对象被从内存中销毁之前,会自动调用__del__方法。

应用场景

  • __init__改造初始化方法,可以让创建对象更加灵活。
  • __del___如果希望在对象被销毁之前,再做一些事情,可以使用__del__方法。

生命周期

  • 一个对象从调用类名()创建,生命周期开始。
  • 一个对象的__del__方法一旦被调用,生命周期结束。
  • 在对象的生命周期内,可以访问对象属性,或者让对象调用方法。

先来看一个案例,代码如下所示:

代码A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Cat:
def __init__(self, new_name):
self.name = new_name
print("%s 来了" % self.name)
def __del__(self):
print("%s 我去了"% self.name)
# tom是一个全局变量
tom = Cat("Tom")
print(tom.name)
# del tom
print("-"*50)

结果如下所示:

1
2
3
4
Tom 来了
Tom
--------------------------------------------------
Tom 我去了

现在,我们将代码中的del tom显示出来,如下所示:

代码B:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Cat:
def __init__(self, new_name):
self.name = new_name
print("%s 来了" % self.name)
def __del__(self):
print("%s 我去了"% self.name)
# tom是一个全局变量
tom = Cat("Tom")
print(tom.name)
del tom
print("-"*50)

运行结果如下所示:

1
2
3
4
Tom 来了
Tom
Tom 我去了
--------------------------------------------------

从结果中我们可以发现,代码A中的运行结果中,Tom 我去了出现在了点线下面,而代码B中的Tom 我去了则出现在了点线上面。而代码A与代码B的区别就在于,代码A中没有del tom这段代码,而代码B中有。

代码A中没有del tom这段代码,就说明,只有Python把tom这个对象自动删除(不是指操作者自己操作)后,才会出现__del__方法中的字符。我猜测这可能是Python自动回收内存的一种机制(注:关于Python的内存回收机制我也不太懂,有空了再补上)。而如果你自己亲自操作,也就是说使用了del tom这个代码后,Python就知道你把tom这个对象删除了,就开始运行__del__方法中的内容,然后再运行print("-"*50)这段代码。

__str__方法

  • 在Python中,使用print输出对象变量,默认情况下,会输出这个变量引用的对象是由哪一个类创建的对象,以及在内存中的地址(十六进制表示)。

  • 如果在开发中,希望使用print输出对象变量时,能够打印自定义的内容,就可以利用__str__这个内置方法了。需要注意的是,__str__方法必须返回一个字符。

看下面的案例,在这个案例,我们直接输出一个对象(有的教程在此处称之为“实例”),如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Cat:
def __init__(self, new_name):
self.name = new_name
print("%s 来了" % self.name)
def __del__(self):
print("%s 我去了"% self.name)
# tom是一个全局变量
tom = Cat("Tom")
print(tom)

运行结果如下所示:

1
2
3
Tom 来了
<__main__.Cat object at 0x0000021E1311B278>
Tom 我去了

其中第二行,即<__main__.Cat object at 0x0000021E1311B278>这里输出的是Cat这个类,并把tom这个对象在内存中的地址显示出来。现在我们在类中增加__str___这个方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Cat:
def __init__(self, new_name):
self.name = new_name
print("%s 来了" % self.name)
def __del__(self):
print("%s 我去了"% self.name)
def __str__(self):
return "我是小猫"
# tom是一个全局变量
tom = Cat("Tom")
print(tom)

运行结果如下所示:

1
2
3
Tom 来了
我是小猫
Tom 我去了

此时,我们改造了__str__这个方法后,输出的就不再是这个对象的类,以及这个对象的地址了。而是我们自定义的内容,现在把__str__中的方法再改造一下,改成return "我是小猫[%s]"% self.name,则结果就如下所示:

1
2
3
Tom 来了
我是小猫[Tom]
Tom 我去了

面向对象封装(encapsulation)案例

封装

  1. 封装是面向对象编程的一大特点;
  2. 面向对象编程的第一步就是将属性和方法封装到一个抽象的类中;
  3. 外界使用类创建对象(有的教程叫实例),然后让对象调用方法;
  4. 对象方法中的细节都被封装在类的内部。

案例分析——小明爱跑步

我们以“小明爱跑步”为例说明一下,先分析需求,如下所示:

  1. 小明体重75.0公斤;
  2. 小明每次跑步会减肥0.5公斤;
  3. 小明每次吃东西体重增加1公斤。

代码分析

代码如下所示:

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
class Person:
def __init__(self, name, weight):
self.name = name
self.weight = weight
def __str__(self):
return "我的名字叫 %s 体重是 %.2f 公斤" %(self.name, self.weight)
def run(self):
print("%s 爱跑步, 跑步锻炼身体"%self.name)
self.weight -= 0.5
def eat(self):
print("%s 是吃货,吃完这顿再减肥"%self.name)
self.weight += 1
xiaoming = Person("小明", 75.0)
xiaoming.run()
xiaoming.eat()
print(xiaoming)

运行结果如下所示:

1
2
3
小明 爱跑步, 跑步锻炼身体
小明 是吃货,吃完这顿再减肥
我的名字叫 小明 体重是 75.50 公斤

如果我们把Person这个类折叠起来,就是下面的这个样子,如下所示:

1
2
3
4
5
6
7
8
class Person:...
xiaoming = Person("小明", 75.0)
xiaoming.run()
xiaoming.eat()
print(xiaoming)

从上面折叠后的代码我们就知道,我们创建了一个Person这个类,再由这个类创建了一个叫xiaoming的对象,然后这个对象进行了跑步(run)和吃东西(eat),具体这跑步与吃东西它们的代码,都已经封装到了Person这个类中,我们直接调用即可。

案例扩展——小美也爱跑步

在原来案例的基础上进行扩展,先看一下需要:

  1. 小明和小美都爱跑步;
  2. 小明体重75.0公斤;
  3. 小美体重45.0公斤;
  4. 每次跑步都会减少0.5公斤;
  5. 每次吃东西都会增加1公斤。

代码分析——案例扩展

再使用Persion这个类创建一个新对象,即xiaomei,如下所示:

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 Person:
def __init__(self, name, weight):
self.name = name
self.weight = weight
def __str__(self):
return "我的名字叫 %s 体重是 %.2f 公斤" %(self.name, self.weight)
def run(self):
print("%s 爱跑步, 跑步锻炼身体"%self.name)
self.weight -= 0.5
def eat(self):
print("%s 是吃货,吃完这顿再减肥"%self.name)
self.weight += 1
xiaoming = Person("小明", 75.0)
xiaoming.run()
xiaoming.eat()
print(xiaoming)
# 小美爱跑步
xiaomei = Person("小美", 45)
xiaomei.eat()
xiaomei.run()
print(xiaomei)
print(xiaoming)

结果如下所示:

1
2
3
4
5
6
7
小明 爱跑步, 跑步锻炼身体
小明 是吃货,吃完这顿再减肥
我的名字叫 小明 体重是 75.50 公斤
小美 是吃货,吃完这顿再减肥
小美 爱跑步, 跑步锻炼身体
我的名字叫 小美 体重是 45.50 公斤
我的名字叫 小明 体重是 75.50 公斤

我们可以看到,由一个类创建的两个对象。在每个对象的方法内部,可以直接访问对象的属性。每个对象各自使用各自的方法,属性互不影响。

案例分析三——摆放家具

现在我们再看一个案例,摆放家具,看一下需求:

  1. 房子(House)有户型、总面积和家具名称列表,而新房子没有任何家具;
  2. 家具(HouseItem)有名字和占地面积,其中不同的家具占地面积也不一样,例如床(bed)占地4平方米,衣柜(chest)占地2平方米,餐桌(table)占地1.5平方米;
  3. 现在我们要将2中的3样家具添加到房子中;
  4. 输出房子时,要求输出这些信息:户型、总面积、剩余面积和家具名称列表。

有了上面需求后,我们要考虑一下如何设计代码:

  1. 定义2个类,一个是房子(House),一个是家具(HouseItem)。
  2. 房子有4个属性;
  3. 家具有2个属性。

但是,房子与家具这两个类先定义哪个?思路就是,由于房子这个类中要用到家具(家具有面积,房子的剩余面积与家具有关),因此我们要先定义家具这个类,总之就是,被使用的类要先定义。

定义家具类

根据前面的分析思路,现在先定义一个家具类,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class HouseItem():
def __init__(self, name, area):
self.name = name
self.area = area
def __str__(self):
return "[%s] 占地 %.2f"%(self.name, self.area)
# 创建家具
bed = HouseItem("床", 4)
chest = HouseItem("衣柜", 2)
table = HouseItem("餐桌", 1.5)
print(bed)
print(chest)
print(table)

结果运行如下所示:

1
2
3
[床] 占地 4.00
[衣柜] 占地 2.00
[餐桌] 占地 1.50

定义房子类

思路:房子中需要定义4个属性,分别为房子类型(house_type),面积(area),剩余面积(free_area),家具列表(item_list)。但是,在给房子传递参数时,只需要传递其中的2个即可,分别是房子类型(house_type)与面积(area),因为剩余面积(free_area)可以由这2个参数算出来,而家具列表(item_list)在初始情况下是一个空列表,,也不需要传入。

注:在同一个代码文件中,如果要定义2个以及2个以上的类,类与类的代码之间要空两行,现在前2个属性的代码如下所示:

1
2
3
4
5
6
class House:
def __init__(self, house_type, area):
self.house_type = house_type
self.area = area

这段代码定义了房子的2个需要传入的属性,现在再定义剩余面积(free_area)和家具名称列表,如下所示:

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
class House:
def __init__(self, house_type, area):
self.house_type = house_type
self.area = area
#剩余面积
self.free_area = area
#家具名称
self.item_list = []
# 刚开始的时候,就是一个空列表
def __str__(self):
# 定义描述方法
# 这里显示的是return返回的内容,如下所示:
return ("户型:%s \n总面积: %.2f[剩余: %.2f]\n家具: %s"
% (self.house_type,
self.area,
self.free_area,
self.item_list))
# 创建房子对象
my_house = House("两室一厅", 60)
my_house.add_item(bed)
# 添加一张床
my_house.add_item(chest)
# 添加一个衣柜
my_house.add_item(table)
# 添加一张餐桌

此时,完成的任务包括:定义了一个家具类,定义了一个房子类。当输入了房子类型与面积这2个参数后,代码可以显示用房输入的这些内容,前面的这些代码运行的结果如下所示:

1
2
3
4
5
6
7
8
9
[床] 占地 4.00
[衣柜] 占地 2.00
[餐桌] 占地 1.50
要添加 [床] 占地 4.00
要添加 [衣柜] 占地 2.00
要添加 [餐桌] 占地 1.50
户型:两室一厅
总面积: 60.00[剩余: 60.00]
家具: []

但是,还有2个任务没有完成,分别是①剩余面积还没有计算;②家具列表还是空的。下面完成这2个任务,思路是这个样子的:

  1. 我们需要判断一下家具的面积是否超过了剩余面积,如果超过,则提示不能添加这些家具;
  2. 将家具的名称追加到家具名称的列表中;
  3. 用房子的剩余面积减去家具面积。

现在我们往代码中补充一个添加家具(add_item)方法,这个方法的代码以及要实现的功能如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def add_item(self, item):
print("要添加 %s" % item)
# 1. 判断家具的面积
if item.area > self.free_area:
print("%s 的面积太大了,无法添加"% item.name)
return
# 2. 将家具的名称添加到列表中
self.item_list.append(item.name)
# 3. 计算剩余面积
self.free_area -= item.area

实现全部功能的完整代码如下所示:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class HouseItem():
def __init__(self, name, area):
self.name = name
self.area = area
def __str__(self):
return "[%s] 占地 %.2f"%(self.name, self.area)
class House:
def __init__(self, house_type, area):
self.house_type = house_type
self.area = area
#剩余面积
self.free_area = area
#家具名称
self.item_list = []
# 刚开始的时候,就是一个空列表
def __str__(self):
# 这里显示的是return返回的内容,如下所示:
return ("户型:%s \n总面积: %.2f[剩余: %.2f]\n家具: %s"
% (self.house_type,
self.area,
self.free_area,
self.item_list))
def add_item(self, item):
print("要添加 %s" % item)
# 1. 判断家具的面积
if item.area > self.free_area:
print("%s 的面积太大了,无法添加"% item.name)
return
# 2. 将家具的名称添加到列表中
self.item_list.append(item.name)
# 3. 计算剩余面积
self.free_area -= item.area
# 创建家具
bed = HouseItem("床", 4)
chest = HouseItem("衣柜", 2)
table = HouseItem("餐桌", 1.5)
print(bed)
print(chest)
print(table)
# # 创建房子对象
my_house = House("两室一厅", 60)
#
my_house.add_item(bed)
# # 添加一张床
#
# my_house.add_item(chest)
# # 添加一个衣柜
#
# my_house.add_item(table)
# # 添加一张餐桌
print(my_house)

运行结果如下所示:

1
2
3
4
5
6
7
[床] 占地 4.00
[衣柜] 占地 2.00
[餐桌] 占地 1.50
要添加 [床] 占地 4.00
户型:两室一厅
总面积: 60.00[剩余: 56.00]
家具: ['床']

如果我们把床的面积改为40,那么运行结果如下所示:

1
2
3
4
5
6
7
[床] 占地 40.00
[衣柜] 占地 2.00
[餐桌] 占地 1.50
要添加 [床] 占地 40.00
户型:两室一厅
总面积: 60.00[剩余: 20.00]
家具: ['床']

如果我们把这三个家具的总面积改为大于60(例如床面积为40,餐桌为20,衣柜为20),结果如下所示:

1
2
3
4
5
6
7
8
9
10
[床] 占地 40.00
[衣柜] 占地 20.00
[餐桌] 占地 20.00
要添加 [床] 占地 40.00
要添加 [衣柜] 占地 20.00
要添加 [餐桌] 占地 20.00
餐桌 的面积太大了,无法添加
户型:两室一厅
总面积: 60.00[剩余: 0.00]
家具: ['床', '衣柜']

从添加家具这个案例我们可以知道这个案例中的面向对象思想:

  1. 主程序只负责创建房子对象和家具对象;
  2. 让房子对象调用add_item方法将家具添加到房子中;
  3. 面积计算、剩余面积、家具列表等处理都被封装到房子类的内部中。

案例分析四——士兵突击

在这里再次复习一下封装:

  1. 封装是面积对象编程的一个特点;;
  2. 面积对象编程的第一步就是将属性和方法封装到一个抽象的类中;
  3. 外界使用类创建对象(有的教程叫实例),然后让对象调用方法;
  4. 对象方法的细节都被封装到类的内部。

在这一小节中,还要学到一个知识点就是:一个对象的属性可以是另外一个类创建的对象。

案例需求

现在我们先看一下这个案例的需求:

  1. 士兵许三多有一把AK47
  2. 士兵可以开火;
  3. 枪能发射子弹;
  4. 枪装填子弹——增加子弹的数量。

从第一项需求中我们可以知道,我们要创建一个士兵类(Soldier)以及一个枪类(Gun),并且这个士兵类(Soldier)中含有枪类(Gun)这样一个属性,而这个属性则是由枪类(Gun)创建出来的一个对象。这就对应了我们前面提到的这个知识点,也就是说一个对象的属性可以是另外一个类创建的对象

从第二项和第三项需求我们可以知道,士兵(Solider)对象中有一个开火(fire)的方法,而开火则是由枪发射子弹,那么还要在枪类(Gun)中创建一个发射的方法(shoot)。

从第四项需求可以知道,枪里面还应该有一个子弹数量这个属性,同时还要给枪创建一个装填子弹方法。

因此总结如下:

  1. 需要创建一个士兵类(Soldier),这个类中含有2属性,一个是士兵的名字(name),一个是枪(gun),同时还要定义一个方法,即开火(fire);
  2. 需要创建一个枪类(Gun),这个类中含有2个属性,一个是型号(model),即AK47,还有一个是子弹数量(count),同时还要定义2个方法,即装填子弹(add_bullet)与射击(shoot)这两个方法。
  3. 这里还有一个问题,是先定义枪类,还是士兵类,根据前面的知识,哪个类要被使用,就先定义哪个类。在这个案例中,是士兵使用枪,就先定义枪类。因为如果我们先定义士兵类,那么在士兵类的内部,还要用到枪的对象,此时枪类还没有被定义,就会比较麻烦。个人觉得,这个思路就是从小范围到大范围,从局部到整体。

以上两个类的示意图如下所示:

创建枪(Gun)类

在枪这个类中,型号(model)需要外界传递,而子弹数量(bullet_count)这个属性,我们假定开始的时候是没有子弹的,设为0,子弹需要人工装填,因此这个属性在初始阶段不需要外界输入,从上面的类图可以知道,枪里还有一个装填子弹方法(add_bullet)和发射子弹方法(shoot),因此枪类代码如下所示:

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
class Gun:
def __init__(self, model):
# 1. 枪的型号
self.model = model
# 2. 子弹的数量
self.bullet_count = 0
def add_bullet(self, count):
self.bullet_count += count
def shoot(self):
# 1. 判断子弹数量
if self.bullet_count <= 0:
print("[%s] 没有子弹了..." % self.model)
return
# 2. 发射子弹
self.bullet_count -= 1
# 3. 提示发射信息
print("[%s] 突突突...[%d]" % (self.model, self.bullet_count))
ak47 = Gun("AK47")
ak47.add_bullet(50)
ak47.shoot()

运行结果如下所示:

1
[AK47] 突突突...[49]

代码能够正常运行,到此,枪类(Gun)的定义已经完成。

现在设计士兵类(Soldier),这个类中含有2个属性,分别是姓名(name)和枪(gun),不过在这里,先假设每一个新兵都没有枪。在这里还要提示一下,如果不知道设置什么初始值,可以设置为None

  • None关键字表示什么都没有;
  • 表示一个空对象,没有方法和属性,是一个特殊的常量;
  • 可以将None赋值给任何一个变量。

现在分析一下士兵类中的fire方法需求:

  1. 判断是否有枪,没有枪法没法冲锋;
  2. 减一声口号;
  3. 装填子弹;
  4. 射击。

完整代码如下所示:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Gun:
def __init__(self, model):
# 1. 枪的型号
self.model = model
# 2. 子弹的数量
self.bullet_count = 0
def add_bullet(self, count):
self.bullet_count += count
def shoot(self):
# 1. 判断子弹数量
if self.bullet_count <= 0:
print("[%s] 没有子弹了..." % self.model)
return
# 2. 发射子弹
self.bullet_count -= 1
# 3. 提示发射信息
print("[%s] 突突突...[%d]" % (self.model, self.bullet_count))
class Soldier:
def __init__(self, name):
# 1. 姓名
self.name = name
# 2. 枪 - 新兵没有枪
self.gun = None
def fire(self):
# 1. 判断士兵是否有枪
if self.gun == None:
print("[%s] 还没有枪..."% self.name)
return
# 2. 高喊口号
print("冲啊...[%s]"% self.name)
# 3. 让枪装填子弹
self.gun.add_bullet(50)
# 4. 发射子弹
self.gun.shoot()
ak47 = Gun("AK47")
# 2. 创建许三多
xusanduo = Soldier("许三多")
xusanduo.gun = ak47
xusanduo.fire()
print(xusanduo.gun)

运行结果如下所示:

1
2
3
冲啊...[许三多]
[AK47] 突突突...[49]
<__main__.Gun object at 0x000002E8B48D9668>

在这个案例中,我们学到的内容就是:如果我们要实现某个任务,这个任务中有两个类,例如A类与B类,那么通过A类创建的对象中的属性可以是B类来源的对象,此时A创建的这个对象中的属性就是能调用B类中的方法。这个案例我觉得有点复杂,笔记不太可能记得很详细,可以多看几遍视频。

身份运算符

再来看一下前面代码中的某一句,即if self.gun == None,选中后,会出现如下提示信息:

PyCharm的提示信息显示,如果与None进行比较时,最好使用isis not,而不是使用==。这里的is就是身份运算符。

身份运算符用于比较两个对象的内存地址是否一致,也就是说是否是对同一个对象的引用。在Python中,针对None进行比较时,建议使用is判断。Python中的身份运算符有2个,分别是isis not,它们的功能如下所示:

运算符 描述 实例
is is是判断两个标识符是不是引用同一个对象 x is y,类似id(x)==id(y)
is not is not是判断两个标识符是不是引用不同的对象 x is not y,类似id(x)!=id(y)

这里需要区分一下is==的区别

is用于判断两个变量引用对象是否为同一个;

==用于判断引用变量的值是否相等,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In [1]: a = [1, 2, 3]
In [2]: id(a)
Out[2]: 2681339468296
In [3]: b = [1, 2, 3]
In [4]: id(b)
Out[4]: 2681339465864
In [5]: a == b
Out[5]: True
In [6]: a is b
Out[6]: False

从上面的案例我们可以知道,a与b这两个列表的值相等,但引用不相等(也就是说这两个变量引用的内存地址不相同)。

None在Python中算是一个空对象,空对象不能把它理解为零,它是Python中的一个特殊的常量,指向内存中的一个地址,一个变量如果是None,它一定和None指向同一个内存地址。在上面的代码中,即if self.gun == None这句中,==后面接的是一个对象,因此最好使用is,因为is主要用于判断对象的引用,而不是值,因此Python建议使用is来判断None。

网上又检索到了一些资料,如下所示:

在区分is==这两种运算符区别之前,首先要知道Python中对象包含的三个基本要素,分别是:id(身份标识)、type(数据类型)和value(值)。is==都是对对象进行比较判断作用的,但对对象比较判断的内容并不相同。==比较操作符和is同一性运算符区别

==是python标准操作符中的比较操作符,用来比较判断两个对象的value(值)是否相等,is也被叫做同一性运算符,这个运算符比较判断的是对象间的唯一身份标识,也就是id是否相同。

私有属性和私有方法

应用场景及定义方式

应用场景

  • 在实际开发中,对象的某些属性和方法可能只希望在对象的内部被使用,而不希望在外部被访问到;
  • 私有属性就是对象不希望公开的属性;
  • 私有方法就是对象不希望公开的方法。

定义方式

  • 在定义属性和方法时,在属性名或者方法名前增加两个下划线,定义的就是私有属性或方法;
  • 我们先看一个最常规的案例,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Women:
def __init__(self, name):
self.name = name
self.age = 18
def secret(self):
print("%s 的年龄是%d"%(self.name, self.age))
xiaofang = Women("小芳")
print(xiaofang.age)
xiaofang.secret()

运行结果如下所示:

1
2
18
小芳 的年龄是18

在这个案例中,我们定义了一个女人类(Women),通过这个类创建了一个xiaofang对象,然后输出了这个对象的属性(age)与方法(secret)。

现在我们将age这个属性与方法改为私有属性,也就是在它们前面加两个下划线,变成__age,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Women:
def __init__(self, name):
self.name = name
self.__age = 18
def secret(self):
print("%s 的年龄是%d"%(self.name, self.__age))
xiaofang = Women("小芳")
# 私有属性在外界不能被直接访问
print(xiaofang.__age)
xiaofang.secret()

运行结果如下所示:

1
2
3
4
Traceback (most recent call last):
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/面向对象/hm_17_私有属性和方法.py", line 13, in <module>
print(xiaofang.__age)
AttributeError: 'Women' object has no attribute '__age'

运行结果出错,系统提示缺少属性__age。现在我们将print(xiaofang.__age)这句注释掉,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Women:
def __init__(self, name):
self.name = name
self.__age = 18
def secret(self):
# 在对象的方法内部,是可以访问对象的私有属性的
print("%s 的年龄是%d"%(self.name, self.__age))
xiaofang = Women("小芳")
# 私有属性在外界不能被直接访问
# print(xiaofang.__age)
xiaofang.secret()

运行结果如下所示:

1
小芳 的年龄是18

能够正常运行,这说明在对象的secret这个方法内部,可以访问这个对象的私有属性,也就是self.__age

现在我们把secret这个方法也改为私有方法,即改为__secret,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Women:
def __init__(self, name):
self.name = name
self.__age = 18
def __secret(self):
print("%s 的年龄是%d"%(self.name, self.__age))
xiaofang = Women("小芳")
# print(xiaofang.__age)
xiaofang.__secret()

结果运算如下所示:

1
2
3
4
Traceback (most recent call last):
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/面向对象/hm_17_私有属性和方法.py", line 15, in <module>
xiaofang.__secret()
AttributeError: 'Women' object has no attribute '__secret'

结果也无法运行,提示没有__secret这个方法。

伪私有属性和伪私有方法

在Python中并没有真正意义上的私有:

  • 在给属性、方法命名时,实际是对名称做了一些特殊处理,使得外界无法访问到;
  • 如果我们要强行访问这些智能属性与方法,处理方式就是在名称前面加上__类名=>_类名__名称

因此在Python中是有办法访问这些私有属性和私有方法,但在日常开发中,我们最好不要用这种方式来访问对象的私有属性或私有方法。

还来看一下前面的案例,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Women:
def __init__(self, name):
self.name = name
self.__age = 18
def __secret(self):
print("%s 的年龄是%d"%(self.name, self.__age))
xiaofang = Women("小芳")
print(xiaofang.__age)
# xiaofang.__secret()

运行结果如下所示:

1
2
3
4
Traceback (most recent call last):
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/面向对象/hm_18_伪私有属性和方法.py", line 13, in <module>
print(xiaofang.__age)
AttributeError: 'Women' object has no attribute '__age'

解释器提示,没有__age这个属性,现在我们改变一下代码,也就是将print(xiaofang.__age)这句改为print(xiaofang._Women__age),改动的地方就是将私有属性前面添加上类名,并在类名前面再加一个下划线,完整代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Women:
def __init__(self, name):
self.name = name
self.__age = 18
def __secret(self):
print("%s 的年龄是%d"%(self.name, self.__age))
xiaofang = Women("小芳")
print(xiaofang._Women__age)
# xiaofang.__secret()

运行结果如下所示:

1
18

通过这种方法,我们就能访问对象的私有属性。再来看一下__secret这个私有方法的访问,也是同样的方法,即xiaofang._Women__secret,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Women:
def __init__(self, name):
self.name = name
self.__age = 18
def __secret(self):
print("%s 的年龄是%d"%(self.name, self.__age))
xiaofang = Women("小芳")
print(xiaofang._Women__age)
xiaofang._Women__secret()

运行结果如下所示:

1
2
18
小芳 的年龄是18

我们现在也能访问这个私有方法,因此在Python中,没有绝对意义上的私有属性与私有方法,因此可以称为伪私有属性和伪私有方法

参考资料

  1. 黑马Python视频教程
  2. Python基础教程(第3版).Magnus Lie Hetland
  3. Python无师自通:专业程序员的养成