Python 面向对象编程基础(类与对象)

63 阅读13分钟

Python-面向对象编程基础

一、概念引入

我们之前学习过“面向过程编程”的概念,面向过程的核心是“过程”二字,过程的终极奥义就是将程序流程化;过程是“流水线”,用来分步骤解决问题的。

相对应的,“面向对象”的核心是“对象”二字,我们说 ==“对象”就好像是一个容器,用来盛放数据与功能。类也是“容器”,该容器用来存放同类对象共有的数据与功能。==

举个例子:

  • 我们要写一个简单的餐厅人员信息程序,要求具有厨师和服务员两类人的数据和个人简介功能(函数)。

    # 厨师数据:name、grade、sex
    cook_name = '山炮'
    cook_grade = 2
    cook_sex = '未知'
    
    # 服务员数据:name、age、sex
    waiter_name = '丽丽'
    waiter_age = 19
    waiter_sex = '萌妹子'
    
    
    # 厨师功能:说个人简介
    def cook_speak(name, grade, sex):
        print('我叫%s,厨师等级%s级,性别%s' % (name, grade, sex))
    
    
    # 服务员功能:说个人简介
    def waiter_speak(name, age, sex):
        print('我叫%s,年龄%s岁,大家都叫我%s' % (name, age, sex))
    
    
    # 此时若想执行说个人简介的功能,需要同时拿来两样东西,一类是功能函数,另外一类则是name等多个数据,然后才能执行,非常麻烦。
    
    cook_speak(cook_name, cook_grade, cook_sex)
    waiter_speak(waiter_name, waiter_age, waiter_sex)
    # 我叫山炮,厨师等级2级,性别未知
    # 我叫丽丽,年龄19岁,大家都叫我萌妹子
    
  • 我们说,对象就像一个“容器”,该容器可以盛放数据与功能。所以我们可以说:对象是把数据与功能整合到一起的产物,或者说对象就是一个盛放数据与功能的容器/箱子/盒子。我们现在引入对象的思想来重构一下这个程序,看看是什么样子。

    def cook_speak(name, grade, sex):
        print('我叫%s,厨师等级%s级,性别%s' % (name, grade, sex))
    
    
    cook_obj = {
        'cook_name': '山炮',
        'cook_grade': '2',
        'cook_sex': '未知',
        'cook_speak': cook_speak
    }
    cook_obj['cook_speak'](cook_obj['cook_name'], cook_obj['cook_grade'], cook_obj['cook_sex'])
    # 我叫山炮,厨师等级2级,性别未知
    

    或者我们还可以这样写:

    def cook_speak(obj):
        print('我叫%s,厨师等级%s级,性别%s' % (obj['cook_name'], obj['cook_grade'], obj['cook_sex']))
    
    
    cook_obj = {
        'cook_name': '山炮',
        'cook_grade': '2',
        'cook_sex': '未知',
        'cook_speak': cook_speak
    }
    cook_speak(cook_obj)
    

    在上面的两个程序中,我们使用字典来对厨师山炮的信息和个人简介功能做了整合,此时这个字典就是一个对象,这种思想就是面向对象的思想。但美中不足的是,功能函数没有能放到这个厨师对象里面。所以这个容器还不够好,我们想要一个把数据和功能都完完全全能放进去的容器。

通过上面的例子,所以我们说,==对象的终极奥义(思想)就是将程序“整合”。==

那么python这门语言到底提供了什么语法来允许我们将数据与功能很好地整合到一起呢???

二、类与对象

先定义类,再调用类产生对象。

概念区分

类即类别/种类,是面向对象分析和设计的基石,如果多个对象有相似的数据与功能,那么该多个对象就属于同一种类。

有了类的好处是:我们可以把同一类对象相同的数据与功能存放到类里,而无需每个对象都重复存一份,这样每个对象里只需存自己独有的数据即可,极大地节省了空间。

==所以,如果说对象是用来存放数据与功能的容器,那么类则是用来存放多个对象相同的数据与功能的容器,是对象相似数据与功能的集合体。==

在这里插入图片描述


定义类

我们使用class来定义一个类。

类体中最常见的是变量与函数的定义,但是类体其实是可以包含任意其他代码的。

注意:==类体代码是在类定义阶段就会立即执行,会产生类的名称空间。==

我们以开发一个大学的选课系统为例,需要先提取选课系统里的角色:学生、老师、课程等。然后显而易见的是:学生有学生相关的数据与功能,老师有老师相关的数据与功能,等等。

