跳转至

面向对象基础

add_circle2025-03-03update2025-03-03

Python是一门面向对象的编程语言。python的世界观:万物皆为对象。

image-20220419234318592

1. 基本概念

1.1 OO与PO

面向对象(Object-Oriented,简称OO),是一种解决问题的思想,与之相对的还有面向过程(Procedure Oriented,简称PO)。当然,不管是面向对象还是面向过程,其实都是解决问题的思路而已,并没有什么优劣之分,仅仅是解决问题过程中不同的方案而已,并且面向对象也不是什么新玩法,面向过程是1973年出现的,而面向对象则早在1967年就有了。

这里,我们可以取生活中的一个例子来简单区分下面向对象和面向过程这两种思想。例如,小明五一要从北京去深圳旅游。

如果是使用面向过程的思想解决旅游这件事情,则会把解决问题的过程划分成多个步骤,每个步骤分别解决整个旅游这件事情中所会遇到的每一个问题和困难。

1. 是否有钱有时间
   没有OK别想了

2. 订票先确定旅游的交通工具然后选择不同的APP或者到线下去买票
   买不到票别扯了这年头还有走路上京赶考的么去不了啊

3. 订酒店先选择一个目的地附近的酒店然后确定入住时间和离开时间
   没酒店没房间阿西吧洗洗睡吧

4. 景点是否开放
   不开放去干吗别去了

从上面可以看到,面向过程这种解决问题的思想的特点就是:分析出解决问题所需要的每一个步骤,按先后顺序进行解决,解决了上一个步骤以后才需要考虑下一个步骤,当所有步骤解决完成,则问题就解决了。

如果是使用面向对象的思想解决旅游这件事情,则会把解决问题的结果作为导向,然后分析出能解决问题的对象出来,让对象去解决问题。

1. 旅行社:告诉小明旅行团需要的钱和出行时间。
   钱不对?或者出行时间不对?小名换个旅行社

2. 旅行社:订票这事,你不用管了,我买完了。

3. 旅行社:酒店这事,你不用管了,我订好了。

4. 旅行社:景点这事,你不用管了,我安排了。

从上面可以看到,面向对象这种解决问题的思想的特点就是:分析出解决问题所需要的对象,让对象去解决问题,最终得到解决整个事情的结果。

因此,基于面向过程思想去旅游,小明会知道旅游过程中所需要解决的所有过程,包括酒店、交通工具、景点等费用,一目了然。但是,如果小红也想去旅游,则除非完整复制小明这次旅游的过程,否则只能仅仅作为参考。。。

反之,基于面向对象思想去旅游,小明不需要考虑各种解决问题的细节,只需要准备好钱和时间,剩下的交给旅行社这个对象去解决。当然,小明是不会知道酒店、交通工具、景点等真正费用的,不过,如果小红想去旅游,则小明只需要介绍旅行社给小红即可,这个过程是完全可以复制的。

总结:

  • 面向对象以对象为核心,不关注解决问题的细节,只关注结果。整个过程可以复用。
  • 面向过程以过程为核心,关注解决问题的步骤,而结果则是一个个步骤承上启下执行完成以后产生的。整个过程不好复用。

1.2 OOP与POP

面向对象编程(Object-Oriented Programming,简称 OOP)是一种基于面向对象思想为指导编写程序的一种编程风格或者编程规范。同理,面向过程编程(Procedure-Oriented Programming,简称POP)则是一种基于面向过程思想为指导编写程序的一种编程风格或者编程规范。

面向对象编程(OOP)经常和面向对象分析(OOA)面向对象设计(OOD) 放在一块讨论,OOA、OOD、OOP 三个连一起就是面向对象分析、设计、实现(编程),正好是面向对象软件开发要经历的三个阶段。它以类与对象两种数据结构来组织代码的,并以封装、继承、多态 三个特性作为基础设计与实现代码的。

封装(Encapsulation):基于类与对象的语法结构,把代表数据的变量和操作数据的函数进行封装成一个类或对象,通过类与对象语法对外公开少部分的数据操作。
继承(Inheritance):基于类的单继承或多继承语法,把重复的代码封装到父类(超类)中,然后子类(后代类)去复用父类封装号的代码。
多态(Polymorphism):基于接口(interface)、抽象类(Abstract class)或鸭子类型(duck type)的语法特点,让子类(后代类、派生类)的代码可以覆盖/修正父类封装的代码。

