Python——面向对象(OOP)封装篇(实例对象、类对象、类方法、实例方法、静态方法、类属性、实例属性)

面向对象与面向过程

面向对象编程方式

面向对象程序设计的基本思想是把人们对现实世界的认识过程应用到程序设计上来,使得现实世界中的事物与程序中的类和对象直接对应

两种编写代码的方式——“面向过程"和"面向对象”

“面向过程"的编程特点就是往往要通过 函数 和 数据 实现需求,这种编程方式使得"可用数据”(即普通变量)和"目标功能"(即函数)独立开来,容易出现编程错误,而"面向对象"的编程方式可以将 “可用数据”(即后面提到的属性)和 “目标功能”(即后面提到的方法)结合起来,形成一个整体,从而有效弥补"面向过程"编程方式的在这方面的不足

结合实际生活的中的例子简单对"面向过程"和"面向对象"进行一个区分:
"面向过程"就是当我们要吃饭的时候,自己动手做饭做菜,自给自足
"面向对象"就是当我们要吃饭的时候,去使用手机这个对象,点一个外卖

面向对象具有三大特征:封装、继承、多态

其中封装是基础,会使用大量篇幅讲解,继承的篇幅次之,而多态由于在Python中体现得并不十分明显,于是篇幅最短,但是会在其他面向对象编程语言的讲解中加以描述

本篇讲解面向对象三大特征中的"继承"

基本概念

封装

封装就是前面谈到的将数据(即属性)和功能(即方法)进行整合,然后封装到一个类中的行为

类对象

在运行下面这个代码的时候,就会进行一个类名为C的类对象的创建,可以类比于函数对象的创建过程。一个类对象可以分成这几个部分:类名、类属性、方法、被继承的父类名

class C:	#创建了一个类对象,变量C保存着该类对象的引用(如同函数名其实是一个变量,保存着函数对象的引用一样)
    "这里是关于类对象C的描述"      #类对象中可以像函数一样,添加注释,使用help可以查询到
    pass

help()

#输入内容: __main__.C
#输出结果:
"""
Help on class C in __main__:

__main__.C = class C(builtins.object)
 |  这里是关于类对象C的描述
 |
 |  Data descriptors defined here:
 |
 |  __dict__
 |      dictionary for instance variables (if defined)
 |
 |  __weakref__
 |      list of weak references to the object (if defined)
"""

值得一提的是,函数代码块中的代码只有在函数被调用的时候才会执行,而类代码块中的代码在创建类的时候就会执行

class C:
    print('因吹斯挺')
   
#输出结果:因吹斯挺

虽然还没提到后面的概念,但是请记住一点:类对象对应的内存空间中存放着所有方法(即实例方法、类方法、静态方法)以及类属性

Python中处处都是引用,在类对象中也是一样,其实类对象空间中就是一堆变量,这些变量保存着一些对象的id,这些对象有些是函数对象(对应的变量称为方法),有些是非函数对象(对应的变量称为类属性)

实例对象

所谓实例对象,就是将类对象实例化后的对象,可以将类对象看成一个建筑图纸,将该类对象实例化后得到的实例对象看成该图纸对应的建筑,而且同一个类对象实例化出来的实例对象是没有联系的(这种实例对象间的相互独立仅仅是相对来说,必要时,我们可以通过让一个对象的属性保存另一个对象的引用的方式,来关联两个对象,实现一定的功能)

在运行代码"实例对象名 = 类名()"的时候,就会进行实例对象的创建

class C:
    pass

ins = C()		#ins即为类对象C对应的一个实例对象

方法

通常类中保存函数对象id的变量称为方法,类中的方法,实际上也就是函数对象,即函数放到类中,改了个名字叫做方法,其实二者没有本质区别,但是,方法是一种特殊的函数,其特殊性不仅仅体现在位于类中,还体现在有些方法可以自动传入实参

切记,所有的方法的 id 都只保存在类对象中,实例对象中是不存在保存着方法 id 的变量的

class C:
    def func(self):     #这里的self就是一个形参而已,名字随意,只不过一般第一个形参取名self
        pass

类属性

通常类中保存非函数对象 id 的变量称为类属性。由于Python中处处为引用,类中所谓的方法和类属性实际上就是一堆变量而已,于是本质上方法和类属性就是一类东西,因此你可能会发现方法和类属性具有一些相同的特征,这是极其正常的现象。有的时候,方法和类属性甚至可以统称为属性