我们单以学生为例:定义一个学生的类来存放学生们共有的属性和功能。

class Student:
    # 1、变量的定义
    stu_school = 'bilibili大学'

    # 2、功能的定义
    def stu_info(stu_obj):
        print('学生信息:名字:%s 年龄:%s 性别:%s' % (
            stu_obj['stu_name'],
            stu_obj['stu_age'],
            stu_obj['stu_gender']
        ))
属性访问的语法
  1. 访问数据属性

    print(Student.stu_school)  # 等同于 print(Student.__dict__['stu_school'])
    
    # bilibili大学
    
  2. 修改数据属性

    Student.stu_school = '哔哩哔哩高等院校'
    print(Student.stu_school)  # 哔哩哔哩高等院校
    
  3. 添加数据属性

    Student.x = '混入其中'
    print(Student.x)
    

    我们可以看到,类名.xx = xx 这种方式的本质其实是有则更改,无则添加。

  4. 访问函数属性

    print(Student.stu_info)  # 等同于 print(Student.__dict__['stu_info'])
    
    # <function Student.stu_info at 0x000001D255726160>
    

调用类产生对象

我们调用三次,来产生三个学生对象

class Student:
    stu_school = 'bilibili大学'

    def stu_info(stu_obj):
        print('学生信息:名字:%s 年龄:%s 性别:%s' % (
            stu_obj['stu_name'],
            stu_obj['stu_age'],
            stu_obj['stu_gender']
        ))


stu1_obj = Student()
stu2_obj = Student()
stu3_obj = Student()

调用类的过程称为将类实例化,拿到的返回值就是程序中的对象,或称为一个实例。

我们现在来看一下拿到的三个对象里都有什么。

print(stu1_obj.__dict__)  # {}
print(stu2_obj.__dict__)  # {}
print(stu3_obj.__dict__)  # {}

结果全都为空。

我们现在给拿到的对象里添加东西。

stu1_obj = Student()

stu1_obj.stu_name = '山炮'  # stu1_obj.__dict__['stu_name']='山炮'
stu1_obj.stu_age = 18  # stu1_obj.__dict__['stu_age']=18
stu1_obj.stu_gender = 'male'  # stu1_obj.__dict__['stu_gender']='male'

print(stu1_obj.__dict__)
# {'stu_name': '山炮', 'stu_age': 18, 'stu_gender': 'male'}

stu2_obj = Student()

stu2_obj.stu_name = '张三'
stu2_obj.stu_age = 19
stu2_obj.stu_gender = 'male'


stu3_obj = Student()

stu3_obj.stu_name = '李四'
stu3_obj.stu_age = 20
stu3_obj.stu_gender = 'male'

在我们给拿到的对象添加属性之后,就看到对象里有内容了。

可是又有了一个新的问题 → 代码重复,我们也知道解决代码重复的方法,就是函数

利用函数来优化一下代码:

def init(obj, x, y, z):
    obj.stu_name = x
    obj.stu_age = y
    obj.stu_gender = z


stu1_obj = Student()
stu2_obj = Student()
stu3_obj = Student()

init(stu1_obj, '山炮', 18, '男')
init(stu2_obj, '张三', 19, '男')
init(stu3_obj, '李四', 20, '男')

print(stu1_obj.__dict__)  # {'stu_name': '山炮', 'stu_age': 18, 'stu_gender': '男'}

我们利用函数解决了代码重复的问题,简洁了代码。我们说,对象的奥义在于对代码的整合类又是对对象共有部分的整合,那我们能不能把这个init函数整合到里面去呢?

class Student:
    stu_school = 'bilibili大学'

    def init(obj, x, y, z):
        obj.stu_name = x
        obj.stu_age = y
        obj.stu_gender = z

    def stu_info(stu_obj):
        print('学生信息:名字:%s 年龄:%s 性别:%s' % (
            stu_obj['stu_name'],
            stu_obj['stu_age'],
            stu_obj['stu_gender']
        ))

像上面这样子,直接将这个函数放到类里去,可以吗?

我们想要的最理想效果是:在调用类产生对象的过程当中,最好有一种机制能自动执行这个init函数,这样子我们连调用的步骤都可以省掉,程序的整合程度就更高了。

但像上面这样直接把函数扔到类里面,在我们调用类产生对象的时候,init函数并不会被自动执行。那我们该怎么办呢?python为我们提供了一种方法:那就是把这个函数的函数名改为“_ _init_ _”。(==注意:名字必需且只能是“_ _init_ _”,才能在调用类产生对象的过程中自动执行。而在类定义的时候,则不会执行这个函数。==)

