深入理解面向对象-->内置方法与class关键字实现机制

118 阅读11分钟

注:本文以Python语言为例

反射

如果要更深入的一些理解面向对象,那么理解''一切皆对象''这句话可以帮助你事半功倍而更好的使用这种思想的优点:''一切皆对象'',顾名思义就是你在编程中所看到的任何一个变量、数据结构、函数、类,都可以看成一个对象。当然,光这么说是凭空无证的,本文后续会以对象产生的机制来论证,在这之前,我们需要先理解''反射''的概念

反射是相较于c++、java、go等静态语言,python、javascript等脚本(动态)语言所独有的一种机制。
对于反射的应用:通过字符串来操作对象的属性或者方法

静态语言与动态语言

动态语言即在运行前,程序不会去纠结一个变量的数据类型,而是在运行到这个变量时才会得知:

比如在Python中:

a = 10             # 程序在运行时才会确定它的类型是int
print(a)

静态语言则往往需要在创建一个变量时,就要声明它的数据类型,程序在编译时即明确了变量的数据类型:

比如在c语言中:

int a = 10
printf(''%d'',''a'')

反射概念

反射机制指的是在程序的运行状态中:
1.对于任意一个类,都可以知道这个类的所有属性与方法;
2.对于任意一个对象,都能够调用它的任意方法和属性
这种动态获取程序信息以及动态调用对象的功能称为反射机制

对于①,很好理解,如果我们自定义了一个类,或许你想查看任何一个常用类的方法与属性可以通过:

print(list.__dict__)               # 类名.__dict__ ,即类的字典,查看该类下所有的方法与属性
# 也可以在类体外部通过实例.__dict__["newattribute"] = value来增加实例属性

对于②,我们在享受面向对象编程思想为我们在一些场景带来的便利的时候,肯定印象深刻,当你实例化了一个类,创造了对象,直接对象.方法/属性 就可以使用到类中的方法或查看对象的属性,太方便了!

反射机制使得程序有了可以访问、检测和修改它本身状态或行为的这种能力,这也再次体现了面向对象编程的代码去冗余性、以及可扩展性和易维护性的优点所在。

动态语言由于拥有这种方便快捷的反射机制,因此开发效率一般都比较高~

内置方法

字符串调用方法 --> 四种方法

Python中有四个类的函数,其用途是可供开发者实现通过字符串来操作属性值的功能

hasattr()函数

用于判断一个对象是否拥有某个属性,有着返回True,无返回False

print(hasatter(obj, 'name'))    # 判断对象中有没有name这个属性

getattr()函数

用于调用获取对象的一个属性或方法

print(getattr(obj, 'name', None))    # 相当于obj.name,调用某一个属性,
# None所在的参数位是默认值,即如果没有目标属性就返回默认值

setattr()函数

对于对象的某个的属性做赋值操作

print(setattr(obj, 'name', 'xiaoming'))    # 相当于obj.name = 'xiaoming'

delattr()函数

对于对象的某个属性做删除操作

print(delattr(obj, 'name'))   # 相当于del obj.name

当我们掌握了上述的四个函数,我们就可以以另外一种方式去调用类中的方法或属性了,即''输入什么调用什么''

比如:

class Ftp:

    def put(self):
        print('正在上传...')

    def get(self):
        print('正在下载...')

    def interactive(self):
        method = input('请输入你要做的事: ').strip()

        if hasattr(self, method):     # 判断输入的method存不存在
            getattr(self, method)()   # 存在就用getatter调用

        else:
            print('输入指令不存在')
obj = Ftp()
obj.interactive()             # 完成了交互式的调用方法

内置方法概念

凡是定义在类的内部,以__开头并以__结尾的方法,都为类的内置方法。

它的名字反映了它的特点:即在这一类方法是本身就有的,会在一些条件下自动触发,不需要代码明文调用。

比如上述的内置方法,当你调用getattr(),时python会自动触发### __getattribute__方法,调用setattr(),会自动触发__dict__方法,以修改类的字典的键值,调用delattr()方法时,会自动调用__delattr__方法...