所以某种程度来说,面向对象的出现主要是为了解决代码冲突和代码复用的问题的。

在开发中,代码语法中支持类与对象这种数据结构的编程语言都统称为面向对象语言,如:java,C++,python,php,javascript,rust,go等。
而代码语法中不支持类与对象这种数据结构的编程语言则统称为面向过程语言,如:C,Fortran

事实上而言,面向对象和面向过程的编程语言实际上区分并不大,因为不熟悉面向对象的人会在面向对象编程语言中常常写出面向过程风格的代码,而熟悉面向对象的人自然也可以在面向过程的编程语言中写出面向对象风格的代码,仅仅是因为每个开发者编程能力水平的不同而已。所以学习面向对象以后,我们也不过是多掌握了一种不同风格的代码写法而已。

2. 类与对象

前面说到,面向对象编程是以类(Class)与对象(Object或Instance)2种数据结构组织代码的。所以python中要掌握面向对象编程,则需要学习类与对象这两种数据结构的基本语法。类与对象是面向对象编程的2个非常重要的概念。

2.1 基本概念

学习python的类与对象这两种数据结构之前,首先我们要清楚一点就是,类与对象这两个概念实际上是来源于生活。
例如,我们考虑一个问题,这个世界是由什么组成的?让不同的人来回答这个问题,因为每个人对世界的看法都会不太一样,所以自然会得到不同的答案。化学家会说,世界是由分子、原子、离子等等的化学物质组成的。画家会说,这个世界是由不同的颜色所组成的。而作为面向对象的开发人员来说,我们应该站在分类学家的角度去考虑这个问题!这个世界是由生物域与非生物域等组成的。而生物又分为动物界,植物界,微生物界组成,而动物界又会细分为不同的门、亚门、纲、亚纲、目、亚目、科、亚科、属、亚属、种、亚种等分类。

assets/v2-cf663390c24dd5ace1ea0676ea894162_1440w.jpg

所以,类实际上就是一种现实生活中个体特征相似的抽象的集体概念,是一个人们抽象出来的集体概念,不存在于现实。而对象呢?则是类的一员,是实际生活中,我们可以触碰到,可以观察到的客观存在的人事物的个体。拥有相同(或者类似)特征和行为的对象就可以抽像出一个类。
因此,类是对象的抽象,而对象是类的具体(实例)。

类   与 对象
奥迪 与 小明家前天买的奥迪R8 
狗   与 小红刚刚牵过去的灰色毛的小奶狗
苹果 与 小明刚吃了一口的苹果
学生 与 刚放学回到家的小明

2.2 类

2.2.1 类的声明

python提供的类的语法:

class 类名(object):
    """类的说明文档"""
    类属性 = 数据
    @classmethod
    def 类方法名(cls[, 参数列表]):
        函数代码一般编写用于操作类属性的代码

代码:

"""最简单的一个类"""
class Person(object):
    """人类"""
    pass

定义类有2种写法:经典类和新式类,类名后面直接跟着冒号:则是经典类;类名后面跟着(object)则为新式类。建议写新式类。
类名的命名规则按照"大驼峰命名法",即每个单词首字母大写,其他字母小写。

三种变量的命名写法
1. 大驼峰式每个单词首字母大写
   GoodsStudentMiddleStudent

2. 小驼峰式首个单词首字母小写其他单词首字母全部大写
   goodsmiddleStudent

3. 匈牙利写法拼接符把多个单词拼接的写法
   middle_student
   top_menu
   top-menu

2.2.2 类的结构

类(Class) 由3个部分构成:

  • 类名称(class name):用于区分不同分类的变量名,采用大驼峰命名法
  • 类属性(class property):记录同一个类的所有对象的公共数据的变量(共同特征)
  • 类方法(class method):操作同一个类的所有对象的公共数据的的函数 (公共行为)

类属性实际上就是一组对象的共同变量,类方法实际上就是一组对象的公共函数。

如,人类设计:

  • 类名称:人(Person、Humen)
  • 类属性:手的数量(handlength),腿的数量(leglength)、眼睛的数量(eyelength),最大年龄(maxage)等。
  • 类方法:跑(run)、说话(speak)、吃(eat)