class Student:
    stu_school = 'bilibili大学'

    def __init__(obj, x, y, z):
        obj.stu_name = x
        obj.stu_age = y
        obj.stu_gender = z

    def stu_info(stu_obj):
        print('学生信息:名字:%s 年龄:%s 性别:%s' % (
            stu_obj['stu_name'],
            stu_obj['stu_age'],
            stu_obj['stu_gender']
        ))

像上面这样,我们就成功的把给对象添加属性的这个函数也整合到类里面去了。

但可能会产生一个疑问:我还没创造对象呢,这怎么就已经把“给已经创造出来的对象”添加属性的函数给整合进去了呢?那该怎么使用它呢?

Python给了我们在调用类产生对象的过程中,自动执行这个“_ _init_ _”函数的机制,同时也秉承了帮人帮到底的原则。在我们调用这个类产生对象的时候,就可以这样:

class Student:
    stu_school = 'bilibili大学'

    def __init__(obj, x, y, z):
        obj.stu_name = x
        obj.stu_age = y
        obj.stu_gender = z

    def stu_info(stu_obj):
        print('学生信息:名字:%s 年龄:%s 性别:%s' % (
            stu_obj['stu_name'],
            stu_obj['stu_age'],
            stu_obj['stu_gender']
        ))


stu1_obj = Student('山炮', 18, '男')
# 1.在这个过程中,Python自动调用__init__函数,所以我们直接给括号里传值,其实就是在给__init__函数传参。
# 2.在这个过程中,Python自动就把stu1_obj这个我们定义的对象名,作为__init__函数的第一个参数传了进去,所以我们只需要传剩下三个参数。
# 3.以上两步就是Python帮人帮到底的优秀品质。

print(stu1_obj.__dict__)  # {'stu_name': '山炮', 'stu_age': 18, 'stu_gender': '男'}

调用类产生对象的过程又被称为实例化。(可能含义就是调用类这个抽象的货产生出实际的案例?) 这个过程中发生了三件事:

  1. 先产生一个空对象
  2. python会自动调用类中的_ _init_ _方法然后将空对象和调用类时括号内传入的参数一同传给_ _init_ _方法
  3. 返回被_ _init_ _方法处理过后的对象,一般也叫初始化后的对象

总结一下_ _init_ _方法:

  1. 会在调用类时自动触发执行,用来为对象初始化自己独有的数据
  2. _ _init_ _内应该存放是为对象初始化属性的功能,但是是可以存放任意其他代码,想要在类调用时就立刻执行的代码都可以放到该方法内
  3. _ _init_ _方法必须返回None

但我们现在使用不了stu_info方法了,因为stu_info方法最开始是使用字典的思路进行设计的,我们现在需要重新设计一下:

class Student:
    stu_school = 'bilibili大学'

    def __init__(obj, x, y, z):
        obj.stu_name = x
        obj.stu_age = y
        obj.stu_gender = z

    def stu_info(obj):
        print('学生信息:名字:%s 年龄:%s 性别:%s' % (
            obj.stu_name,
            obj.stu_age,
            obj.stu_gender
        ))


stu1_obj = Student('山炮', 18, '男')
# 类调用自己的函数属性必须严格按照函数的用法来
Student.stu_info(stu1_obj)  # 学生信息:名字:山炮 年龄:18 性别:男

现在就可以正常使用了。


属性查找与绑定方法

对象的名称空间里只存放着对象独有的属性,而对象们共有的属性是存放于类中的。对象在访问属性时,会优先从对象本身的_ _dict_ _中查找,若未找到,则去类的_ _dict_ _中查找。

class Student:
    stu_school = 'bilibili大学'

    def __init__(obj, x, y, z):
        obj.stu_name = x
        obj.stu_age = y
        obj.stu_gender = z

    def stu_info(obj):
        print('学生信息:名字:%s 年龄:%s 性别:%s' % (
            obj.stu_name,
            obj.stu_age,
            obj.stu_gender
        ))


stu1_obj = Student('山炮', 18, '未知')
stu2_obj = Student('张三', 19, '女')
stu3_obj = Student('李四', 20, '男')
类可以访问
  1. 类的数据属性

    print(Student.stu_school)
    
  2. 类的函数属性

    print(Student.stu_info)
    