class C:
    data = 10

实例属性

实例属性就是实例对象唯一拥有的东西,即实例对象中仅仅保存着实例属性的 id,实例属性中也可以保存着函数的 id,可以实现和类对象中的方法完全一样的功能,但是一般来说不这么做,因为就像开头说到的,"面向对象"的编程方式,其目的就是为了将功能(即函数)和数据(即普通变量)有机地整合起来,形成一个类对象,即一般直接将所有的功能都集中到类对象中

如果实例属性保存的 id 为一个函数代码块的 id,那这个实例属性我们也不会称其为"方法",所以方法只有在类对象中才会出现,即只有类对象才会保存方法代码块的 id,保存函数代码块id的实例属性并不算数

class C:
    def func(self):
        self.num = 10       #这里的num就是一个实例属性

实例方法

实例方法是三大方法中最贴近Python基础语法学习者的,是长得最像普通函数的一种方法,就像前面说的,方法和函数的区别就在于不仅在于函数在类对象外部定义,而方法在类对象内部定义,更重要的是方法是会自动传入实参的,这意味着,在定义方法的时候,必须要设置相应的形参进行接收,否则就会导致异常的抛出

对于实例方法,其被调用的时候会自动传入一个实例对象的引用作为实参,而且接收该引用的实参必定是方法中的第一个形参(因为既然是自动传入,自然是比我们手动传入实参要快,所以对应第一个形参)。所以一般而言,我们将实例对象的第一个形参名称取为self,并将其对应于实例对象,也不是说非要取为self,更不是取其他名称就会抛出异常,而是一般都这样干,这是一种不成文的规定,并不是语法的硬性要求

class C:
    def func(self):      #func就是一个实例方法,至少要设置一个形参接收自动传入的实例对象的引用
        pass

类方法

类方法为一个被装饰器classmethod装饰的函数(没有学习过装饰器的读者可以移步Python——三器一包(迭代器、生成器、装饰器、闭包))
对于类方法,和实例对象一样,会自动传入一个实参,只不过类方法传入的实参为类对象的引用,当然和实例对象一样,自动传入的实参都是与第一个形参相对应的,即第一个形参就是被传入的类对象,同样地,一般我们也会给这个形参取一个特殊的名称,为cls

class C:
    @classmethod
    def func(cls):      #func就是一个类方法,至少要设置一个形参接收自动传入的类对象的引用
        pass

静态方法

静态方法为一个被装饰器staticmethod装饰的函数(没有学习过装饰器的读者可以移步Python——三器一包(迭代器、生成器、装饰器、闭包))
对于静态方法,它就是很独特了,它是三个方法中唯一不会自动传参的方法,也就是说,在定义静态方法的时候,可以选择不设置任何形参

class C(object):
    @staticmethod
    def func():      #func就是一个静态方法,没有自动传参机制,无需设置一个形参接收对象的引用
        pass

用法演示

通过上面的讲解,相信大家已经对Python面向对象的几个基本概念有了大概的了解
接下来会依次对每一个概念的用法配合实例进行一个细致的讲解

这里先介绍一个独特的属性,即__dict__,这个属性是每一个对象创建出来默认就有的,该属性保存了对象的所有属性信息

具体来说,对于实例对象,__dict__这个属性为实例属性,保存了该实例对象中所有实例属性的名称和对应的值;对于类对象,这个属性为类属性,保存了该类对象中所有类属性的名称和对应的值,以及所有方法的名称和代码块所在id

类属性

  • 创建途径
    • 直接在类对象中进行创建(即在定义类对象的时候就会创建)
    • 通过类对象创建,代码格式为"类对象.类属性名 = 属性值"(即在定义类对象后创建)

直接在类对象中进行创建

class C:
    a = 10

print(C.__dict__)   #以字典形式打印出类对象中的所有属性(包括类属性和方法)

#输出结果:
"""
{'__module__': '__main__', 'a': 10, '__dict__': <attribute '__dict__' of 'C' objects>, '__weakref__': <attribute '__weakref__' of 'C' objects>, '__doc__': None}
"""

通过类对象创建

class C:
    pass

C.a = 10
print(C.__dict__)

