跳转至

面向对象进阶

add_circle2025-03-03update2025-03-03

1. 三大特征

1.1 封装

没有学习封装之前,效果如下:

class Person(object):

    def __init__(self, name, age):
        self.name = name
        self.age = age

xiaoming = Person("alvin",16)

xiaoming.age = 1000
print(xiaoming.age)
xiaoming.age = -10
print(xiaoming.age)
# 可以看到,外界可以随意修改数据,并可能把数据设置成一个不合理的脏数据。

所谓的封装,就是对类中成员属性和方法进行保护,设置对应的访问权限控制外界对类的内部成员的访问,修改,删除等操作,有条件的开放外界对数据的操作。

1.1.1 访问权限

常见的编程语言中对于类或对象的成员保护与访问机制,分3个等级:

访问权限 描述 格式
私有的
(private)
只能在当前类内部可以访问与操作,类的外部不可以。 __方法名
__属性名
公有的
(public)
在当前类内部和外部都可以访问操作。 方法名
属性名
受保护的
(protected)
在当前类内部和子类内部可以访问,外界无法访问。
python原生不支持,但是python开发者都约定俗成了左边单个下划线开头属于受保护的
_方法名()
_属性名

Python对成员的保护只有两个等级:私有的(private)和公有的(public)。在java,php,c++等编程语言里面就存在3种等级。

import re

class Person(object):
    def __init__(self, name):
        self.__name = name

    def get_name(self):
        return self.__name

    def set_name(self, name):
        if self.__check_name(name):
            self.__name = name
        else:
            raise Exception("名字长度不能大于7个字符!")

    def __check_name(self, name):
        return len(re.split("", name)[1:-1]) < 5

xiaoming = Person("小明")
# print(xiaoming.__name) # 报错!
xiaoming.set_name("小红")
# xiaoming.__check_name("小红") # 报错!
print(xiaoming.get_name())
xiaoming.set_name("小红五一到此一游") # 报错!

所以,所谓的封装其实就是为了让一些属性和方法,不能被外界直接修改和调用而已。对数据的修改,可以通过提供公有方法的方式给外界操作,那么外界操作时,我们就可以设置一些判断了。而判断名字这种方法,外界没有必要使用到,所以我们可以设置为私有级别,这样,对于外界来说,就不存在这个判断名字的方法了。

注意:类中不仅可以把实例属性和实例方法设置私有的或公有的,类的类属性和类方法也可以这么设置,只是很少有人这么用而已,因此我们这里不作介绍。

1.1.2 属性封装

使用接口函数获取修改数据 和 使用赋值设置数据相比, 赋值设置数据使用起来更方便,我们有什么方法达到 既能使用赋值方式,同时又能在赋值时直接调用到我们定义的方法接口对数据校验呢?有的,答案就是@property(属性装饰器)。

class Student(object):
    def __init__(self, name, age):
        self.__name = name
        self.__sex = sex

    @property
    def name(self):
        return self.__name

    @name.setter
    def name(self,name):
        if len(name) > 1 :
            self.__name = name
        else:
            print("name的长度必须要大于1个长度")

注意,使用 @property 装饰器时,接口名不必与属性名相同。

python还提供了更加人性化的操作,可以通过property函数限制方式完成只读、只写、读写、删除等各种操作

class Person(object):
    def __init__(self, name):
        self.__name = name

    def __get_name(self):
        return self.__name

    def __set_name(self, name):
        self.__name = name

    def __del_name(self):
        del self.__name
    # property()中定义了读取、赋值、删除的操作
    # name = property(__get_name, __set_name, __del_name)
    name = property(__get_name, __set_name)

xiaoming = Person("xiaoming")

print(xiaoming.name)    # 合法:调用__get_name
xiaoming.name = "小明"  # 合法:调用__set_name
print(xiaoming.name)

# property中没有添加__del_name函数,所以不能删除指定的属性
del xiaoming.name  # 错误:AttributeError: can't delete Attribute