# Person就是类名
class Person(object):
    # 一个类中可以有0或多个类属性
    eye_length = 2
    leg_length = 2
    hand_length = 2
    max_age = 150
    # 类方法,必须在函数上方一行加上@classmethod用于区分普通函数与后面学到的对象方法
    @classmethod
    def run(cls): # cls是类方法的第一个固定参数,变量名必须叫cls,代表当前类
        print("跑....")  # 函数内部就是普通函数代码,当然也可以通过cls调用当前类的其他类属性或者类方法

    # 一个类中可以有0或多个方法
    @classmethod
    def speak(cls, message):
        print(message)

    @classmethod
    def eat(cls, food):
        print(f"吃{food}")

如,狗类的设计:

  • 类名称:狗(Dog)
  • 类属性:腿的数量(leglength)、最大年龄(maxage)、眼睛的数量(eye_length)等
  • 类方法:跑(run)、吃(eat)、摇尾巴(wag_tail)
class Dog(object):
    # 类属性列表
    eye_length = 2
    leg_length = 4
    max_age = 30
    # 类方法,必须在函数上方一行加上@classmethod用于区分普通函数
    # 在一个函数的上方加上@的用法,表示装饰器,我们后面会学习到。
    @classmethod
    def run(cls): # cls是类方法的第一个固定参数,变量名必须叫cls,代表当前类
        print("跑....")  # 函数内部就是普通函数代码,当然可以传递参数,也可以使用return返回结果

    @classmethod
    def eat(cls, food):
        print(f"吃{food}")

    @classmethod
    def wag_tail(cls):
        return "摇尾巴...."

如果是设计电脑类呢?

类名称:Computer

类属性:最少CPU数量(mincpunum),最少内存条的数量(minramnum),最少硬盘数量(mindisknum)等

类方法:开机(open,run),操作文件(open_txt),关机(close, poweroff)

class Computer(object):
    """电脑"""
    # 类属性
    min_cpu_num = 1
    min_ram_num = 1
    min_disk_num = 1

    # 类的方法
    @classmethod
    def open(cls):
        return "打开电脑"

    @classmethod
    def open_txt(cls):
        return "操作文件"

    @classmethod
    def close(cls):
        return "关闭电脑"

如果是设计书类呢?

类名称:Book

类属性:最少书页(minpage),最少作者数量(maxauthor_num)等。。

类方法:打开(open),关闭(close)

class Book(object):
    """书类"""
    # 类属性列表
    min_page = 1
    max_author_num = 1

    @classmethod
    def open(cls):
        print("打开书")

    @classmethod
    def close(cls):
        print("关闭书")

2.2.3 类的操作

上面设计并编写的类,就是一个数据结构,针对类结构,我们可以直接对它里面的类属性和类方法进行使用。可以在内部方法中调用类属性和类方法,也可以在类的外部调用类属性和类方法。

class Dog(object):
    # 类属性列表
    eye_length = 2
    leg_length = 4
    max_age = 30
    # 类方法,必须在函数上方一行加上@classmethod用于区分普通函数
    # 每个类方法必须有至少一个参数,而且第一个参数是固定必须叫cls,代表当前类
    @classmethod
    def run(cls):
        print("跑....")  # 函数内部就是普通函数代码,当然也可以通过cls调用当前类的其他类属性或者类方法

    @classmethod
    def eat(cls, food):
        print(f"吃{food}")

    @classmethod
    def wag_tail(cls):
        return "摇尾巴...."

"""在类的外界调用类属性或类方法,必须使用 类名. 作为前缀,否则会变成访问其他的变量或函数。"""
# 类属性的调用
print(Dog.eye_length)

# 修改类属性
# 例如,哪天科技进步了,狗的寿命增长了。
Dog.max_age = 50
print(Dog.max_age)
print(f"一条狗的寿命是{Dog.max_age}年,有{Dog.eye_length}腿")

# 类方法的调用
Dog.wag_tail()

除了外部调用以外,类的方法内部也可以通过cls调用当前类的其他类属性或者类方法。