#输出结果:
"""
{'__module__': '__main__', '__dict__': <attribute '__dict__' of 'C' objects>, '__weakref__': <attribute '__weakref__' of 'C' objects>, '__doc__': None, 'a': 10}
"""
class C:
    @classmethod
    def func(cls):
        cls.data = 10   #前面说过,类方法会自动传入类对象的引用,所以这里的cls就对应类对象C

print(C.__dict__)
C.func()        #使用类对象调用类方法
print(C.__dict__)

#输出结果:
"""
{'__module__': '__main__', 'func': <classmethod object at 0x00000204F974A760>, '__dict__': <attribute '__dict__' of 'C' objects>, '__weakref__': <attribute '__weakref__' of 'C' objects>, '__doc__': None}
{'__module__': '__main__', 'func': <classmethod object at 0x00000204F974A760>, '__dict__': <attribute '__dict__' of 'C' objects>, '__weakref__': <attribute '__weakref__' of 'C' objects>, '__doc__': None, 'data': 10}
"""
  • 访问途径
    • 通过该类对象创建的实例对象访问,代码格式为"实例对象.类属性名"(有些情况下会访问不到,后面会介绍)
    • 通过该类对象访问,代码格式为"类对象.类属性名"(100%可以访问到)
    • 在类对象内部,方法外部直接访问(基本不会用到,至少我目前是基本不用这个方式访问)

通过该类对象创建的实例对象访问

class C:
    a = 10

ins = C()		#先创建一个C类对象对应的实例对象ins
print(ins.a)

#输出结果:10

通过该类对象访问

class C:
    a = 10

print(C.a)

#输出结果:10

在类对象内部,方法外部直接访问

class C:
    a = 10
    print(a)

#输出结果:10
  • 修改途径
    • 通过该类对象修改,代码格式为"类对象.类属性名 = 新属性值"
    • 在类对象内部,方法外部直接修改
class C:
    pass

C.data = 10			#通过类对象创建一个名为data的类属性,初始值为10
print(C.data)
C.data = 20			#通过类对象修改类属性值
print(C.data)

#输出结果:
"""
10
20
"""

实例属性

  • 创建途径
    • 通过该实例对象创建,代码格式为"实例对象.实例属性名 = 属性值"

通过该实例对象创建

class C:
    pass

ins = C()
print(ins.__dict__)
ins.data = 10
print(ins.__dict__)

#输出结果:
"""
{}
{'data': 10}
"""
class C:
    def func(self):
        self.data = 10      #前面说过,实例方法会自动传入实例对象的引用,所以这里的self就对应实例对象ins

ins = C()
print(ins.__dict__)
ins.func()			#使用实例对象调用实例方法
print(ins.__dict__)

#输出结果:
"""
{}
{'data': 10}
"""
  • 访问途径
    • 通过该实例对象访问,代码格式为"实例对象.实例属性名"

通过该实例对象访问

class C:
    pass

ins = C()
ins.data = 10
print(ins.data)

#输出结果:10

在前面的类属性的访问方式中的第二种方式,即"通过该类对象创建的实例对象来访问类属性",可能有比较认真的读者就会想到,为什么类对象中的类属性,可以被其所创建的实例对象访问呢?

原因是每一个对象都会具有一个属性(具体来说这是一个魔法属性),即__class__,它的作用就是能够让对象的可以访问到其类对象的内存空间,可以理解为一个儿子总是会记得它母亲的住址的。后面实例对象可以调用类方法、静态方法的原因也是如此,后续就不再赘述了

而一个类对象中是不存在关于其创建的实例对象的相关信息的,即类对象无法访问其创建的实例对象的内存空间,所以类对象无法访问实例对象中的实例属性

class C:
    pass

ins = C()
ins.data = 10
C.data          #使用类对象尝试访问实例对象的实例属性

#引发异常:type object 'C' has no attribute 'data'
  • 修改途径
    • 通过该实例对象修改,代码格式为"实例对象.实例属性名 = 新属性值"

通过该实例对象修改

class C:
    pass

ins = C()
ins.data = 10			#通过实例对象创建一个名为data的实例属性,初始值为10
print(ins.data)
ins.data = 20			#通过实例对象修改实例属性值
print(ins.data)

#输出结果:
"""
10
20
"""