即使你目前还不了解什么是内置方法,但其实在Python这门一切皆对象的语言中,内置方法已经被我们调用无数次了,比较典型的几个例子是:

经典:当你使用了len()这个函数,len(一个字符串's')  python就自动调用了内置方法:s.__len__
巨经典:当你在与用户做着友好的交互:使用了print()这个函数,print(一个字符串's')  
python就自动调用了内置方法:s.__str__

__str__方法

class People:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):             # 打印对象时就会自动运行
        return f'{self.name}:{self.age}'   # 返回值必须是字符串

obj = People('xiaoming', '18')
print(obj)
输出结果:会有__str__方法返回的字符串

即如果你在一个类中写定了__str__方法,当你print这个类的实例化对象后,它会自动触发,直接打印你__str__方法中返回的字符串。

__del__方法

class People:

    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.x = open('a.txt', mode='w')
        # self.x = 占用的是操作系统资源

    def __del__(self):            
        print('正在删除...')
        # 发起系统调用,会告诉操作系统回收相关的系统的资源
        self.x.close()

obj = People('xiaoming', '18')
# obj.x 占据的是操作系统资源,因为该属性对应的操作是文件操作,会设置到占用操作系统的资源
del obj
print('===========')            # 程序结束之后会自动清理对象,所以如果结束之前没有清理,就会在结束程序在释放内存时清理掉

注意上面这段代码,程序在结束时会回收内存、清理对象,会自动触发内置方法__del__,但如果在程序中已经有了清理内存的操作,则不会自动触发,因此大家可以试一试,删除del obj这一行代码再运行一下,将print的内容作为一个标志去帮助理解释放内存的过程。

class关键字实现机制

元类

元类的概念是很好理解的,在''一切皆对象的''的淫威下,当你把用类去套入了你之前所认知的对象的概念,那么元类则对应的是你所之前所理解的类的概念,即''类的类'',元即''开元'',指某一个对象在最开始被实例化的类。

Python中的内置元类是''type'',我们用class关键字所定义的任一个类以及内置的类(比如list),其元类都是type,你可以拿你所创建的任何一个类试验一下:

class People:

    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.x = open('a.txt', mode='w')
        # self.x = 占用的是操作系统资源

    def __del__(self):            
        print('正在删除...')
        self.x.close()
        
print(type(People))             # 试验方法
输出:<class 'type'>

即People是Tpye类的实例化 ----> People = type(...) 理解元类的作用可以从你之前理解的类对于对象的作用去切入,元类就是用来产生其实例化的类的类(而类可以再经过其的实例化得到对象)

通过class关键字创造的类是如何来的?

class创建一个类的过程有点类似于大学生入校的过程:
1.拿到类名(收集你的姓名信息)----> class_name = 'People'
2.拿到基类(收集你的父母信息)----> class_bases = (object,)
3.拿到类体(收集你的基本信息,包括身高、体重(属性),你的技能、习惯(方法))、创建类的字典 class_dic = {} ---->
class_body =
'''def init(self, name, age):
self.name = name
self.age = age
def say(self): print(f'{self.name},{self.age}')
....'''
class_dic = {'init': ....,'say': .....}
4.调用元类(你的信息被封装到你的学校中,你从此成为了你的学校的一个实例化对象...)----> People = type(class_name, class_bases, class_dic)

以上的过程,也同样是使用class关键字去创造类时所经历的四件事~

如何自定义元类来产生一个类?

既然我们知道了一个类在创建时实例化内置元类type的实现方法,那如果我们自己写一个元类,是不是就可以产生实例化的是自定义的类的类了?也就是说,你再去print(type(类名))时,输出的结果就是<class '自定义的元类名'> 了?
当然可以!虽然这在开发中不常用,因为type中的内置方法太丰富了,所以在开发中很少有人会再花很多时间去重写一个能和type一样创建类的机制十分完善的元类,不过我们这么做对我们理解class机制是很有帮助的。

在这之前,我们需要再回顾一下实例化一个类、造出一个对象时发生的过程:

1.先造了一个空对象 ----> 对象调用了类内的__new__方法;
2.初始化对象 ----> 对象调用了类内的__init__方法,完成对象初始化操作;
3.返回初始化好的对象