@property广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。

1.2 继承

面向对象的编程带来的主要好处之一是复用代码,实现复用代码的方法之一是通过类的继承机制。

1.2.1 基本使用

通过继承创建的新类称为子类派生类后代类,被继承的类称为基类父类超类

class 子类名(父类名):
    pass

继承是使用已有父类作为基础创建子类的一种方式。子类的定义可以增加新的属性或方法,也可以复用或覆盖父类已有的属性和方法。

注意:其他的语言,是可以选择性的复用父类属性和方法,而python中默认是全局复用的。

使用继承之前:

class Student(object):
    """学生类"""
    def __init__(self, name, age, achievement):
        self.name=name
        self.age=age
        self.achievement = achievement

    def eat(self):
        print("eating...")

    def sleep(self):
        print("sleep...")

    def do_homework(self):
        print("do_homework.....")

    def do_read(self):
        print("reading........")

class Teacher(object):
    """老师类"""
    def __init__(self, name, age, salary):
        self.name=name
        self.age=age
        self.salary = salary

    def eat(self):
        print("eating...")

    def sleep(self):
        print("sleep...")

    def teaching(self):
        print("teaching.....")

使用继承以后:

class Person(object):
    """人类"""
    def eat(self):
        print("eating...")

    def sleep(self):
        print("sleep...")

class Student(object):
    """学生类"""
    def __init__(self, name, age, achievement):
        self.name=name
        self.age=age
        self.achievement = achievement

    def do_homework(self):
        print("do_homework.....")

    def do_read(self):
        print("reading........")

class Teacher(object):
    """老师类"""
    def __init__(self, name, age, salary):
        self.name=name
        self.age=age
        self.salary = salary

    def teaching(self):
        print("teaching.....")

继承不仅可以复用父类的方法,也可以复用父类的属性,甚至增加属于自己的属性。

class Person(object):
    """人类"""
    def __init__(self,name,age):
        self.name=name
        self.age=age

    def eat(self):
        print("eating...")
    def sleep(self):
        print("sleep...")

class Student(Person):
    """学生类"""
    def __init__(self, name, age, achievement):
        super().__init__(name, age)
        self.achievement = achievement

    def do_homework(self):
        print("do_homework.....")
    def do_read(self):
        print("reading........")

class Teacher(Person):
    """老师类"""
    def __init__(self, name, age, salary):
        super().__init__(name, age)
        self.salary = salary

    def teaching(self):
        print("teaching.....")

调用父类的属性或方法有三种写法:

格式 写法
子类名.方法(self, 参数列表) Person.__init__(self, name, age)
super(子类名, self).方法(参数列表) super(Student, self).__init__(name,age)
super().方法(参数列表) super().init(name, age)

继承不仅可以复用父类的方法,还可以重写父类的方法。

注意:此处为重写!不是重载,Python不支持重载。重写是同一个方法在父类和子类中同时重现,而子类改写了父类,而重载是同一个方法多次出现在同一个类中,还不会被覆盖,可以正常使用。java和C++这种强类型的面向对象的语言则支持方法重载。当然,Python支持*args和**kwargs缺省参数的操作,所以Python本身可以实现类似重载效果。

class Person(object):

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def sleep(self):
        print("sleep...")


class Student(Person):
    def sleep(self, where):
        if where != "课堂":
            super().sleep()
        else:
            print("不能睡觉!")

s = Student("小明", 17)
s.sleep("课堂")

继承过程中,实际上继承者(子类)是被继承者(父类)的特殊化。子类除了拥有父类的特性以外,还拥有自己独有的特性。例如:动物是父类,猫和牛是子类,猫有抓老鼠、爬树行为等,但是牛类不一定有,所以作为父类的动物自然也就没有。因此在继承关系中,继承者(子类)完全可以替换被继承者(父类),反之则父类不可以完全替换子类,例如我们可以说猫是动物,但不能说动物是猫就是这个道理。这种情况,我们可以将其称之为“向上转型” 。