实例方法

  • 创建途径
    • 直接在类对象中进行创建(即在定义类对象的时候就创建)
    • 通过类对象创建,代码格式为"类对象.实例方法名 = 函数"(即在定义类对象后创建)

直接在类对象中进行创建

class C:
    func1 = lambda self, x : x		#注意,实例方法会自动传入实例对象的引用,要设置相应的形参接收

    def func2(self):
        pass

print(C.__dict__)
print(type(C.func1))
print(type(C.func2))

#输出结果:
"""
{'__module__': '__main__', 'func1': <function C.<lambda> at 0x0000019CD8054B80>, 'func2': <function C.func2 at 0x0000019CD84B95E0>, '__dict__': <attribute '__dict__' of 'C' objects>, '__weakref__': <attribute '__weakref__' of 'C' objects>, '__doc__': None}
<class 'function'>
<class 'function'>
"""

通过类对象创建

func1 = lambda self, x : x

def func2(self):
    pass

class C:
    pass

C.func_1 = func1
C.func_2 = func2
print(C.__dict__)
print(type(C.func_1))
print(type(C.func_2))

#输出结果:
"""
{'__module__': '__main__', '__dict__': <attribute '__dict__' of 'C' objects>, '__weakref__': <attribute '__weakref__' of 'C' objects>, '__doc__': None, 'func_1': <function <lambda> at 0x000001BABD5AD820>, 'func_2': <function func2 at 0x000001BABE404B80>}
<class 'function'>
<class 'function'>
"""
  • 调用途径
    • 通过该类对象创建的实例对象访问,代码格式为"实例对象.实例方法名()"(有些情况下会访问不到,后面会介绍)
    • 通过该类对象访问(基本不会用到,100%可以访问到)
    • 在类对象内部,方法外部直接调用(基本不会用到,至少我目前是基本不用这个方式调用)

对于实例方法会自动传参的严谨说法
要说明一点,实例方法是否有实参传入取决于调用该方法的方式是通过实例对象调用还是通过类对象调用(如果是通过实例对象调用,那么会自动传入实参;如果通过类对象调用,则不会自动传入实参),也就是说,前面关于实例方法一定会自动传入实例对象的引用是不准确的,当时那么说是因为大部分情况下,我们并不会通过类对象去调用实例方法,而是通过实例对象调用实例方法,但是更加严谨的说法是现在这个说法

自定义对象的案例

class C:
    def func(self, data):
        print(data + 1)

ins = C()
ins.func(1)

#输出结果:2

class C:
    def func(self, data):
        print(data + 1)

ins = C()
C.func(ins, 1)

#输出结果:2

内置对象的案例

lst = [1, 2, 3]
lst.append(4)       #使用实例对象调用实例方法
print(lst)

#输出结果:[1, 2, 3, 4]

lst = [1, 2, 3]
list.append(lst, 4)   #使用类对象调用实例方法,由于类对象不会自动传入实例对象的引用,于是我们要手动传入
print(lst)

#输出结果:[1, 2, 3, 4]

通过该类对象创建的实例对象访问(那个实例对象调用了这个实例方法,这个方法中的形参self保存的就是谁的引用)

class C:
    func1 = lambda self, x : x + 1	#由于实例对象会自动传入实参,必须要设置形参接收

    def func2(self):
        print(self)
        print('阿梅井')

ins = C()
print(ins.func1(1))
ins.func2()

#输出结果:
"""
2
<__main__.C object at 0x000002569ECBA7C0>
阿梅井
"""

通过该类对象访问

class C:
    func1 = lambda x : x + 1		#由于类对象不会自动传入实参,也就不需要设置形参接收

    def func2():
        print('阿梅井')

print(C.func1(1))
C.func2()

#输出结果:
"""
2
阿梅井
"""

在类对象内部,方法外部直接调用

class C:
    func1 = lambda x : x + 1		#由于类对象不会自动传入实参,也就不需要设置形参接收

    def func2():
        print('阿梅井')

    print(func1(1))
    func2()

#输出结果:
"""
2
阿梅井
"""

类方法

  • 创建途径
    • 直接在类对象中进行创建(即在定义类对象的时候就创建)
    • 通过类对象创建,代码格式为"类对象.类方法名 = 函数"(即在定义类对象后创建)

直接在类对象中进行创建

class C:
    @classmethod
    def func(cls):
        pass