这代表着,我们自己写的元类中,至少要有__new__方法与__init__方法,注意这里我们实例化的是一个类,得到的对象也是一个类,调用的__init__是被实例化的类的,就像你之前的理解中得到的对象,在实例化一个类的时候会自动调用它的__init__方法。

是时候写一个元类了!

class Mymeta(type):           # 只有继承了type类的类才是元类,type内丰富的内置函数可以直接用了
    # 传了四个参数:空对象、类名、类的基类名、类的字典,除了第一个是自动传入,其余三个即反映了class创建一个
    # 类的过程
    def __init__(self, class_name, class_bases, class_dic):
        print('正在执行...')
        if not class_name.istitle():      
        # 自己定制的元类可以规定其对象的创造规则,比如要求首字母必须大写,否则报错
            raise NameError('类名的首字母必须大写')

    def __new__(cls, *args, **kwargs):              # 接收调用类时所传入的参数
        # 造Mymete的空对象
        print('先于__init__执行....')
        print(cls, args, kwargs)
        return super().__new__(cls, *args, **kwargs)  # 这里直接使用type的__new__方法
       
    def __call__(self, *args, **kwargs):
       people_obj = self.__new__(self)
       self.__init__(people_obj, *args, **kwargs)
       return people_obj

        

实例化它!

class People(metaclass=Mymeta):           # 给metaclass赋值来规定该类的元类,默认是type,即内置元类

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def say(self):      
        print(f'{self.name},{self.age}')

    def __new__(cls, *args, **kwargs):
        return object.__new__(cls)     # 该类没有继承任何类则默认继承经典类object,调用了其中的__new__方法来造People的对象

再去看type(People)的返回值,你会发现,即使没有使用class关键字默认的元类type,你依然造出了一个可以实例化的类!

实例化所必要的过程:__call__方法

直接上代码

class Foo:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __call__(self):
        print('哈!我被调用了!')

obj = Foo(111, 222)
obj()

call__方法的应用很直接:如果想让一个对象可以加括号来调用,需要在该对象的类中添加一个方法__call

总结:
如果一个对象可以调用,一定触发类内的__call__
调用一个类,一定触发自定义元类的__call__方法
自定义元类 ---> 内置元类的__call__方法

应用到我们上文造的元类及其实例化的类:

1、Mymeta.__call__先调用People内的__new__方法
2、Mymeta.__call__先调用People内的__init__方法,完成初始化对象的操作
3、Mymeta.__call__返回初始化好的对象

当你执行:

print(type(People))
obj = People('xiaoming', '18')
obj.__dict__

输出结果为: 先于__init__执行....
<class 'main.Mymeta'> ('People', (), {'module': 'main', 'qualname': 'People', 'init': <function People.init at 0x00000206EB41D3F0>, 'say': <function People.say at 0x00000206EB41D480>, 'new': <function People.new at 0x00000206EB41D510>}) {}
正在执行...
(<class 'object'>,)
<class 'main.Mymeta'>

宣告成功定义了元类!

属性查找的原则

class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
    n=444

    def __call__(self, *args, **kwargs): #self=<class '__main__.StanfordTeacher'>
        obj=self.__new__(self)
        self.__init__(obj,*args,**kwargs)
        return obj

class Bar(object):
    n=333

class Foo(Bar):
    n=222

class StanfordTeacher(Foo,metaclass=Mymeta):
    n=111

    school='Stanford'

    def __init__(self,name,age):
        self.name=name
        self.age=age

    def say(self):
        print('%s says welcome to the Stanford to learn Python' %self.name)


print(StanfordTeacher.n)
#自下而上依次注释各个类中的n=xxx,然后重新运行程序,发现n的查找顺序为StanfordTeacher->Foo->Bar->object->Mymeta->type

属性查找应该分成两层,一层是对象层(基于c3算法的MRO)的查找,另外一个层则是类层(即元类层)的查找。

image.png

总结:属性查找的原则:对象 --> 类 --> 父类 --->(没找到) ---> 元类

以上内容若有不正之处,恳请您不吝指正!