因此,在使用继承时需要记住三句话:

1、子类只能继承父类的公有属性和方法,无法继承父类的私有属性和私有方法。

2、子类可以拥有自己属性和方法,即子类可以对父类进行扩展。

3、子类可以用自己的方式重写父类的方法。

python中,子类不仅可以继承一个父类的属性与方法,还可以继承多个类的属性与方法。

如果声明类时,当前类只继承一个父类,则这种方式就是单继承,类似生活中的父子关系,小明只有一个生物学爸爸,他爸爸也只有一个生物学爸爸,代代相传。

如果声明类时,当前类同时继承了多个父类,则这种方式就是多继承,类似生活中的师生关系,小明读书时有多个不同学科的老师,每个老师小时候读书时也有多个不同学科的老师,代代相传。

image-20220422014154061

1.2.2 单继承

蝙蝠和老鹰都可以飞,飞的功能就重复定义了。

class Animal(object):

    def eat(self):
        print("eating...")

    def sleep(self):
        print("sleep...")

class Eagle(Animal):

    def fly(self):
        print("fly...")
    def hunt(self):
        print("hunt....")

class Bat(Animal):

    def fly(self):
        print("fly...")

    def upside_down(self):
        print("upsize down ....")

此时,我们可以使用多层单继承解决

class Animal(object):

    def eat(self):
        print("eating...")

    def sleep(self):
        print("sleep...")

class FlyAnimal(Animal):
    def fly(self):
        print("fly...")

class Eagle(FlyAnimal):
    def hunt(self):
        print("hunt....")

class Bat(FlyAnimal):     
    def upside_down(self):
        print("upsize down ....")

上面的操作中,我们单独设计了一种飞行动物类出来,解决了代码重复的问题,但是有时候,会飞的不一定是动物,也可以能风筝或者是飞机。这样的话,那么我们可以单独的设计出一种会飞行的类型的出来即可。那么,我们如果要让老鹰、蝙蝠,飞机都可以继承的话,就要使用多继承了。

1.2.3 多继承

class Animal(object):

    def eat(self):
        print("eating...")

    def sleep(self):
        print("sleep...")

class Fly(Animal):
    def fly(self):
        print("fly...")

class Eagle(Animal, Fly):
    def hunt(self):
        print("hunt....")

class Bat(Animal, Fly):
    def upside_down(self):
        print("upsize down ....")

class Plane(Fly):
    pass

写多继承时,已尽量避免造成不同类相同方法名的情况,尽量遵守单一职责(SOLID之S,面向对象五个基本原则之一),提高代码质量,追求高内聚(一个模块只完成一个任务,专一性高),低耦合(模块与模块之间可以彼此独立不冲突,方便移植复用)。

1.2.4 菱形继承

也叫钻石继承。工作中,避免出现菱形继承,容易找不清调用顺序。

class Human(object):
    id = 1

    def eat(self):
        print("1-饿了去打猎")
        print(self.id)
        print("2-抓到猎物,直接生吃!")


class Man(Human):
    id = 2

    def eat(self):
        print("3-饿了去买酒卖肉")
        print(super().id)
        print("4-一边喝酒,一边吃肉!")


class Woman(Human):
    id = 3

    def eat(self):
        print("5-饿了去煮饭买菜")
        print(super().id)
        print("6-动作优雅,小口尝试!")


class Children(Man, Woman):
    id = 4

    def eat(self):
        print("7-饿了就哭.")
        print(super().id)
        print("8-吃了接着哭。")


c = Children()
c.eat()

那么,上面的菱形继承中,Children到底改写的是哪个父类的方法呢?

