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

前言

这是黑马Python教程的笔记,内容是有关面向对象。

什么是类

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

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

创建类

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

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

属性和方法的确定

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

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

  • 小明今年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键,后面就会出现这个对象能使用的方法,如下所示:

In [3]: gl_list = []

In [4]: gl_list.
    append() count() insert() reverse()
    clear() extend() pop() sort()
    copy() index() remove()

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

In [6]: def demo():
   ...:     """这是一个测试函数"""
   ...:     print("Hello python")
   ...:

In [7]: demo()
Hello python

In [8]: demo.

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

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__,如下所示:

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

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

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

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

定义简单的类

格式

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

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

创建对象

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

对象变量=类名()

创建对象

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

分析:

  1. 定义一个猫类,Cat()
  2. 定义两个方法eatdrink
  3. 此时并没有涉及属性,因此我们不用定义属性。
class Cat:

    def eat(self):
        print("小猫爱吃鱼")

    def drink(self):
        print("小猫要喝水")


# 创建猫对象
tom = Cat()

tom.eat()
tom.drink()

运行结果如下所示:

小猫爱吃鱼
小猫要喝水

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

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

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

创建多个对象

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

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)

我们来看一下结果:

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

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

lazy_cat2 = lazy_cat
print(lazy_cat2)
print(lazy_cat)

结果如下:

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

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

方法中的self参数

给对象添加属性

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

tom.name = "Tom"

输出对象属性

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

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

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()

结果如下所示:

Tom 爱吃鱼
大懒猫 爱吃鱼

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

初始化方法

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

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

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

class Cat:

    def __init__(self):

        print("这是一个初始化方法")


tom = Cat()

运行结果如下所示:

这是一个初始化方法

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

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

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

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

class Cat:

    def __init__(self):

        print("这是一个初始化方法")

        # self.属性名 = 属性的初始值
        self.name = "Tom"


tom = Cat()

print(tom.name)

运行结果,如下所示:

这是一个初始化方法
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,如下所示:

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,如下所示:

这是一个初始化方法
Tom
这是一个初始化方法
Tom爱吃鱼

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

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

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()

运行结果如下所示:

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

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

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

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

内置方法和属性

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

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

__del__方法

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

应用场景

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

生命周期

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

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

代码A:

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)

结果如下所示:

Tom 来了
Tom
--------------------------------------------------
Tom 我去了

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

代码B:

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)

运行结果如下所示:

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__方法必须返回一个字符。

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

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)

运行结果如下所示:

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

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

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)

运行结果如下所示:

Tom 来了
我是小猫
Tom 我去了

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

Tom 来了
我是小猫[Tom]
Tom 我去了

面向对象封装案例

封装

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

案例分析——小明爱跑步

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

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

代码分析

代码如下所示:

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)

运行结果如下所示:

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

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

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,如下所示:

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)

结果如下所示:

小明 爱跑步, 跑步锻炼身体
小明 是吃货,吃完这顿再减肥
我的名字叫 小明 体重是 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个属性。

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

定义家具类

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

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)

结果运行如下所示:

[床] 占地 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个属性的代码如下所示:

class House:

    def __init__(self, house_type, area):

        self.house_type = house_type 
        self.area = area

这段代码定义了房子的2个需要传入的属性,现在再定义剩余面积(free_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))
    
    # 创建房子对象
my_house = House("两室一厅", 60)

my_house.add_item(bed)
# 添加一张床

my_house.add_item(chest)
# 添加一个衣柜

my_house.add_item(table)
# 添加一张餐桌

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

[床] 占地 4.00
[衣柜] 占地 2.00
[餐桌] 占地 1.50
要添加 [床] 占地 4.00
要添加 [衣柜] 占地 2.00
要添加 [餐桌] 占地 1.50
户型:两室一厅 
总面积: 60.00[剩余: 60.00]
家具: []

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

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

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

    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

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

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)

运行结果如下所示:

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

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

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

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

[床] 占地 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. 这里还有一个问题,是先定义枪类,还是士兵类,根据前面的知识,哪个类要被使用,就先定义哪个类。在这个案例中,是士兵使用枪,就先定义枪类。因为如果我们先定义士兵类,那么在士兵类的内部,还要用到枪的对象,此时枪类还没有被定义,就会比较麻烦。个人觉得,这个思路就是从小范围到大范围,从局部到整体。

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

image

创建枪(Gun)类

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

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()

运行结果如下所示:

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

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

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

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

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

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

完整代码如下所示:

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)

运行结果如下所示:

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

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

身份运算符

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

image

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用于判断两个变量引用对象是否为同一个;

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

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是否相同。

私有属性和私有方法

应用场景及定义方式

应用场景

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

定义方式

  • 在定义属性和方法时,在属性名或者方法名前增加两个下划线,定义的就是私有属性或方法;
  • 我们先看一个最常规的案例,如下所示:
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()

运行结果如下所示:

18
小芳 的年龄是18

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

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

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()

运行结果如下所示:

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)这句注释掉,如下所示:

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()

运行结果如下所示:

小芳 的年龄是18

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

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

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()

结果运算如下所示:

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中是有办法访问这些私有属性和私有方法,但在日常开发中,我们最好不要用这种方式来访问对象的私有属性或私有方法。

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

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()

运行结果如下所示:

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),改动的地方就是将私有属性前面添加上类名,并在类名前面再加一个下划线,完整代码如下所示:

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()

运行结果如下所示:

18

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

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()

运行结果如下所示:

18
小芳 的年龄是18

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容