print(C.__dict__)
print(type(C.func))

#输出结果:
"""
{'__module__': '__main__', 'func': <classmethod object at 0x000001EC85D4A760>, '__dict__': <attribute '__dict__' of 'C' objects>, '__weakref__': <attribute '__weakref__' of 'C' objects>, '__doc__': None}
<class 'method'>
"""

通过类对象创建

@classmethod
def func(cls):
    pass

class C:
    pass

C.func_1 = func
print(C.__dict__)
print(type(C.func_1))

#输出结果:
"""
{'__module__': '__main__', '__dict__': <attribute '__dict__' of 'C' objects>, '__weakref__': <attribute '__weakref__' of 'C' objects>, '__doc__': None, 'func_1': <classmethod object at 0x000001D5E917AA90>}
<class 'method'>
"""
  • 调用途径
    • 通过该类对象创建的实例对象调用,代码格式为"实例对象.类方法名()"(有些情况下会访问不到,后面会介绍)
    • 通过该类对象调用(基本不会用到,100%可以访问到)

对于类方法来说,无论是实例对象调用还是类对象调用,都是会自动传入类对象的引用的,这和前面的说法一致

通过该类对象创建的实例对象访问

class C:
    @classmethod
    def func(cls):
        print(cls)
        print('阿梅井')

ins = C()
ins.func()

#输出结果:
"""
<class '__main__.C'>
阿梅井
"""

通过该类对象访问

class C:
    @classmethod
    def func(cls):
        print(cls)
        print('阿梅井')

C.func()

#输出结果:
"""
<class '__main__.C'>
阿梅井
"""

静态方法

  • 创建途径
    • 直接在类对象中进行创建(即在定义类对象的时候就创建)
    • 通过类对象创建,代码格式为"类对象.类方法名 = 函数"(即在定义类对象后创建)

直接在类对象中进行创建

class C:
    @staticmethod
    def func():
        pass

print(C.__dict__)
print(type(C.func))

#输出结果:
"""
{'__module__': '__main__', 'func': <staticmethod object at 0x000001F0FC87A7C0>, '__dict__': <attribute '__dict__' of 'C' objects>, '__weakref__': <attribute '__weakref__' of 'C' objects>, '__doc__': None}
<class 'function'>
"""

通过类对象创建

@staticmethod
def func():
    pass

class C:
    pass

C.func_1 = func
print(C.__dict__)
print(type(C.func_1))

#输出结果:
"""
{'__module__': '__main__', '__dict__': <attribute '__dict__' of 'C' objects>, '__weakref__': <attribute '__weakref__' of 'C' objects>, '__doc__': None, 'func_1': <staticmethod object at 0x00000218EF77ACD0>}
<class 'function'>
"""
  • 调用途径
    • 通过该类对象创建的实例对象调用,代码格式为"实例对象.静态方法名()"(有些情况下会访问不到,后面会介绍)
    • 通过该类对象调用(基本不会用到,100%可以访问到)

对于静态方法,为了是通过实例对象调用还是通过类对象调用,都是不会自动传入任何实参的,所以不需要设置实参进行接收

通过该类对象创建的实例对象访问

class C:
    @staticmethod
    def func():
        print('因吹斯挺')

ins = C()
ins.func()

#输出结果:因吹斯挺

通过该类对象访问

class C:
    @staticmethod
    def func():
        print('因吹斯挺')

C.func()

#输出结果:因吹斯挺

如果有读者同步在做笔记的话,应该可以发现,实例方法、类方法、静态方法和类属性的创建和访问的途径完全是一样的,这也佐证了前面提到的类对象中的三种方法和类属性本质就是一种东西(通常会统称为"属性"),具有一些相同的特点

特性演示

通过上面的代码案例,就基本将所有的用法展示了一遍,希望读者后续可以自行练习巩固一下
接下来的内容是对上面的深化,在掌握了基本用法的基础上,还需要了解一些特性,以便将各种属性、方法用得出神入化

类属性

类属性的优势
类属性的优势就是该类对象创建出的所有实例对象都共享同一个类属性,即在任意一个实例对象通过调用方法(一般是类方法)对类属性进行修改(类方法内部使用类对象对类属性进行修改)后,其他所有的实例对象再次进行访问的时候,就可以看到修改后的值,并且可以再次进行更改
案例:使用类属性统计一个类对象创建了多少个实例对象