上面问题的根源都跟MRO(Method Resolution Order,方法解析顺序)有关,主要用于在多重继承时判断当前类调用的属性或方法来自于哪个父类,其使用了一种叫做C3的算法,基本思想是在避免同一类被调用多次的前提下,使用广度优先还是深度优先的原则去寻找需要的属性和方法。,当然这主要跟我们使用的类声明方式有关。前面学习中,我们知道Python类声明方式分为两种,一种叫经典类,一种叫新式类

  • 新式类:从object继承来的类。(如:class 类名(object)),采用广度优先搜索的方式继承(即先水平搜索,再向上搜索)。
  • 经典类:不从object继承来的类。(如:class 类名() 或 class 类名),采用深度优先搜索的方式继承(即先深入继承树的左侧,再返回,再找右侧)。

1.3 多态

多态的概念是应用于Java和C#这一类强类型语言中,而Python的多态的特点则是如下:

  • 继承以后子类重写父类方法,不同子类对于同一个父类方法可以有不同的实现方式。龙生九子,子子不同。

  • 崇尚和支持"鸭子类型(DuckType)"。

    所谓鸭子类型就是编程语言在函数的参数检测一个对象是否符合类的要求时,并非绝对的判断当前对象是否属于指定类的实例,而是根据当前对象是否实现了当前类的方法与属性来决定的。

class Payment(object):
    def __init__(self, name, money):
        self.money = money
        self.name = name

    def pay(self, *args, **kwargs):
        pass


class AliPay(Payment):

    def pay(self):
        # 支付宝支付渠道
        print('%s通过支付宝消费了%s元' % (self.name, self.money))


class WeChatPay(Payment):

    def pay(self):
        # 微信支付渠道
        print('%s通过微信消费了%s元' % (self.name, self.money))


class Order(object):
    @staticmethod
    def account(pay_obj: Payment):
        pay_obj.pay()


pay1 = WeChatPay("moluo", 100)
pay2 = AliPay("xiaoming", 200)

order = Order()
order.account(pay1)
order.account(pay2)

面向对象五个基本原则,也叫:

  • 单一职责(Single Responsibility Principle,简称SRP):将不同的职责功能分开封装到不同的类或模块中。尽量一个类/模块/函数只完成一件事情。
  • 开闭原则(Open Closed Principle,简称OCP):对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的。对扩展开放即基于多态思想,允许子类重写父类方法,让不同子类在调用同一个父类方法时效果不同。对修改进行封闭的,即基于封装思想,选择性开发修改数据的操作,
  • 里氏替换原则(Liskov Substitution Principle,简称LSP):派生类(子类)可以在代码中代替其基类(超类)对象。
  • 接口隔离原则(Interface Segregation Principle,简称ISP):一个类中如果有调用了另一个类的方法或属性时,应该将依赖性建立在最小的接口上的,避免出现一个类修改了影响污染另一个类。
  • 依赖反转原则(Dependence Inversion Principle,简称DIP):程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
  • 最少知识原则(LeastKnowledge Principle, 简称LKP):一个软件实体应当尽可能少地与其他实体发生相互作用,减少副作用。

2. 魔术方法

魔术方法,也叫魔术函数,就是在特定条件下会自动执行的方法(函数)。

注意:类中所有魔术方法都是私有方法,只会在类的内部自动执行,无法被外界调用。

