Python面向对象

430 阅读10分钟

这是我参与更文挑战的第2天,活动详情查看: 更文挑战

微信公众号搜索【程序媛小庄】,关注半路出家的程序媛如何靠python开发养家糊口~

前言

在介绍编程范式时,提到了两种编程思想,分别是面向过程和面向对象,在Python中一切皆对象,那么对象通俗的理解到底是什么?面向对象编程又是如何实现的?请擦亮bling bling的大眼睛和小庄一起来瞅瞅吧~

对象

面向对象编程的核心是对象,在Python中一切皆对象,而对象的核心就是整合,之前写过的所有的程序都是由数据和功能(函数)组成的,因此开发程序就是定义数据然后定义功能对数据进行操作,在学习面向对象之前,编写的程序中数据和功能是分开的,比如:

# 程序中定义的数据
name = 'cow'
age = 1
gender = 'female'

# 程序中定义的功能
def query_info(name, age, gender):
    print(f'name:{name}, age:{age}, gender:{gender})

# 如果需要查看信息,需要获取数据和功能,然后调用函数得到想要的信息
query_info(name=name, age=age, gender=gender)

在学到函数的时候,我们说是将重复的代码进行抽取封装,减少代码冗余,实质上就是将实现了某种功能的代码块放到了一块内存空间中,将这段代码起来,而对象也是类似的,只是对象是将数据功能一起起来,相当于一个容器,因此可以说对象是把数据和功能整合到一起的产物。

如果您还觉得抽象的话,我们举一个简单的日常生活中的例子---拧螺丝的工具箱,在这个箱子中有各种螺丝刀以及不同种类的螺丝,可以将螺丝比喻为数据,将拧螺丝的螺丝刀比喻为功能,因此这个工具箱就是一个对象,它将数据(螺丝)与功能(螺丝刀)装在一起,如果想要执行拧螺丝这个业务逻辑,只需要提供工具箱即可,因为工具箱中装好了拧螺丝这个业务逻辑所需要的数据(螺丝)和功能(螺丝刀)。

在理解了对象之后,理解面向对象编程思想就简单一些了,面向对象编程就是创建出一个个的对象,把原本分散的有关联的数据和功能整合到一个个的对象中,因此使用面向对象的编程思想既方便使用也可以提高程序的解耦合度,进而提高程序的可扩展性。

将数据和功能整合到一起,基于我们之前所学的知识也是可以实现的,就是借助模块,将数据与功能存放在py文件中,每产生一个对象就放在一个文件中,存在的问题就是太难管理,因此Python解释器提供了将数据和功能整合到一起的方式并且能够实现面向对象编程的方式 --- 类。

类与对象

类即种类/类别,是面向对象的基础,如果多个对象由相似的功能和数据组成,那么这多个对象就属于同一类。比如:

image-20210602152844546

有了类以后,可以将同一类对象相同的数据与功能存放到类中,无需每个对象都重复存一份,这样每个对象只需要存自己独有的数据即可,极大的节省了空间。

如果说对象是用来数据和功能的容器,类就是用来多个对象相同的数据和功能的容器,因此,类也是对象。

image-20210602153916543

在程序中使用面向对象的思想编程,必须要先定义类,然后调用类产生对象(调用类产生的返回值就是对象),产生对象的类与对象之间存在关联,这种关联体现在一下几个方面:

1 对象可以访问到类中的数据和功能;

2 类中的数据和功能仍然是属于对象的;

3 类只不过是一种节省空间 减少代码冗余的机制,面向对象的核心仍然是使用对象;

实现面向对象编程

实现面向对象编程最基本的思路就是把程序中用到的、相关联的数据和功能整合到对象中,然后再去使用。在一个程序的开发中需要用到的数据和功能非常多,因此可以通过提取程序中不同的角色来进行分类,比如开发一个简单的python外卖系统,在这个系统中至少会有三种角色,分别是商家、外卖小哥、顾客,每种角色的功能都是不同的,单以商家为例:

# 商家的数据包括
商家入驻品牌  # 商家类相同的数据
商家名称  # 不同商家对象有不同的名称
商家地址  # 不同商家对象有不同的地址
商家联系电话  # 不同商家对象有不同的联系电话

# 商家的功能有
接订单  # 商家类相同的功能

这样可以总结出一个商家类,用来存放商家相同的数据和功能:

# 商家类
相同的数据:商家入驻品牌 = python外卖系统
相同的功能:接订单

接下来可以根据上述分析的结果在程序中定义出类,然后调用类产生对象:

class Merchants():  # 定义类使用class关键字,类的命名推荐使用驼峰体
    
    # 数据的定义
    brand = 'python外卖系统'
    # 功能的定义
    def receive_order(merchants_obj):
        print('已经接收订单')

类体代码中最常见的就是变量与函数的定义,但是类体代码中可以任意包含其他代码,类体代码在定义阶段就会执行,产生类的名称空间,可以通过打印类.__dict__查看类这个容器中的数据和功能,也称为属性。

>>> Merchants.__dict__  # 本质是一个字典

mappingproxy({..., 'brand': 'python外卖系统', 'receive_order': <function Merchants.receive_order at 0x000002314779E3A0>, ...})

有了类之后,调用类产生该类的对象,调用类的过程也称为将类实例化,拿到的返回值就是程序中的对象,或称为一个实例:

>>> mer1_obj = Merchants()  # 调用一次类就产生一个对象
>>> mer2_obj = Merchants()
>>> mer1_obj.__dict__
{} 
>>> mer2_obj.__dict__
{}

实例化得到的两个对象是完全一样的,只有类中共有的数据和功能,但是没有各自独有的数据,如何为对象定制独有属性呢?

第一种,low版解决方案,为每个对象的属性字典手动添加值。使用这种方案为每个对象定制独有属性的问题就是代码重复。

mer1_obj.name = '吃嘛嘛香川菜'  # mer1_obj.__dict__['name'] = '吃嘛嘛香川菜'
mer1_obj.addr = '山清水秀区'   # mer1_obj.__dict__['addr'] = '山清水秀区'
mer1_obj.tel = '111119'   # mer1_obj.__dict__['tel'] = '111119'

mer2_obj.name = '养生粤菜'  # mer1_obj.__dict__['name'] = '养生粤菜'
mer2_obj.addr = '无敌海景区'   # mer1_obj.__dict__['addr'] = '无敌海景区'
mer2_obj.tel = '111120'   # mer1_obj.__dict__['tel'] = '111120'

第二种,解决第一种方案代码重复的问题,将重复代码封装称为函数。使用这种方案又违背了面向对象的整合思想,没有把功能整合到类中。

def init(obj, name, addr, tel):
    obj.name = name
    obj.addr = addr
    obj.tel = tel
    
init(mer1_obj, '吃嘛嘛香川菜', '山清水秀区', '111119')
init(mer1_obj, '养生粤菜', '无敌海景区', '111120')

第三种,终极版解决方案,将第二种方案中的函数名改为__init__,放在类体代码中,在调用类的时候会自动执行__init__函数。

__init__内存放的应该是为对象初始化具备的属性,但是也可以存放任意其他代码,想要在类调用时就立刻执行的代码都可以放到该方法内__init__方法的返回值必须是None。

在调用类产生对象的过程也就是实例化对象时会发生三件事情:

1.调用类首先产生一个空对象;

2.会自动调用类中的__init__方法,然后将空对象以及调用类时括号内传入的参数一同传给__init__方法的形参;

3.返回一个初始化完成的对象。

class Merchants():  # 类的命名推荐使用驼峰体
    
    def __init__(self, name, addr, tel):  # 官方推荐__init__方法,第一个形参名约定俗成就是self,表示类实例化产生的对象
        obj.name = name
    	obj.addr = addr
    	obj.tel = tel
  
    # 数据的定义
    brand = 'python外卖系统'
    # 功能的定义
    def receive_order(self):  
        print('已经接收订单')
        
# 调用类产生不同的对象
mer1_obj = Merchants('吃嘛嘛香川菜', '山清水秀区', '111119')
mer1_obj = Merchants('养生粤菜', '无敌海景区', '111120')

至此,已经造出了两个具有独有属性的两个对象,这两个对象同属一个类,将数据和功能存在对象中,我们如何访问对象的属性呢?

属性访问

在类中定义的名字都是类的属性,可以通过__dict__访问属性的值,python提供了一种专门的属性访问语法,就是对象.属性,注意:类也是对象哦!

# 访问类的属性
>>> Merchants.brand  # 等价于Merchants.__dict__['brand']
'python外卖系统'

# 修改类的属性 
>>> Merchants.brand = '外卖系统'  # 等价于Merchants.__dict__['brand'] = '外卖系统'
>>> Merchants.brand
'外卖系统'

# 增加类的属性
>>> Merchants.xx = '增加的属性'  # 等价于Merchants.__dict__['xx'] = '增加的属性'
>>> Merchants.xx
'增加的属性' 

# 删除属性
del Merchants.xx

操作对象的属性也是相同的。

mer1_obj.name  # 查看属性,等价于 mer1_obj.__dict__['name']
mer1_obj.xx = 'xx'   # 新增属性, mer1_obj.__dict__['xx'] = 'xx'
mer1_obj.name = '湘菜'  # 修改属性,等价于 mer1_obj.__dict__['name'] = '湘菜'
del mer1_obj.xx   # 删除属性

非官方来讲,类中定义的属性有两种,一种是数据属性,比如变量,另一种是函数属性,就是在类中定义的函数。

类中的数据属性是共享给所有对象使用的所以大家访问到的内存地址也是相同的:

>>> id(Merchants.brand)
2410675771504
>>> id(mer1_obj.brand)
2410675771504
>>> id(mer2_obj.brand)
2410675771504

类中定义的函数是类的函数属性,类可以使用,但必须遵循函数的参数规则,有几个参数需要传几个参数:

Merchants.receive_order(mer1)

但是,类中定义的函数(功能)主要是给对象用的,而且是绑定给对象的,虽然所有对象指向的都是相同的功能,但是绑定到不同的对象就会产生不同的内存地址。

>>> id(Merchants.receive_order)
2410675823520
>>> id(mer1.receive_order)
2410675842432
>>> id(mer2.receive_order)
2410675842368

绑定方法的特殊之处在于:哪个对象来调用绑定的方法,就会将哪个对象当作第一个参数自动传入,因此类中定义的函数一般情况下第一个参数都是类产生的对象,注意,是一般情况,其他情况后面文章会后续介绍。

虽然都是接订单功能,但是mer1接的订单不会给mer2,这就是绑定的核心所在。

mer1.receive_order()
mer2.receive_order()

在python中一切皆对象,在Python3中类与类型是一个概念,因而绑定方法我们其实已经接触过:

# 类型dict就是类
>>> dict
<class 'dict'>

# 实例化得到字典
d1 = dict(x=1, y=2)
d2 = dict(a=1, b=2)

# 两个实例化得到的对象都有popitem方法,功能相同但是内存地址不同
>>> d1.popitem
<built-in method popitem of dict object at 0x00000231477A50C0>
>>> d2.popitem
<built-in method popitem of dict object at 0x00000231477A5180>

# 操作绑定方法d1.popitem(),就是随机删除d1中的一组键值对,绝对不会删除d2中的键值对
>>> d1.popitem()
('y', 2)
>>> d1
{'x': 1}
>>> d2
{'a': 1, 'b': 2}

总结

对象是一个高度整合的产物,有了对象之后,我们只需要使用对象.属性的语法就可以得到和这个对象相关的所有属性,十分方便而且解耦合程度非常高。