class Dog(object):
    # 类属性列表
    eye_length = 2
    leg_length = 4
    max_age = 30

    @classmethod
    def run(cls):
        print(f"{cls.leg_length}条腿交叉来回地跑....")

    @classmethod
    def eat(cls, food):
        print(f"边吃{food},边{cls.wag_tail()}")

    @classmethod
    def wag_tail(cls):
        return "摇尾巴...."

"""在类的外界调用类属性或类方法,必须使用 类名. 作为前缀,否则会变成访问其他的变量或函数。"""
# 类属性的调用
print(Dog.eye_length)

# 修改类属性,类似修改变量,区别在于类属性是有前缀的
# 例如,哪天科技进步了,狗的寿命增长了。
Dog.max_age = 50
print(Dog.max_age)

print(f"一条狗的寿命是{Dog.max_age}年,有{Dog.eye_length}条腿")

# 类方法的调用
print( Dog.wag_tail() )  # 摇尾巴....

Dog.run()

Dog.eat("狗粮")

2.3 对象

2.3.1 对象的实例化

对象的声明就是对象的实例化过程,必须通过类来创建。所以对象创建之前,必须先声明类,类可以是开发人员自己定义的,也可以是python内置的。

"""先声明类,后调用类创建对象,就可以完成实力化的过程"""


class Person(object):
    """人类"""
    pass

print(Person, id(Person))


"""一个类可以实例化1个或多个对象"""
xiaoming = Person()
print(xiaoming, id(xiaoming))
xiaohong = Person()
print(xiaohong, id(xiaohong))
xiaobai  = Person()
print(xiaobai, id(xiaobai))

# 查看类型
print(type(xiaoming))
print(type(xiaohong))
print(type(xiaobai))

# 判断一个对象是否是否某个类的实例
print( isinstance(xiaoming, Person)) # True
print( isinstance(xiaohong, Person)) # True
print( isinstance(xiaobai, Person))  # True


class Dog(object):
    pass


print(isinstance(xiaoming, Dog))  # False

2.3.2 对象的结构

对象(Object / Instance) 由2个部分构成:

  • 属性(property):记录当前对象独有特征(描述对象的相关数据)的变量。(也叫特征、对象属性、实例属性)
  • 方法(method):操作当前对象独有特征的函数。(也叫行为、对象方法、实例方法)

因为对象是由类创建出来的,所以对象的属性和方法基本都是写在类里面的。在类创建对象以后,提供给对象。

class Person(object):
    # 每一个对象,如果有属性,则属性一般写在__init__构造方法中
    # 对象的每个实例方法必须有至少一个参数,而且第一个参数是固定必须叫self,代表当前对象
    def __init__(self, name, age, money):
        """构造方法"""
        # 构造方法,是一个固定函数名的魔术实例方法,在初始化对象时自动运行,并把类后面的参数提取作为自己的参数
        # 在构造方法中的代码,一般就是对象的实例属性声明的代码,当然,也可以编写一些对象初始化时就要执行的代码
        self.name  = name
        self.age   = age
        self.money = money

    # 实例方法,有别于类方法,不需要加上@classmethod
    def save_money(self, money):
        """存钱"""
        # 在对象中如果要修改或访问对象的其他方法或属性,需要使用self.开头作为前缀,否则会变成访问全局变量。
        self.money += money

    def get_money(self, money):
        """取钱"""
        self.money -= money

    def introduction(self):
        """自我介绍"""
        print(f"我叫{self.name},今年{self.age}岁。")

如果要给一条狗创建实例属性和实例方法呢?

实例属性:名字(name),age(age),

实例方法:表演(act),导盲(guide),

class NiceDog(object):
    """高质量狗类"""
    def __init__(self, name, age):
        self.name = name
        self.age  = age

    def get_act(self,act):
        print(act)

    def get_guide(self, where):
        print(f"导盲去{where}")

    def introduction(self):
        '''自我推销'''
        print(f"我是条好狗,叫{self.name}")

2.3.3 对象的操作

在类中声明了对象相关的实例属性和实例方法以后,我们就可以在类的外面实例化对象,并通过对象调用和访问对象的实例属性和实例方法。

class Person(object):
    def __init__(self, name, age, money):
        """构造方法"""
        # 构造方法,是一个固定函数名的魔术实例方法,在初始化对象时自动运行,并把类后面的参数提取作为自己的参数
        self.name  = name
        self.age   = age
        self.money = money

    def introduction(self):
        return f"我叫{self.name},今年{self.age}岁。"

    def save_money(self, money):
        self.money += money

    def get_money(self, money):
        self.money -= money

