ITPub博客

首页 > 数据库 > 数据库开发技术 > python面向对象编程篇(1):和python对象处CP初步篇-此对象非彼面向对象

python面向对象编程篇(1):和python对象处CP初步篇-此对象非彼面向对象

原创 数据库开发技术 作者:程序员小钟同学 时间:2021-03-11 15:44:06 0 删除 编辑

叙文相关描述主要是个人的认知和见解,如有不当之处,还望各位大佬指正。

小钟同学 | 文 【原创】| QQ:308711822 个人其他博客地址:

简书:https://www.jianshu.com/u/d6960089b087

掘金:https://juejin.cn/user/2963939079225608

个人公众号:

前言

之前带一个初学编程语言的小弟的时候,就问过这么一个问题,什么是对象,(这个对象能处CP吗)? 今时今日对象对我回眸一笑,发现对这个对象才“百媚生”。

对象是什么?

答:对象就是你媳妇呗。送你一首《凉凉》吧!哈哈

其实在编程语言里面,对象是一个比较抽象的概念。对于编程语言来说,它是一个相对独立的实体,众所周知,我们的python是动态型语言,意思就是说:在Python语言里,对象的类型和内存占用都是运行时确定的。虽然中间解析器会把我们的.py有编译成.pyc,但是它的动态性说明也说明了它是一个解释型的语言,程序运行时候,解释器也可以根据语法和右侧的操作数来决定新对象的类型。

在Python底层中不论是整数,字符串,甚至是其他容器级别的数据类型,都由C底层由一个叫struct PyObject结构体所封装。先前在内存一些介绍中,如图示:

有所介绍,从PyObject的结构体可以看出:类型是跟着堆内存中的对象走的。所以甚至于:我们的python中的类和方法等其实都是对象。如下示例:是对象都会分配堆内存地址:

class Person():

    @classmethod
    def run(cls):
        pass

    def cat(self):
        pass

print(Person)
print(hex(id(Person)))
print(hex(id(Person.run())))
a = Person()
print(hex(id(a)))
print(hex(id(a.cat())))

输出的结果:

<class '__main__.Person'>
0x1f29dc65eb8
0x7fffa8487ce0
0x1f29e35b898
0x7fffa8487ce0

那接下来本节主要梳理一些模糊的概念知识点,也是我想重新梳理的几个点:

  • Python 中的对象是由哪些元素的组成?
  • Python的引用是指什么?
  • 栈内存和堆内存到底是什么东西?
  • 栈内存与堆内存的区别?
  • 栈内存与堆内存什么时候被释放?
  • python中深拷贝和浅拷贝的区别?
  • 通常说的泄漏是哪个内存的泄漏?
  • 堆和栈是怎么联系起来的呢?

由Python 中的对象存贮位置引发的关于堆内存和栈内存思考

通常我们的一个程序启动后,程序都会向我们的系统征收一点内存来,支撑我们的程序的运行。而相关内存分配出于对内存优化机制,一般有两种方式进行分配:

  • 一种是系统分配
  • 一种是程序动态的申请开辟新的内存。

图示通常是一个程序运行的时候相关内存分类的情况(PS:可能不同的言语不一样哟,这个还得再细究,不过C和C++应该就是按图示的)

Java的内存划分:

专业术语来说的话就是:

1.栈内存(程序运行时分配局部变量):