固定方法名 描述 执行条件
__init__ 构造方法,当对象初始化会自动调用,常用于对象属性的初始化。 类名()
__new__ 创建实例方法,是调用类时自动执行的第1个魔术方法,也是一个类方法。
__init__的执行时间要早,返回值是实例对象,也就是__init__中的self。
类名()
__del__ 析构方法,当对象被删除时,或者被系统内存回收时,自动执行。 del 对象
__call__ 可调用协议方法,当对象被当成函数时,自动执行 对象()
__str__ 打印对象方法,当对象被print打印时,自动执行 print(对象)
__repr__ 打印对象方法,当对象被终端下输出时,自动执行,类似__str__ > 对象
__enter__ 执行上下文协议方法,当对象写在with语句后面时,自动执行 with 对象
__exit__ 执行上下文协议方法,当对象写在with语句后面,而with语句执行结束时,自动执行 with 对象
__setitem__ 序列协议方法,当对象被当成序列操作,设置索引/键时,自动执行 对象[索引] = 值
__getitem__ 序列协议方法,当对象被当成序列调用,读取索引/键时,自动执行 对象[索引]
__delitem__ 序列协议方法,当对象被当成序列调用,删除索引/键时,自动执行 del 对象[索引]
__len__ 序列协议方法,当对象被当成序列调用,使用len获取长度时,自动执行 len(对象)
__getattr__ 属性读取方法,当对象的属性被访问,而属性不存在时,自动执行 对象.属性
__setattr__ 属性读取方法,当对象的属性被修改/赋值时,自动执行 对象.属性 = 值
__delattr__ 字典协议方法,当对象的属性被删除时,自动执行 del 对象.属性
__iter__ 可迭代协议方法,当对象被当成可迭代对象调用,使用iter函数或者for循环时,自动执行 for item in 对象:
__next__ 迭代器协议方法,当对象被当成可迭代对象调用,for循环时,自动执行 for item in 对象:

2.1 __init____new__方法

类名() 创建对象时,在自动执行 __init__()方法前,会先执行 object.__new__方法,在内存中开辟对象空间并返回对象提供给__init__()

class Persion(object):
    def __new__(cls, *args, **kwargs):
        print("Persion.__new__ 被调用了")
        return object.__new__(cls)

    def __init__(self, name, age):
        print("Persion.__init__被调用了")
        self.name = name
        self.age = age

"""
__new__  用来制造对象的
__init__ 用来初始化对象的
"""
xiaoming = Persion("小明", 16)
print(xiaoming.name)
print(xiaoming.age)

__init__ 只能初始化当前类的对象,其他对象不触发,也不会被自动不调用。

__new__可以返回其他类的对象,但不会触发当前类的__init__魔术方法。

class A(object):
    def __init__(self):
        print(1)
    a = 90

obj1 = A()

class B(object):
    def __new__(cls, *args, **kwargs):
        return obj1

    # init 不会初始化其他类的对象,不调用
    def __init__(self, *args, **kwargs):
        print(2)


obj2 = B()
print(obj2, type(obj2))

2.1.1 单例模式

基于__new__上面的特点,我们在开发中,就可以控制类创建对象的过程了。在开发中,有时候需要在整个程序运行过程中,保证一个类不管实例化多少次,生成的都是同一个对象,例如,购物车,日志,数据库操作等。

当然,在软件设计中,有一种设计模式,叫单例模式(Singleton Pattern,也叫单态模式):保证一个类仅有一个实例,并提供一个访问它的全局访问点。

class Sington(object):
    # 定义一个私有的成员属性__instance , 用来存储接下来创建的唯一实例对象.
    __instance = None
    def __new__(cls):
        # 如果类的私有成员属性__instance 不存在 是一个None,那么通过父类object的new创建一个对象
        if cls.__instance is None:
            # 创建对象
            instance = object.__new__(cls)
            # 把对象存在私有成员属性__instance当中
            cls.__instance = instance
        # 将该类对象直接返回
        return cls.__instance

# 第一次 触发cls.__instance is None 返回真,创建对象
obj1 = Sington()
print(id(obj1))
# 第二次 触发__new__ 发现cls.__instance is None 返回假 直接return cls.__instance
obj2 = Sington()
print(id(obj2))
# 第三次 与 第二次相同
obj3 = Sington()
print(id(obj3))
print(obj1 is obj2)
print(obj1 is obj3)

2.2 __del__方法