# 实例化对象
xiaoming = Person("小明", 17, 1000) # 类后面的参数会作为__init__的参数,自动传递进去
print(xiaoming) # xiaoming是一个变量,但是这个变量被对象赋值了,所以变量名就成了对象名

# 调用对象的属性
print(f"这个人叫{xiaoming.name},今年{xiaoming.age}岁!")

# 修改对象的属性
xiaoming.name = "李晓明"
xiaoming.age += 1
print(f"这个人叫{xiaoming.name},今年{xiaoming.age}岁!")

# 调用对象的方法
print(xiaoming.introduction())

"""
对象的实例属性和实例方法的调用:
1. 在类的内部,通过 self. 进行调用
2. 在类的外部,通过 对象名. 进行调用
"""

# 一个类可以实例化多个对象,不同的对象的属性值可以是不一样的,不同对象的信息保存在不同的内存地址中。
xiaohong = Person("小红", 15, 2200)
print(f"这个人叫{xiaohong.name},今年{xiaohong.age}岁!")

# 修改对象的属性
xiaohong.name = "红太狼"
xiaohong.age += 1
print(f"这个人叫{xiaohong.name},今年{xiaohong.age}岁!")

"""
同一个类实例化出来的不同对象, 所拥有的实例属性和实例方法,都是相互独立,互不干扰的、
"""

# 调用对象的方法
print(xiaohong.introduction())

因为类属性和类方法是所有对象共同拥有的特征和行为,所以实例化以后的对象也可以调用类属性和类方法。

class Dog(object):
    # 类属性列表
    eye_length = 2
    leg_length = 4
    max_age = 30
    tail_length = 1

    def __init__(self, name, age):
        """实例属性列表"""
        self.name = name
        self.age = age


    @classmethod
    def run(cls):
        # 在类中如果要修改或访问当前类的其他类方法或类属性,需要使用cls.开头作为前缀,否则会变成访问全局变量。
        print(f"{cls.leg_length}条腿交叉来回地跑....")

    @classmethod
    def wag_tail(cls):
        return "摇尾巴...."

    def get_guide(self, where):
        print(f"导盲去{where}")


xiaoqiang = Dog("小强", 7)
# 对象访问类属性
print(f"我家的狗[{xiaoqiang.name}]今年{xiaoqiang.age}岁了,但是它能活{xiaoqiang.max_age}年。")

wangcai = Dog("旺财", 3)
print(f"我家的狗[{wangcai.name}]今年{wangcai.age}岁了,但是它能活{wangcai.max_age}年。")


"""不同对象所有拥有的类属性和类方法,是一模一样的。而且内存地址也是一样的。"""
Dog.max_age = 50
print(f"我家的狗[{xiaoqiang.name}]今年{xiaoqiang.age}岁了,但是它能活{xiaoqiang.max_age}年。")
print(f"我家的狗[{wangcai.name}]今年{wangcai.age}岁了,但是它能活{wangcai.max_age}年。")

# 给类新增属性
Dog.skill_list = ["打架", "拆迁"]
print(f"我家的狗[{xiaoqiang.name}]今年{xiaoqiang.age}岁了,擅长{xiaoqiang.skill_list}。")
print(f"我家的狗[{wangcai.name}]今年{wangcai.age}岁了,,擅长{wangcai.skill_list}。")

# 证明不同对象所有拥有的类属性和类方法,是一模一样的。而且内存地址也是一样的。
print(id(Dog.skill_list))
print(id(xiaoqiang.skill_list))

# 证明不同对象所有拥有的实例属性和实例方法是独立的!
xiaoqiang.lve_where = ["家", "公园"]
# print(wangcai.lve_where) # 这里会报错!

"""对象无法修改类属性的值,下方会变成新增属性"""
xiaoqiang.max_age = 50
print(f"我家的狗[{xiaoqiang.name}]今年{xiaoqiang.age}岁了,但是它能活{xiaoqiang.max_age}年。")
print(f"我家的狗[{wangcai.name}]今年{wangcai.age}岁了,但是它能活{wangcai.max_age}年。")

