Python 23 面向对象编程进阶 反射与加工标准类型(包装和授权)

Python 23 面向对象编程进阶 反射与加工标准类型(包装和授权)

旧博客Python系列

反射

反射的概念是由Smith在1982年首次提出的,主要是指程序在运行时可以访问、检测和修改它本身状态或行为的一种能力(自省)。这一概念的提出很快引发了计算机科学领域关于应用反射性的研究。它首先被程序语言的设计领域所采用,并在Lisp和面向对象方面取得了成绩。
反射简单一点说,就是程序可以在运行的时候对自己做操作.这和早期计算机使用固定的代码进行操作有着巨大的区别.
有四个可以实现反射的内置函数,都可以作用于类和对象:

hasattr(object,name)

class Data():
    '''这是一个测试用数据类型'''
    default_type = 'int'
    default_value = 42
    default_name = 'essense'

    def __init__(self, name, value, type):
        self.name = name
        self.value = value
        self.type = type

    def show_obj(self):
        print(""{}'s value is {}, type is {}."".format(self.name, self.value, self.type))


data1 = Data('cony',42,'int')

print(hasattr(data1,'name'))  # True
print(hasattr(data1,'show_obj')) #True

可见hasattr并不是检查属性字典而是检查这个对象能否调用到第二个参数的方法,如果没有返回False

getattr(object, name, default=None)

# 获得属性
print(getattr(data1,'name'))  # cony
print(getattr(data2,'show_obj')) # <bound method Data.show_obj of <__main__.Data object at 0x0000023CFFE3EC18>>

就是返回这个属性.如果没有这个属性,就会报错.可以设置default在找不到的时候返回default.
getattr的结果基本上就是和object.name的结果相同,但是可以多一个default参数.

setattr(x, y, v)

# 设置属性
setattr(data1,'age',4)
print(data1.__dict__)
# {'name': 'cony', 'value': 42, 'type': 'int', 'age': 4}

delattr(x, y)

# 删除属性
delattr(data1,'name')
print(data1.__dict__)
# {'value': 42, 'type': 'int'}

基本上相当于del object.name

为什么需要反射呢?
以setattr为例,可以动态的在程序运行的时候传入一个数据属性甚至是添加一个方法.(添加了方法你会发现,添加的方法并不是一个绑定方法,就相当于给对象添加了一个静态方法,是对象的工具库.)
但是可以把方法写的和绑定方法类似,要求传入自身.这就相当于在类或者基类没有给对象提供方法的时候,依然不影响对象功能的实现,而且在类或者基类的对应方法上线之后,直接删除setattr这行代码即可,只是在调用的时候可能发生略微改变.或者在编写对象的时候,可以动态获取一下运行起来的对象到底有没有相关方法,如果没有,可以执行其他逻辑,不影响继续编写代码.
如果没有反射,则必须等类或者基类添加了该方法,才能在该方法上继续编写代码.

动态加载模块

有了反射以后,就可以动态的加载模块了. 首先看 __import__(string)内置函数 如果有一个模块m1,下边有一个子文件/目录/模块t
from m1 import t 
# 如果拿到的是一个字符串,可以用__import__内置函数来导入这个名字对应的字符串
module = __import__('m1.t')
# 可以print(module)来看,然后发现返回的是m1模块
# 使用__import__导入的是最顶级的模块,和import不同,如果要调用t里边的方法,必须写成 module.t.method()

动态加载模块指的就是无需用import将导入的模块写死,而是可以维护一个用于程序的字符串列表,每个字符串是需要导入的模块,然后根据需要,用__import__来导入需要的部分,更多可以看深入python.
如果模块里的方法开头加上了单下划线,在from xxx import * 的时候,不会自动导入.但是可以指定名称导入,所以也没有真正的限制.

利用一个模块importlib也可以实现动态导入

import importlib
m = importlib.import_module('m1.t')
print(m)

发现这个时候返回的是m1.t模块,可以直接m.method(),所以使用importlib要比使用__import__方便.二者都可以实现动态加载模块.

对象内的反射方法:

getaddr
当取不到属性的时候,__getaddr__会被触发.

 class Data():
    '''这是一个测试用数据类型'''
    default_type = 'int'
    default_value = 42
    default_name = 'essense'

    def __init__(self, name, value, type):
        self.name = name
        self.value = value
        self.type = type

    def show_obj(self):
        print(""{}'s value is {}, type is {}."".format(self.name, self.value, self.type))

    def __getattr__(self, item):
        print(""__getattr__ is triggered"")
data1 = Data('cony',42,'int')
print(data1.defa321lue)
# __getattr__ is triggered
# None

可见当没有属性的时候,如果设置了__getattr__方法,不会报错,会触发__getattr__方法的执行

delattr
当从对象中删除一个属性的时候,会触发__delattr__方法.

class Data():
    '''这是一个测试用数据类型'''
    default_type = 'int'
    default_value = 42
    default_name = 'essense'

    def __init__(self, name, value, type):
        self.name = name
        self.value = value
        self.type = type

    def show_obj(self):
        print(""{}'s value is {}, type is {}."".format(self.name, self.value, self.type))

    def __getattr__(self, item):
        print(""__getattr__ is triggered"")

    def __delattr__(self, item):
        print(""__delattr__ is triggered"")

data1 = Data('cony',42,'int')

del data1.default_name
del data1.name
print(data1.name)

结果很有意思,两次del都成功触发了__delattr__方法.但是被删除的属性依然可以访问,说明__delattr__截获了删除属性并且用来触发自身,而不是删除属性.

setattr
在设置(修改或者新增)属性的时候会被触发,设置一个就触发一个.如果想在__setattr__里添加东西,不能写成self.attr的名字,最好采用self.__dict[key] = value的方式.

class Data():
    '''这是一个测试用数据类型'''
    default_type = 'int'
    default_value = 42
    default_name = 'essense'

    def __init__(self, name, value, type):
        self.name = name
        self.value = value
        self.type = type

    def show_obj(self):
        print(""{}'s value is {}, type is {}."".format(self.name, self.value, self.type))

    def __getattr__(self, item):
        print(""__getattr__ is triggered"")

    def __delattr__(self, item):
        print(""__delattr__ is triggered"")

    def __setattr__(self, key, value):
        print('__setattr__ is triggered')
        self.__dict__[key] = value

data1 = Data('cony',42,'int')
print(data1.__dict__)
# {'name': 'cony', 'value': 42, 'type': 'int'}

注意,__setattr__也会截获操作,如果__setattr__里什么都不做,或者写了别的东西,没有用默认的key和value参数,结果会变成如下:

class Data():
    '''这是一个测试用数据类型'''
    default_type = 'int'
    default_value = 42
    default_name = 'essense'

    def __init__(self, name, value, type):
        self.name = name
        self.valu e = value
        self.type = type

    def show_obj(self):
        print(""{}'s value is {}, type is {}."".format(self.name, self.value, self.type))

    def __setattr__(self, key, value):
        print('__setattr__ is triggered')
        self.__dict__['mom'] = 'jenny'

data1 = Data('cony',42,'int')
print(data1.__dict__)  # {'mom': 'jenny'}

结果会发现只添加了self.dict里的那一对键值,其他的都被__setattr__方法所拦截.
有了__getattr__, delattr, 和 __setattr__之后,有三种操作都会被拦截,这样就可以通过在这三个方法里加上各种逻辑,更好的控制对象的行为.只要记住三个方法里如果要操作属性,一定要通过属性字典的操作,而不要直接操作属性,否则会再次触发形成死循环.

加工标准类型(包装)

面向对象概念提出之后,python提供的各种标准数据类型和其内置方法,都是对象及其方法的应用.如果很多方法满足不了我们的要求,可以在继承标准类型的基础上,通过重写子类的内置方法,让子类的功能改变或者扩展,就相当于在原始的方法上进行加工.术语也就是对原数据类型进行了包装.
# 自定义列表类型
class List(list):
    pass

l1 = List('hello world!')
print(l1,type(l1))
# ['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'] <class '__main__.List'>

可见类型已经变成了List类,而不是原来的list类,不过这个类所有的属性全都继承字标准数据类型list

# 加工列表的方法,想让List类可以返回中间的值,新增元素只能是字符串类型.
class List(list):

    def show_middle(self):
        mid_index = int(len(self)/2)
        return self[mid_index]

    def append(self,p_object):
        if not isinstance(p_object,str):
            print('Support only append string type')
        else:
            super().append(p_object)    


l1 = List('hello world!')
print(l1.show_middle()) # w

l1.append(3) # Support only append string type

注意自己加工的append方法里,如果判断要添加,不能直接调用自己的append方法,否则会循环,由于是加工而不是重新实现,一般是采用父类的append方法.

加工标准类型(包装)

授权是包装的一个特性,包装广义上是指对已经存在的类型的一些定制.授权是指在不继承的情况下,所有新增功能都是由新类的某部分来处理,已存在的功能就授权给默认对象的属性.授权的关键是修改__getattr__属性.
class Open:

    def __init__(self, name, mode='r', encoding='utf-8'):
        self.filename = name
        self.mode = mode
        self.encoding = encoding
        self.file = open(name,mode,encoding=encoding)

    def __getattr__(self, item):
        return getattr(self.file,item)

f1 = Open('new.txt')

这里文件的读并没有继承文件对象的类,而是新建了一个类.
在self.file里边封入了内置函数open的所有操作.对于不打算重写的方法,由于没有继承,类里也没有该方法,会报错.但是self.file里边有,因此只要重写__getattr__,让类里没有的方法全部从self.file里边去找.新增功能只要自己写就可以了.比如新增一个写入时间加内容的方法:

import time
class Open:

    def __init__(self, name, mode='r', encoding='utf-8'):
        self.filename = name
        self.mode = mode
        self.encoding = encoding
        self.file = open(name,mode,encoding=encoding)

    def __getattr__(self, item):
        return getattr(self.file,item)

    def write_with_time(self,line):
        t = time.strftime('%Y %m %d %X')
        self.file.write('{} {}\n'.format(t,line))

上述都是基于反射的应用,也可以应用在模块上,内置类型上等等,由于一切皆对象,所以都可以.
模块里也能检测自己,可以采取

# 取当前模块的名称
import sys
current_module_name = sys.moudules[__name__]

内置函数的补充:
isinstance(obj,cls)
检查一个对象是否是一个类的实例

issubclass(sub,super)
检查sub类是否是super类的派生类,两个参数都是类名

__getattr__的补充

除了__getattr__方法以外,类里边还有一个__getattribute__方法. 这两个有什么区别呢.
class Foo:
    def __init__(self,x):
        self.x = x

    def __getattr__(self, item):
        print('__getattr__ is triggered')

    def __getattribute__(self, item):
        print('__getattribute__ is triggered')

f1 = Foo(42)

f1.x   # __getattribute__ is triggered
f1.xx  # __getattribute__ is triggered

可以发现不管找不得到属性,都会触发__getattribute__,说明__getattribute__的优先级比__getattr__要高.但是如果不自定义,程序只会在找不到的时候触发__getattr__方法.说明在__getattribute__内部有某种机制,如果找到就交给属性处理,如果找不到,就交给__getattr__处理.内部机理是:寻找属性首先会触发__getattribute__,如果找到,交给属性执行,如果找不到,会抛出AttributeError错误.这个错误会被__getattr__捕捉到,就会触发__getattr__的运行.内置的__getattr__是会对这个错误不加处理原样抛出,而自定义的不做处理就不会继续抛错误.

LICENSED UNDER CC BY-NC-SA 4.0
Comment