特性(类似弹夹一样):

  • 1.一种连续存储的数据结构
  • 2.元素的顺序是先进后出
  • 3.操作有出栈、入栈(压栈)和取栈顶元素
  • 4.取栈中某元素,需把它上面的所有的元素推出栈。

  1. 存取速度比堆要快,仅次于寄存器
  • 6.栈中主要存放一些常见的基本类型的变量 byte, float, , bool, str等和对象地址引用

  • 其他描述:

    • 由系统自动分配,它的速度较快。程序员是无法控制管理。

    • 它存贮的是变量。(可以理解对象的引用)

    • 栈内存存放的变量生命周期一旦结束就会被释放。

    • 全局栈内存一般会随着程序的结束关闭和释放

    • 若分配失败,则提示栈溢出错误。

    • 私有栈内存:

      (1)一般情况下,内部函数执行完成,形成的私有栈内存一般在作用域结束会被销毁掉。

      (2)如果栈内存中的的地址(一般都是堆内存的地址)被私有作用域以外的事物占用,则无法立即释放(即:私有作用域中的私有变量等信息被保留下来了),但一般情况也会随程序销毁而销毁。

    2.堆内存(是程序运行动态申请开辟内存空间):


    特性(类似弹夹一样):

    • 1.一种非连续的树形存储数据结构
    • 2.每个节点都有一个值
    • 3.整棵树都经过排序。
    • 4.根节点的值最小(或最大),且根节点的两个子树也是一个堆。
    • 5.常用来实现优先队列,存取随意。

    其他描述:

    • 存储的是数组和实体对象。
    • 动态地分配内存大小,生存期也不必事先告诉编译器
    • 其实就是我们的在写代码的过程中各自对象的创建(注意是对象),这种人为的创建对象的,速度相对栈内存有些慢。
    • 堆内存的分配是是在运行时动态分配内存的,地址不连续,容易碎片化。
    • 堆内存的回收通常是由垃圾回收来负责的。除非泄漏了,回收不了!
    • 另外因为是人为的创建,所有有些时候不经意间,也容易引发内存的泄漏。
    • 程序退出的时候,未必会释放内存。

    PS:所以通常说的泄漏是哪个内存的泄漏?其实多数是指堆内存的泄漏!

    3.栈内存和堆内存的关联:


    签名说过,python对象在C的底层内部其实是分配一个对象。里面包含了几个重要的元素信息。

    从PyObject的结构体可以看出:

    • 类型是跟着堆内存中的对象走的
    • 类型属于对象,变量是没有类型的
    • 变量存贮的是地址。

    转化为我们的PyObject对象:

    由我们的刚刚的内存类型分类上来说,我们的所有创建的对象其实都是存在再堆内存里面。所以推导出了下面这张图:

    此时我们的堆内存存在了,但是我们还没有给我们的对象分配一个变量啊?(就是分配栈内存地址,因为我们的栈内存存的是变量),所以我们的此时可以回答-堆和栈是怎么联系起来的呢?

    从上图我们可以得到,其实当创建一个对象的时候,通常是需要一个变量去存贮我们的对象。没有变量进行引用的话多数情况下,可能就要等着‘被人道毁灭,回炉重造’!

    所以此时我们的引出了另一个概念.叫做对象的引用?(类似C++和go中的指针),既然python中的变量都是指针,所以这个指针(内存地址)可以指向任何一个对象。

    那什么是对象引用呐?

    从上图看,第三步中:把堆内存地址存放在栈内存中,供变量a的引用。

    PS:转化过来就是:Python变量持有Python对象在堆内存中的内存地址。

    此时我们的a变量存的是我们对象‘小钟同学的’地址,所以,此时a也叫做也称为:对象(小钟同学的)的引用。

    所以引用的本质就是:变量存储的就是对象的地址。a变量持有Python对象‘小钟同学’的引用.

    专业术语总结的话:

    • 1.引用数据类型在栈中存储了指针(对象地址),
    • 2.该指针(对象地址)指向堆中该实体的起始地址(堆内存地址)

    由此我们可以总结出来:

    解释器寻找引用值的过程:

    从上面的分析关于堆内存和占内存的分析中,我们的可以得到,其实我们变量是存的是一个地址:所以:

    a = ‘小钟同学’
    b = a 
    print(id(a),id(b))得到的地址是一样

    这里的 = 号 赋值 其实不是正确的应该是叫做(赋“地址”)

    示例:

    a= 'a'
    b =a
    print(id(a),a)
    print(id(b),b)
    # 创建了一个对象(申请了新的地址给a)
    a= 'aaaaaaaaaaaaaa'
    print(id(a),a)
    print(id(b),b)

    输出结果:

    2260469193504 a
    2260469193504 a
    2260470942384 aaaaaaaaaaaaaa
    2260469193504 a

    4. 变量和对象的关系

    总结:变量和对象是两个独立个体。

    堆内存中的可变对象和不可变对象

    Python 在 堆内存(heap)中分配的对象分成两类:可变对象和不可变对象。

    • 所谓可变对象是指:对象的内容的值(不是变量地址)是可变的,值的改变,不会开辟新的内存地址空间。例如 list。如:
    a= [1]
    print(id(a),id(a[0]),a)
    a= [1,2]
    print(id(a),id(a[0]),a)
    a[0]=89
    print(id(a),id(a[0]),a)

    文字解说上面的代码:

    第1行代码:a= [1]:


    1. 创建了一个[]对象,此时会给[]分配一个堆内存地址

    1. 在[]对象里面,又创建了一个1的对象,此时会给1既(a[0]的引用)分配一个新的堆内存地址

    1. a持有的是[]对象的地址,而不是a[0]对象的地址

    • 所以最终打印出来的结果:print(id(a),id(a[0]),a)
    • 输出结果:2443469767688 140736017195856 [1]
    • 2443469767688 []对象的堆内存地址
    • 140736017195856 a[0]对象1的堆内存地址

    第3行代码:a= [1,2] :


    1. 创建了一个[1,2] 对象,此时会给[1,2] 分配一个堆内存地址,原来2443469767688 []对象的堆内存地址,在没有没有对应的变量进行绑定关联的,无家可归,只能被回收!引用计数变0,被系统返厂了!

    1. 新建[1,2]对象里面,又创建了一个2的对象,此时会给2既(a[1]的引用)分配一个新的堆内存地址,所以新增了一个地址了

    1. a持有的是新创建[1,2]对象的地址,而不是原来2443469767688 []对象对象的地址了

    1. 但是我们的新创建出来的[1,2]对象的地址中原来的a[0]=1对象的地址还是存在,还是绑定在原来的 140736017195856 a[0]对象1的堆内存地址,所以它的地址不变。

    • 所以最终打印出来的结果:print(id(a),id(a[0]),a)
    • 输出结果:2443469731464 140736017195856 [1, 2]
    • 2443469731464 [1,2] 对象的堆内存地址(a地址有变化)
    • 140736017195856 a[0]对象1的堆内存地址(没有变化)

    第5行代码:a[0]=89:

    修改a[0]原来的对象1,现在变为了89,相当于创建了新的对象,1原来的对象没有引用了,会被回收。此时我们的[]对象里面的元素内容有变化了

    • 所以最终打印出来的结果:print(id(a),id(a[0]),a)
    • 输出结果:2443469731464 140736017198672 [89, 2]
    • 2443469731464 [1,89] 对象的堆内存地址(a地址保持原来[]的2443469731464地址没有变化)
    • 140736017195856 a[0]对象1的堆内存地址已经没了引用,新增了89的地址(有变化)

    综上分析:

    • 而不可变的对象则相反,表示的是对象所指向的堆内存中的值不能被改变,就是对象的值不能改变。(如:str , int,float,bytes)
    print('==================>')
    print("str是不可变对象")
    a = '小钟同学'
    print('a引用地址',id(a))
    b = '小钟同学'
    print('b引用地址',id(b))
    print('is判断引用是否一样:',a is b)
    print('==判断是对象值内容是否一样',a ==b)
    print('==================>')
    b = '小钟同学+一百分(对象值改变了,会开辟新的空间地址)'
    print('b引用地址',id(b))
    print('is判断引用是否一样:',a is b)
    print('==判断是对象值内容是否一样',a ==b)

    第3行代码:a = '小钟同学':


    1. 创建了一个str对象,此时会给str分配一个堆内存地址

    1. a持有的是str对象的地址的引用

    • 所以最终打印出来的结果:print('a引用地址',id(a))
    • 输出结果:a引用地址 2532269760312 第5行代码:b = '小钟同学':

    1. 因为str对象是不可变的对象,它的堆内存的地址是不可变,此时会给str分配一个堆内存地址(还是原来‘小钟同学’的堆内存的地址)

    1. b持有的是str对象的地址的引用(其实还是指向了'小钟同学'堆内存地址)

    • 所以最终打印出来的结果:print('b引用地址',id(b))
    • 输出结果:b引用地址 2532269760312

    • 所以最终打印出来的结果:
    • print('is判断引用是否一样:',a is b)
    • print('==判断是对象值内容是否一样',a ==b)

    • is判断引用是否一样: True
    • ==判断的是对象值内容是否一样 True

    第10行代码:b = '小钟同学+一百分(对象值改变了,会开辟新的空间地址)':


    1. 创建了一个新的str对象,此时会给新的str分配一个新的堆内存地址

    1. b此时是新的str分配一个新的堆内存地址分配的一个栈内存地址(对象的引用)

    1. 原来就的‘小钟同学’对象的引用计数-1,但是还a持有它的引用,所以此时系统不会回收这个内存

    • 所以最终打印出来的结果:print('b引用地址',id(b))
    • 输出结果:b引用地址 2545345760345(b的地址改变了 a的地址没有改变)

    深拷贝和浅拷贝的分析

    因为Python对象的分类,所以在涉及拷贝的时候,我们也需要区分开拷贝的对象。

    深拷贝和浅拷贝区别:

    • 浅拷贝只复制指向某个对象的指针(地址),并非拷贝对象(对象的值),所以新旧对象还是共享同一块内存,如果对象值修改,那么会引发联动变化(但是要区分开拷贝的对象类型)。
    • 深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
    1.浅拷贝

    关于浅拷贝几个需要关注的点,浅拷贝拷贝的对象分:可变和不可变

    • 如果被拷贝对象是不可变对象,则不会生成新的对象,a,b共享同一个堆内存地址。如:
    print('==================>')
    print("str是不可变对象")
    a = '小钟同学'
    print('a引用地址',id(a))
    import copy
    b = copy.copy(a)
    print('浅拷贝后a引用地址',id(a))
    print('浅拷贝后b引用地址',id(b))
    print('浅拷贝后a里面对象的值',a)
    print('浅拷贝后b里面对象的值',b)
    输出结果:
    ==================>
    str是不可变对象
    a引用地址 2241728814904
    浅拷贝后a引用地址 2241728814904
    浅拷贝后b引用地址 2241728814904
    浅拷贝后a里面对象的值 小钟同学
    浅拷贝后b里面对象的值 小钟同学

    • 如果被拷贝对象是可变对象,则会生成新的对象,但是只会对可变对象最外层进行拷贝

    print('==================>')
    print("lits是可变对象")
    a = ['小钟同学']
    print('a引用地址',id(a))
    print('a[0]引用地址',id(a[0]))


    import copy
    b = copy.copy(a)
    print('浅拷贝后a引用地址',id(a))
    print('浅拷贝后a[0]里面引用地址',id(a[0]))
    print('==================>')
    print('浅拷贝后b引用地址(生成了的新地址)',id(b))
    print('浅拷贝后b[0]里面引用地址(共享a[0])对象的引用的地址',id(b[0]))
    print('浅拷贝后b里面对象的值',b)

    输出结果:
    ==================>
    lits是可变对象
    a引用地址 1988972495880
    a[0]引用地址 1988967407416
    浅拷贝后a引用地址 1988972495880
    浅拷贝后a[0]里面引用地址 1988967407416
    ==================>
    浅拷贝后b引用地址(生成了的新地址) 1988999589128
    浅拷贝后b[0]里面引用地址(共享a[0])对象的引用的地址 1988967407416
    浅拷贝后b里面对象的值 ['小钟同学']

    列表浅拷贝前:
    列表浅拷贝后:
    2.深拷贝
    • 如果被拷贝对象是不可变对象,则不会生成新的对象,a,b共享同一个堆内存地址(和浅拷贝一样)
    print('==================>')
    print("str是不可变对象")
    a = '小钟同学'
    print('a引用地址',id(a))
    import copy
    b = copy.deepcopy(a)

    print('深拷贝后a引用地址',id(a))
    print('深拷贝后a对象的值',a)
    print('深拷贝后b里面引用地址',id(b))
    print('深拷贝后b对象的值',b)
    输出的结果为:
    ==================>
    str是不可变对象
    a引用地址 2847099133752
    深拷贝后a引用地址 2847099133752
    深拷贝后a对象的值 小钟同学
    深拷贝后b里面引用地址 2847099133752
    深拷贝后b对象的值 小钟同学
    • 如果被拷贝对象是可变对象,则会生成新的对象,甚至于包括内层的对象,如果也可变也会进行一个新的对象的生成,(但如果内层对象是不可变的话,则共享的还是原来的地址):

    意思就是:列表里面还有列表的话,产生的拷贝的,时候,所有的可变一样会生产新的地址 如:

    print('==================>')
    print("list是可变对象")
    a = ['小钟同学',["内嵌列表的小钟同学"]]
    print('深拷贝前---》a引用地址',id(a))
    print('深拷贝前---》a引用的对象的值是:',a)
    print('深拷贝前---》a[0]引用地址',id(a[0]))
    print('深拷贝前---》a[0]引用的对象的值是:',a[0])
    print("**重点关注内嵌的列表对象**")
    print('深拷贝前---》a[1]引用地址',id(a[1]))
    print('深拷贝前---》a[1]引用的对象的值是:',a[1])

    import copy
    b = copy.deepcopy(a)
    print('==================深拷贝后====a状态==============>')
    print('深拷贝后---》a引用地址',id(a))
    print('深拷贝后---》a引用的对象的值是:',a)
    print('深拷贝后---》a[0]引用地址',id(a[0]))
    print('深拷贝后---》a[0]引用的对象的值是:',a[0])
    print("**重点关注内嵌的列表对象**")
    print('深拷贝后---》a[1]引用地址',id(a[1]))
    print('深拷贝后---》a[1]引用的对象的值是:',a[1])

    print('==================深拷贝后====b状态==============>')
    print('深拷贝后---》b引用地址',id(b))
    print('深拷贝后---》b引用的对象的值是:',b)
    print('深拷贝后---》b[0]引用地址(因为是不可变对象的拷贝,地址保持不变)',id(b[0]))
    print('深拷贝后---》b[0]引用的对象的值是:',b[0])
    print("**重点关注内嵌的列表对象**")
    print('深拷贝后---》b[1]引用地址',id(b[1]))
    print('深拷贝后---》b[1]引用的对象的值是:',b[1])
    print('==================深拷贝后==============>')
    print('==================深拷贝后====对比:b[0]和a[0]=============>')
    print(b[0] is a[0])
    print('==================深拷贝后====对比:b[1]和a[1]=============>')
    print(b[1] is a[1])

    输出的结果为:

    ==================>
    list是可变对象
    深拷贝前---》a引用地址 2238894698248
    深拷贝前---》a引用的对象的值是: ['小钟同学', ['内嵌列表的小钟同学']]
    深拷贝前---》a[0]引用地址 2238889527584
    深拷贝前---》a[0]引用的对象的值是: 小钟同学
    **重点关注内嵌的列表对象**
    深拷贝前---》a[1]引用地址 2238893966472
    深拷贝前---》a[1]引用的对象的值是: ['内嵌列表的小钟同学']
    ==================深拷贝后====a状态==============>
    深拷贝后---》a引用地址 2238894698248
    深拷贝后---》a引用的对象的值是: ['小钟同学', ['内嵌列表的小钟同学']]
    深拷贝后---》a[0]引用地址 2238889527584
    深拷贝后---》a[0]引用的对象的值是: 小钟同学
    **重点关注内嵌的列表对象**
    深拷贝后---》a[1]引用地址 2238893966472
    深拷贝后---》a[1]引用的对象的值是: ['内嵌列表的小钟同学']
    ==================深拷贝后====b状态==============>
    深拷贝后---》b引用地址 2238921063944
    深拷贝后---》b引用的对象的值是: ['小钟同学', ['内嵌列表的小钟同学']]
    深拷贝后---》b[0]引用地址(因为是不可变对象的拷贝,地址保持不变) 2238889527584
    深拷贝后---》b[0]引用的对象的值是: 小钟同学
    **重点关注内嵌的列表对象**
    深拷贝后---》b[1]引用地址 2238895013512
    深拷贝后---》b[1]引用的对象的值是: ['内嵌列表的小钟同学']
    ==================深拷贝后==============>
    ==================深拷贝后====对比:b[0]和a[0]=============>
    True
    ==================深拷贝后====对比:b[1]和a[1]=============>
    False

    深拷贝和浅拷贝总结:

    • 拷贝对象是不可变地址,则共享的是同一个地址
    • 拷贝对象是可变,都会生成新的地址

    End

    纯属个人实践中相关经验之谈,如有纰漏,还希望大佬们多多提点!小钟同学 | 文  【原创】| QQ:308711822


    原文作者:程序员小钟同学;如有侵权,请联系管理员删除。

    请登录后发表评论 登录
    全部评论

    注册时间:2021-02-20

    • 博文量
      224
    • 访问量
      111698