注意,对象虽然可以访问类属性和调用类方法,但是对象无法修改类属性和类方法。同时,类是无法访问对象的实例属性与实例方法的。

经过上面的学习,我们可以发现,类有类属性和类方法,对象有实例属性和实例方法,而且都写在同一个类中,那么什么时候该声明类属性/类方法?什么时候该声明实例属性/实例方法呢?

那么,答案是当有些变量,是所有对象都共用拥有的,那么就应该把这些变量作为类属性进行声明,达到节约内存的作用。有些变量是每一个个体的数值都不一样的,那么这些变量就应该作为实例属性进行声明。同理,操纵类属性的方法,就应该采用类方法进行声明,而操作实例属性的,就应该采用实例方法来声明。

"""
同一个类下所有对象共有的属性,就是类属性,
同一个类下所有对象独有的属性,就是实例属性
类方法操作类属性,实例方法操作实例属性
"""
class Dog(object):
    # 类属性列表
    eye_length = 2
    leg_length = 4
    max_age = 30
    tail_length = 1

    def __init__(self, name, age):
        """实例属性列表"""
        self.name = name
        self.age = age
        self.skill_list = []

    def learn_skill(self, skill):
        self.skill_list.append(skill)

    def get_skill_list(self):
        return self.skill_list

    @classmethod
    def wag_tail(cls):
        return f"{cls.tail_length}条尾巴不断摇晃..."


xiaobai = Dog("小白", 3)
print(xiaobai.get_skill_list())

xiaobai.learn_skill("不要脸")
xiaobai.learn_skill("棉花糖")
xiaobai.learn_skill("变身")
print(xiaobai.get_skill_list())

print(xiaobai.wag_tail())

xiaoqiang = Dog("小强", 5)
xiaoqiang.learn_skill("抗揍")
xiaoqiang.learn_skill("长寿")

print(xiaoqiang.get_skill_list())
print(xiaobai.wag_tail())

2.4 静态方法

那么,如果有些方法,本身在函数内部既没有调用到类属性,也没有调用到实例属性呢?那么就应该作为静态方法(static method)来进行声明。当然,静态方法的使用,仅仅是一种规范,而实际工作中,很多开发人员也会把不操作类或对象数据的方法,使用实例方法或类方法来声明,这也是可以的。

"""
同一个类下所有对象共有的属性,就是类属性,
同一个类下所有对象独有的属性,就是实例属性
类方法操作类属性,实例方法操作实例属性
"""
class Dog(object):
    # 类属性列表
    eye_length = 2
    leg_length = 4
    max_age = 30
    tail_length = 1

    def __init__(self, name, age):
        """实例属性列表"""
        self.name = name
        self.age = age
        self.skill_list = []

    def learn_skill(self, skill):
        self.skill_list.append(skill)

    def get_skill_list(self):
        return self.skill_list

    @classmethod
    def wag_tail(cls):
        return f"{cls.tail_length}条尾巴不断摇晃..."

    # 使用@staticmethod表示当前方法是一个不需要传递当前对象或当前类的静态方法,
    # 内部不会操作到当前类属性或实例属性,仅仅是一个存在关联性而放进来的。
    @staticmethod
    def eat(food):
        print(f"吃{food}")

xiaobai = Dog("小白", 3)
print(xiaobai.get_skill_list())

xiaobai.learn_skill("不要脸")
xiaobai.learn_skill("棉花糖")
xiaobai.learn_skill("变身")
print(xiaobai.get_skill_list())

print(xiaobai.wag_tail())

# 静态方法声明了以后,不仅对象可以调用,类也可以调用
xiaobai.eat("寿司")

xiaoqiang = Dog("小强", 5)
xiaoqiang.learn_skill("抗揍")
xiaoqiang.learn_skill("长寿")

print(xiaoqiang.get_skill_list())

print(xiaobai.wag_tail())

# 静态方法声明了以后,不仅对象可以调用,类也可以调用
xiaoqiang.eat("面条")

"""
静态方法的声明,与实例方法、类方法不一样,实例方法和类方法,在声明时都需要保留一个参数作为当前类或当前对象的变量,而静态方法不会有。
同时,实例方法、类方法在调用时和静态方法是一样,实例方法的self,类方法的cls不需要我们手动传递,python语法会自动传递。
"""