class C:
    ins_num = 0             #为一个类对象,负责记录当前实例对象的个数

    @classmethod
    def add_num(cls):       #负责进行累加实例对象的个数
        cls.ins_num += 1

c = C()         #创建一个实例对象
c.add_num()     #实例对象调用类方法,进行个数累计
c = C()
c.add_num()
c = C()
c.add_num()

print(c.ins_num)  

#输出结果:3

从上面的代码可以看到,计数必须使用实例对象调用类方法实现,如果实例对象个数过多,万一写漏了几个,那不是会导致计数错误?
为了优化代码,我们可以引入方法__init__(和前面提到的__dict__一样,该方法也是所有对象默认就存在的),通过该方法,可以实现实例对象被创建的时候,就会自动调用(是不是非常amazing啊),具体来说,__init__方法自动调用的时机在实例对象创建之后,在执行代码"ins = C( )" (即进行引用赋值操作之前),详细的说法请移步Python——魔法方法,总之,使用了__init__方法,就可以实现自动计数,减少代码量

class C:
    ins_num = 0               #为一个类对象,负责记录当前实例对象的个数

    def __init__(self):       #负责进行累加实例对象的个数,在实例对象创建的时候自动调用
        self.__class__.ins_num += 1     #实例对象中具有__class__实例属性,可以访问到类对象的内存空间
                                        #即 self.__class__ 相对于 类对象C
        
c = C()         #创建一个实例对象 
c = C()
c = C()

print(c.ins_num)    

#输出结果:3

实例属性

实例属性的优势
实例属性的优势就是一个实例对象,无论调用哪一个实例方法,都可以实现对该实例对象的实例属性值的访问和修改操作(可以类比于"面向过程"编程方式中的多个函数使用同一个全局变量)

class C:
    def fun1(self):
        self.data = 1

    def fun2(self):
        self.data = 2

    def printf(self):
        print(self.data)


ins = C()
ins.data = 0        #进行实例属性的创建
ins.printf()        #通过实例方法进行实例属性的访问

ins.fun1()          #通过实例方法进行实例属性值的修改
ins.printf()

ins.fun2()          #通过实例方法进行实例属性值的修改
ins.printf()

#输出结果:
"""
0
1
2
"""

实例属性与类属性

1.如果实例对象中的实例属性与类对象中的类属性同名,是无法访问类对象的那个类属性的
其实有点类似于局部变量会暂时覆盖掉全局变量的机制,这也是为什么前面在提访问途径的时候,会写上"(有些情况下会访问不到,后面会介绍)"

class C:
    data = "类对象的data"       #创建类对象的类属性

ins = C()
print("刚刚创建时,实例对象空间内部:", ins.__dict__)
print(ins.data)                 #探究访问的是哪一个data
ins.data = "实例对象的data"     #创建实例对象的同名实例属性
print("创建实例属性后,实例对象空间内部:", ins.__dict__)
print(ins.data)                 #探究访问的是哪一个data
del ins.data
print("删除实例属性后,实例对象空间内部:", ins.__dict__)
print(ins.data)                 #探究访问的是哪一个data

#输出结果:
"""
刚刚创建时,实例对象空间内部: {}
类对象的data
创建实例属性后,实例对象空间内部: {'data': '实例对象的data'}
实例对象的data
删除实例属性后,实例对象空间内部: {}
类对象的data
"""

由上可以看出,确实是实例对象的data的存在会阻止实例对象对类对象的同名类属性data的访问

原因(代码运行的本质):

  • 一个实例对象要对一个属性进行访问的过程
    • 查看该实例对象的内部空间是否具有同名属性
      • 如果没有,则通过实例对象的__class__属性查看该实例对象的类对象是否具有同名属性
      • 如果有,则直接进行访问,不再看类对象的内存空间

前面说过,从本质上来看,类对象中的类属性和各种方法都是一种类型的东西,都只不过是保存id的变量罢了,而且通常被通称为"属性"
于是如果实例对象中具有与类对象中的方法(无论是哪一种)同名的实例属性(不管该实例属性是否保存了一个函数代码块的id),该实例属性都会将同名的方法给掩盖掉,使得该方法无法正常调用

class C:
    def func(self):
        print(123)