析构方法,当对象在内存中被释放时,自动触发执行。此方法一般无须定义,因为Python是一门高级语言,程序员在使用时无需关心内存的分配和释放,因为此工作都是交给Python解释器来执行,所以析构函数的调用是由解释器在进行垃圾回收时自动触发执行的。当然,如果在对象调用结束以后,需要手动完成一些收尾的工作时,可以把代码写在该魔术方法中。

class Person(object):

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __del__(self):
        print("%s删除了" % self.name)


xiaoming = Person("小明", 16)

# del xiaoming
print("程序执行完了!")

2.3 __call__方法

python中一切皆对象,函数也是对象,同时也是可调用对象(callable)。一个类的实例对象要变成一个可调用对象,只需要实现魔术方法__call__()

class Person(object):
    def __init__(self, name):
        self.name = name

    def __call__(self, friend):
        print(f"我叫{self.name}")
        print(f"我的朋友是{friend}")

p = Person('小明')
p('小红')

# 判断一个对象是否是可调用对象,所谓的可调用对象,就是内部实现了__call__可调用协议的对象。
callable(p)

2.4 __str____repr__方法

Python 定义了__str__()__repr__()两种方法提供打印对象的功能,__str__()用于把对象转换成字符串显示给用户,而__repr__()用于在终端输出时显示对象信息给开发人员的。

class Person(object):
    def __init__(self, name, sex):
        self.name = name
        self.sex = sex
    def __str__(self):
        return f"(Person: {self.name}, {self.sex})"
    __repr__ = __str__

2.5 __enter____exit__方法

python中,经常使用with上下文管理器来完成文件文件,网络文件或软件等操作,如果我们要声明一个对象,用于处理一些指定类型的文件或网络上的文件的话,也可以通过封装对象,让对象支持在with语句中使用。