但其实类中的东西是给对象用的
  1. 类的数据属性是共享给所有对象用的,大家访问的地址都一样。

    print(id(Student.stu_school), Student.stu_school)  # 2065854765856 bilibili大学
    print(id(stu1_obj.stu_school), stu1_obj.stu_school)  # 2065854765856 bilibili大学
    print(id(stu2_obj.stu_school), stu2_obj.stu_school)  # 2065854765856 bilibili大学
    print(id(stu3_obj.stu_school), stu3_obj.stu_school)  # 2065854765856 bilibili大学
    

    所以如果类中的某个属性发生了改变,则通过这个类产生的对象的这个属性的值也一样发生变化,因为通过对象找这个属性找的也是类里的。

    如果是在对象里直接改这个属性的值呢?

    stu1_obj.stu_school = '山炮大学'
    
    print(stu1_obj.stu_school)  # 山炮大学
    
    print(stu2_obj.stu_school)  # bilibili大学
    print(Student.stu_school)  # bilibili大学
    

    可以看到,类和其他对象的这个属性的值并没有发生改变。因为对象在访问属性时,会优先从对象本身的_ _dict_ _中查找,若未找到,则去类的_ _dict_ _中查找。此时在对象里更改属性值,相当于是赋值操作,先在对象本身的_ _dict_ _中查找,遵循的是“有则更改,无则添加”原则,所以相当于是在操作的这个对象的名称空间里新创造了这个属性,且这个新创造的属性只属于它自己。

  2. 类中定义的函数主要是给对象使用的,而且是绑定给对象的。虽然所有对象指向的都是相同的功能,但是绑定到不同的对象就是不同的绑定方法,内存地址各不相同。

    stu1_obj = Student('山炮', 18, '男')
    stu2_obj = Student('张三', 19, '男')
    stu3_obj = Student('李四', 20, '男')
    
    print(Student.stu_info)  # <function Student.stu_info at 0x000002AB473E4700>
    print(stu1_obj.stu_info)  # <bound method Student.stu_info of <__main__.Student object at 0x000002AB369B5640>>
    print(stu2_obj.stu_info)  # <bound method Student.stu_info of <__main__.Student object at 0x000002AB47269C10>>
    print(stu3_obj.stu_info)  # <bound method Student.stu_info of <__main__.Student object at 0x000002AB3690D340>>
    

    ==绑定方法的特殊之处在于:谁来调用绑定方法就会将谁当做第一个参数自动传入==

    stu1_obj.stu_info()  # 相当于Student.stu_info(stu1_obj)
    # 结果:学生信息:名字:山炮 年龄:18 性别:男
    

    这其实也是_ _init_ _方法的原理。

    我们来分析一下,通过类去调用函数,与利用Python绑定方法机制通过对象直接调用函数有什么优劣之分?

    • 通过对象调用函数,可以很清晰的知道函数的调用者与函数打印或处理的信息的相关者,语义更明确。

    • 通过对象调用函数,与通过类调用函数相比少传递一个参数,这个参数就是对象它自己,代码更简洁。

    ==因为存在这种谁来调用绑定方法就会将谁当做第一个参数自动传入的原理,所以我们应景的把函数形参中第一个参数取名叫“self”==

    class Student:
        stu_school = 'bilibili大学'
    
        def __init__(self, x, y, z):
            self.stu_name = x
            self.stu_age = y
            self.stu_gender = z
    
        def stu_info(self):
            print('学生信息:名字:%s 年龄:%s 性别:%s' % (
                self.stu_name,
                self.stu_age,
                self.stu_gender
            ))
    
    
    stu1_obj = Student('山炮', 18, '男')
    stu1_obj.stu_info()  # 学生信息:名字:山炮 年龄:18 性别:男
    

三、联系所学

Python中一切皆为对象,且Python3中类与类型是一个概念,因而绑定方法我们早就接触过。

l = []
print(type(l))  # <class 'list'>

类名就是list

我们现在通过调用类来产生对象

l1 = list([1, 2, 3])
l2 = list([[1, 2], (3, 4), '5'])
l3 = list([6, {'7': 8}])

三个对象都有绑定方法append,是相同的功能,但内存地址不同:

print(l1.append)  # <built-in method append of list object at 0x0000016D12C02880>
print(l2.append)  # <built-in method append of list object at 0x0000016D12C52580>
print(l3.append)  # <built-in method append of list object at 0x0000016D12D32C00>

操作绑定方法l1.append(4),就是在往l1添加4,绝对不会将4添加到l2l3

相对应的,还可以通过list.append(l1, 4)实现相同的效果,因为二者等价。

# l1.append(4)
list.append(l1, 4)
print(l1)  # [1, 2, 3, 4]