ins = C()
print(C.__dict__)
ins.func = 10
print(C.__dict__)
ins.func()

#输出结果:
"""
{'__module__': '__main__', 'func': <function C.func at 0x000002C1D66C4B80>, '__dict__': <attribute '__dict__' of 'C' objects>, '__weakref__': <attribute '__weakref__' of 'C' objects>, '__doc__': None}
{'__module__': '__main__', 'func': <function C.func at 0x000002C1D66C4B80>, '__dict__': <attribute '__dict__' of 'C' objects>, '__weakref__': <attribute '__weakref__' of 'C' objects>, '__doc__': None}
"""
#打印完后,引发异常: 'int' object is not callable

会引发异常就是因为实例对象中具有与类对象中实例方法func同名的实例属性,而且要命的是,这个实例属性还是一个整数类型的对象10,在进行调用的时候,实际上是先去寻找名为func的东西,依照上面提到的查找顺序,先去实例对象中查找,很"幸运",一下子就找到了,就是那个实例属性10,然后进行调用操作,而我们知道,整数类型的对象是不可能被调用的,于是,嘭!异常就被抛出了

2.实例对象只能对实例属性进行创建和修改,无法对类属性进行创建和修改
从上面的例子也可以看到,在执行代码"ins.data = “实例对象的data”“的时候,并不是将类对象中的类属性data的属性值改为"实例对象的data”,而是创建一个该实例对象的实例属性

class C:
    data = 1       #创建类对象的类属性

ins = C()
print("实例对象空间内部:", ins.__dict__)
ins.data += 1
print("实例对象空间内部:", ins.__dict__)
print("类对象空间内部:", C.data)

#输出结果:
"""
实例对象空间内部: {}
实例对象空间内部: {'data': 2}
类对象空间内部: 1
"""

从上面的代码可以看出,确实是无法通过实例对象对类对象的类属性进行修改的,反而导致新创建了一个同名的实例对象
题外话:也可以看出,本质上 所谓的复合运算符+= 就是先做加法运算后做赋值运算的过程

实例方法

由于实例方法具有实例对象的引用,于是实例方法一般用于实现实例属性值的修改

这时有读者可能就要问了,为什么不在方法的外部对实例属性进行修改呢,还要去调用方法进行修改,不是很麻烦吗?
确实,调用方法的方式更加麻烦了,但是使用方法进行修改,可以一定程度上保证数据的合理性,即我们可以在方法中实现一些检验数据合理性的代码,如果数据合理才会进行修改,这样就确保了不会因为写代码的时候手滑写成一些奇奇怪怪的数据,导致程序运行不正常的情况出现
而如果直接对实例属性进行修改,方法内部的检测机制是不会被执行的(函数代码只有在函数被调用的时候,才会执行),这样一来,风险就大大增加了
但是为了讲解方便,我还是采用直接在方法外部进行实例属性修改的方式

其实为了解决步骤麻烦的问题,后续又引入了property属性、描述符等

类方法

由于类方法具有类对象的引用,于是类方法一般用于实现类属性值的修改,原因与上面实例方法的类似

静态方法

由于静态方法既不会自动传入实例对象的引用也不会自动传入类对象的引用,于是静态方法一般用于完全不需要实例对象和类对象的功能实现上,比如仅仅打印一个菜单,那其功能实现中基本就不会使用到实例对象和类对象的引用,使用静态方法就可以实现了,无需使用类方法或者实例方法,杀鸡焉用牛刀

最后,对一个代码以画图讲解的形式来结束面向对象封装相关的内容

代码

class C:
    data_1 = 10
    def func_1(self):
        print(123)

ins = C()
ins.data_2 = 20
ins.func_2 = lambda x : x

内存布局图


文字解析
类对象C中具有两个变量,一个是类属性data_1,一个是实例方法func_1,这两个变量分别保存着整数对象10和函数对象的id

实例对象ins中具有三个变量
一个是默认就有的__class__,保存着实例对象ins对应的类对象C的id,即通过__class__,实例对象ins可以访问到类对象C的内存空间,从而可以实现实例对象访问位于类对象中的类属性和三种方法
一个是实例属性data_2,保存着整数对象20的id
最后一个是实例属性func_2,我们即使该实例属性保存的是一个函数对象的id,但是我们依旧不会将该实例属性称为"方法"

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值