class Person(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # __exit__ 有2个作用:
        print("1. with语句结束了,删除或清理当前对象的相关信息")
        print("2. 处理with语句运行过程中发生的异常,当__exit__返回结果为True,则异常被屏蔽掉")
        print(exc_type, exc_val, exc_tb)
        return True


with Person("小明", 16) as p:
    print(p.name)
    print(p.age)
    raise Exception("报错!!")

print(p.name)

2.6 item系列和__len__方法

序列协议方法,当对象被当成序列操作,设置、读取、删除索引/键时,自动执行。

class Person(object):

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __getitem__(self, item):
        print('obj[key]取值时,执行__getitem__')
        print("取值为:",self.__dict__[item])

    def __setitem__(self, key, value):
        print('obj[key]=value赋值时,执行__setitem__')
        self.__dict__[key] = value

    def __delitem__(self, key):
        print('del obj[key]触发')
        self.__dict__.pop(key)

    def __len__(self):
        return len(self.__dict__)

# obj.["key"]的方式触发__xxxitem__魔法方法
xiaoming = Person("小明", 16)
name = xiaoming["name"]  # 触发__getitem__执行
xiaoming["age"] = 18  # 触发__setattr__执行

del xiaoming["age"] # 触发__delitem__

print( len(xiaoming) )

2.7 attr系列方法

class Person(object):

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __getattr__(self, item):
        print('1. 获取 obj.attr时触发,属性不存在的时候才会触发')

    def __setattr__(self, key, value):
        print('obj.attr=value时触发,添加修改属性时可以使用')
        # self.key=value # 这就无限递归了,因为每次修改属性都会来到这里。
        self.__dict__[key] = value  # 应该使用它

    def __delattr__(self, item):
        print('del obj.attr 时触发,删除属性的时候会触发')
        # del self.item #无限递归了
        self.__dict__.pop(item)


# 对象.属性的方式会自动触发__xxxattr__系列的魔法方法
xiaoming = Person("小明", 16)
print(xiaoming.name)

print(xiaoming.__dict__)

3. 吃鸡案例

3.1 OOA分析

img

3.2 OOD设计

3.2.1 玩家

  • 属性
    • 姓名
    • 血量
    • 持有的枪支
  • 方法
    • 装填子弹
    • 安装弹夹
    • 拿枪(持抢)
    • 开枪

3.2.2 子弹类

  • 属性
    • 杀伤力
  • 方法
    • 伤害敌人(让敌人掉血)

3.2.3 弹夹类

  • 属性
    • 子弹容量(子弹存储的最大值)
    • 当前保存的子弹列表
  • 方法
    • 保存子弹(安装子弹的时候)
    • 弹出子弹(开枪的时候)

3.2.4 枪类

  • 属性
    • 弹夹(默认没有弹夹,需要安装)
  • 方法
    • 使用弹夹
    • 发射子弹(射击)

3.3 OOP实现

class Player(object):
    """玩家"""
    def __init__(self, name):
        """初始化"""
        self.name = name  # 游戏昵称
        self.hp = 100  # 血量
        self.gun = None  # 枪支

    def __str__(self):
        """打印玩家信息"""
        return str(self.__dict__)

    def loading_bullet(self, clip, bullet):
        """装填子弹"""
        clip.save_bullet(bullet)

    def use_clip(self, gun, clip):
        """使用弹夹"""
        gun.use_clip(clip)

    def use_gun(self, gun):
        """使用枪支"""
        self.gun = gun

    def attack(self, enemy):
        """攻击敌人"""
        self.gun.shoot(enemy)

    def blood_loss(self, aggressive):
        """掉血"""
        self.hp -= aggressive


# 弹夹类
class Clip(object):
    def __init__(self, capacity):
        self.capacity = capacity # 子弹容量
        self.bullet_list = []  # 子弹列表

    def __str__(self):
        return f"弹夹当前的子弹数量为:{len(self.bullet_list)}/{self.capacity}"

    def save_bullet(self, bullet):
        """装填子弹"""
        if len(self.bullet_list) < self.capacity:
            self.bullet_list.append(bullet)

    def has_bullet(self):
        """判断当前弹夹中是否还有子弹"""
        if len(self.bullet_list) > 0:
            # 获取最后压入到单间中的子弹
            bullet = self.bullet_list[-1]
            self.bullet_list.pop()
            return bullet
        else:
            return None


# 子弹类
class Bullet(object):
    def __init__(self, aggressive):
        self.aggressive = aggressive

    def hurt(self, enemy):
        """伤害敌人"""
        enemy.blood_loss(self.aggressive)


# 枪类
class Gun(object):
    def __init__(self):
        self.clip = None  # 弹夹

    def __str__(self):
        if self.clip:
            return f"{self.clip}"
        else:
            return "枪没有弹夹"

    def use_clip(self, clip):
        """给枪安装弹夹"""
        self.clip = clip

    def shoot(self, enemy):
        """射击"""
        bullet = self.clip.has_bullet()
        if bullet:
            bullet.hurt(enemy)
        else:
            print("没有子弹了,放了空枪....")


# 创建一个玩家
xiaoming = Player("小明")
print(xiaoming)
# 创建一个弹夹
clip = Clip(30)
print(clip)

# 循环的方式创建子弹,然后让玩家小明把这些子弹压入到弹夹中
i = 0
while i < 30:
    bullet = Bullet(5)
    xiaoming.loading_bullet(clip, bullet)
    i += 1
# 测试一下,安装完子弹后,弹夹中的信息
print(clip)

# 创建一个枪对象
gun = Gun()
print(gun)
# 把弹夹安装到枪上
gun.use_clip(clip)
print(gun)
# 让玩家小明拿起枪
xiaoming.use_gun(gun)
print(xiaoming)


# 创建一个敌人:玩家小红
xiaohong = Player("小红")
print(xiaohong)

# 让玩家小红拿起枪
xiaohong.use_gun(gun)

# 玩家小明开枪射敌人:小红
xiaoming.attack(xiaohong)
print(xiaohong)
print(clip)

# 小明接着开枪
xiaoming.attack(xiaohong)
print(xiaohong)
print(clip)