zoukankan      html  css  js  c++  java
  • 类的内置方法及描述符

    这篇文章用很简单的例子把python类的内置方法串起来梳理,使得知识点之间具有很强关联性,便于理解。
    
    引入
    定义一个类并实例化
    
    class info(object):
        # python3中,新定义的类默认都是object的子类,所以如果只写 class info 没有指明它的父类,那么也是可以的,父类就是object。
        # 之所以有如此多内置方法可用,即使我们自己定义的类里根本看不到这些内容,是因为内置方法正是在其父类object中实现的。
        # 例如,内置方法__str__在我们类定义里找不到,这时候就往其父类object中找,于是该方法就能起到object中所定义的作用。
        # 如果内置方法被我们自己亲自定义了,那么显然,我们自己定义的内置方法取代了它默认的行为。
        message = 'student'
        # 可以在类里存放共享的数据,它可以直接访问:类名.message,也可以用对象去访问:对象.message。
        # 一般情况下如果没有重写过任何内置方法,对象.message会先寻找属性字典,再找有没有共享数据info.message
        # 注意,这个数据是一个变量。在类的定义体里可以显式地修改:info.message=...
        def __init__(self, name, age):  
        # 构造函数__init__是内置方法,以 m = info(...) 的方式实例化对象时自动执行。
        # 同时还有析构函数__del__,但是程序执行结束会自动删除对象,这样__del__便自动执行了。
        # 默认的__del__只是单纯删除对象,其他什么也不做。
            self.name = name  
            # self.name可以当做一个变量名来赋值。其中self指代对象自己,"."后面的名字是自己自定义的一个名字。
            # 这个"."后面的名字,作为key,存储在对象的内置字典__dict__中。字典的key是可hash的,当然,属性名就是一个字符串。
            # 本质上,这里做的事情是self.__dict__[name]=...
            self.age = age
        def get_name(self):
            # self.name能访问到属性的值,本质上做的事情是在self.__dict__字典中搜寻name这个key,返回它的value。
            return self.name
    student = info('poincare', 23)
    
    1.反射:setattr,getattr,delattr
    反射是用属性名字字符串的方式来访问对象的属性或操作对象的属性的一种手段。难道直接用student.name不可以吗?可以,但是现在我有一个特殊的需求,就是让用户自己输入属性的名字,来操作属性。
    
    def operater(student=student):
        attrname = input('请输入你要获取的student的属性的属性名称:')
        print(student.attrname)
    operater()
    
    报错。解释器根本不知道这里的student.attrname是什么东西。首先,student.attrname不是一个已定义过的变量,其次,student对象并没有一个属性的名字叫"attrname"。
    解决方法是,通过对象内置的字典__dict__来找:
    def operater(student=student):
        attrname = input('请输入你要获取的student的属性的属性名称:')
        print(student.__dict__[attrname])
    operater()
    
    确实,因为对象的属性一般存在__dict__中,所以直接调用__dict__做查、改、删操作屡试不爽。但是这样显得太粗暴了。python有反射函数可以完成这个任务:
    def operater(student=student):
        attrname = input('请输入你要获取的student的属性的属性名称:')
        print(getattr(student, attrname))
    operater()
    
    同理,可以使用反射函数setattr,getattr,delattr来替代对对象__dict__的直接操控。
    
    2.内置方法:__setattr__,__getattr__,__delattr__,__getattribute__
    有机会重写对属性的赋值、修改、查找、删除行为。
    
    不管在类的内部还是外部,涉及对属性的赋值或修改,不管是采取student.name='bla'的方式,还是采取反射setattr(student, 'name', 'bla')的方式,只要重写了__setattr__,程序将优先找到自己重写的__setattr__执行。
    怎么会有这种奇葩需求?当然有。最简单的例子就是实现功能:任何方式任何时刻student的name属性被改动,都要在屏幕上给出警告"student.name被修改!"
    class info(object):
        message = 'student'
        def __init__(self, name, age):
            self.name = name
            self.age = age
        def __setattr__(self, key, value):
            if key == 'name'print('student的name属性被修改!')
            self.key = value
        def get_name(self):
            return self.name
        
    看到这里,马上指出问题。因为前面的反射已经提过了,不可以直接self.key=value,这实际上是创建了一个名叫key的属性。key指代一个字符串对象。考虑其他方式:
    class info(object):
        message = 'student'
        def __init__(self, name, age):
            self.name = name
            self.age = age
        def __setattr__(self, key, value):
            if key == 'name':
                self.name = value  # 方式一
                # setattr(self, key, value)  # 方式二
                print('student的name属性被修改!')
            setattr(self, key, value)
        def get_name(self):
            return self.name
    
    实例化对象stundent的过程中,要把name属性进行设置。此时报错。原因是递归溢出。实际上,在实例化时,首先转到__init__,执行self.name=value,因为重写了__setattr__方法,所以转到__setattr__,又准备执行self.name=value,因为重写了__setattr__方法,又要执行self.name=value,无限递归,永不停止。
    方式二把self.name=value改成反射setattr(self,name,value),仍然报错,因为本质上都是在修改这个属性的值,都要触发__setattr__。
    于是居然陷入了自相矛盾的过程!我这个方法需要实现赋值,但是赋值又会触发这个方法...
    解决方案,就是暴力地修改__dict__中的键了。注意!要区分修改__dict__和修改__dict__中的键值的区别。作为一个字典,字典中键值的修改并不会影响到__dict__所指向的字典“容器”本身。
    class info(object):
        message = 'student'
        def __init__(self, name, age):
            self.name = name
            self.age = age
        def __setattr__(self, key, value):
            if key == 'name':
                print('student的name属性被修改!')
            self.__dict__(key) = value  # 直接通过修改__dict__字典的办法操纵对象的属性。__dict__本身也是对象的属性,但是这里修改的是__dict__的键,而不是__dict__,所以不会触发__setattr__。
        def get_name(self):
            return self.name
    
    还有一种好办法。之前曾提到过,如果自己没有实现__setattr__,解释器就会从父类中找__setattr__。因此完全可以在子类中引用父类的__setattr__来完成赋值:
    class info(object):
        message = 'student'
        def __init__(self, name, age):
            self.name = name
            self.age = age
        def __setattr__(self, key, value):
            if key == 'name':
                print('student的name属性被修改!')
            super().__setattr__(key,value)  # 简化的写法,实际上super()内带有默认参数,与如下同理。
            # super(type(self), self).__setattr__(key,value)  # 直接引用父类未经重写的__setattr__方法操纵对象的属性
            # super(info, self).__setattr__(key,value)  # 也可以显式地在super()内用类自己的名称。不建议。因为显式引用失去了通用性。直接type(self)就能表示self所属的类info
        def get_name(self):
            return self.name
    
    再来看__getattr__,任何对属性值的获取,当属性不存在时,就可能会触发这个方法。简单的场景:如果用户想要获取的属性名不存在,不要报错,而是打印信息,并返回None
    class info(object):
        message = 'student'
        def __init__(self, name, age):
            self.name = name
            self.age = age
        def __setattr__(self, key, value):
            if key == 'name':
                print('student的name属性被修改!')
            super().__setattr__(key,value)
        def __getattr__(self,item):
            print('该属性{}不存在!'.format(item))
            return None
        def get_name(self):
            return self.name
            
    再来看__getattribute__,任何对属性值的访问,无论属性是否存在,都要触发这个方法。实现这个方法后如果同时实现了__getattr__,在__getattribute__没报错或没有显式调用__setattr__的情况下,就不会执行__getattr__。
    之所以未亲自实现__getattribute__方法时,属性找不到时才会找__getattr__,就是因为解释器已经先调用__getattribute__找过一遍,报了属性未找到的错误,才转到__getattr__的。
    简单的场景:任何时刻用户获取name属性时要给出警告"有人访问了name属性!"
    class info(object):
        message = 'student'
        def __init__(self, name, age):
            self.name = name
            self.age = age
        def __setattr__(self, key, value):
            if key == 'name':
                print('student的name属性被修改!')
            super().__setattr__(key,value)
        def __getattr__(self,item):  # 显式调用或__getattribute__方法报错时才调用。在本程序中,未找到属性怎么办这个事情在__getattribute__中实现了,所以此处永远不会被触发。
            print('我是__getattr__,报警:属性{}不存在!'.format(item))
            return None
        def __getattribute__(self,item):  # 任何对属性值的访问,注意!包括__dict__,都会先触发这个方法。对__dict__中键值的访问,必须先访问__dict__,所以本质上还是访问了属性__dict__!
            if item not in super().__getattribute__('__dict__'):  # 利用父类未重写的__getattribute__来避免查找属性__dict__时发生无限递归。
                print('我是__getattribute__,报警:属性{}不存在!'.format(item))
                return None
            return super().__getattribute__(item)  # 利用父类未重写的__getattribute__来实现本身应该有的功能并避免无限递归:返回属性的值
        def get_name(self):
            return self.name
    
    介绍到这,对对象的属性的操纵的触发,已经有了“优先级”的概念。默认的查找: __dict__字典 → (找不到才)向上父类__dict__字典...
    对于内置方法们,如果实现了__getattribute__,那么一定先跳转到__getattribute__,按:__getattribute__  →  (报错才)__getattr__  →  (报错才)向上父类...  的顺序进行的。
    (注:原始的__getattribute__做的事:先从属性字典找,再从类共享数据(类也有个字典,存了数和函数)找,再看是否实现了__getattr__,有就跳转,没有就报错。)
    至于向上父类的顺序,遵循MRO线性表。MRO线性表的计算规则已经在本博客随笔有详细介绍和python算法实现。
    
    3.内置方法:__set__,__get__,__delete__
    有机会代理对属性的赋值、修改、查找、删除等行为。
    
    简单的例子:实现功能:当用户调用不同的student对象的age的时候,返回None,并报警。
    class des:
        count = dict()
        def __get__(self, instance, owner):
            des.count[instance] = des.count.get(instance,0)+1
            print('你这是第{}次在对象{}中找不到age属性了!停,不会再去__getattr__了'.format(des.count[instance, default=0],instance))
            return None
    class info:
        age = des()  # age成为了共享属性,因为构造函数没给出self.age
        def __init__(self,name):  # 可以看出,构造函数根本没有实现age属性self.age
            self.name = name
        def __setattr__(self,key,value):
            super().__setattr__(key,value)
        def __getattr__(self,item):  # 实在找不到就只返回个None
            return None
        def __getattribute__(self,item):
            return super().__getattribute__(item)  # 其实重写等于没有重写。因为完全借鉴了原始方法
    student1,student2=info('student1'),info('student2')
    student1.age # 你这是第一次在student1中找不到属性age!
    student1.age # 你这是第二次在student1中找不到属性age!
    student2.age # 你这是第一次在student2中找不到属性age!
    
    以上程序可以用查找顺序很容易理解:
    首先,必然找__getattribute__,无论是否重写了,都要尝试找它。对于默认的__getattribute__,先在对象的属性字典找,没找到age。再在类定义中找共享属性,找到了age。发现age是一个被des类实例化过的对象,本来应该返回这个对象,但是注意内置方法__get__代理了此行为!
    
    再看__set__。简单的例子:实现功能:当用户初始化时,或修改属性值时,如果value值类型不对,就要报警。
    class des:
        def __set__(self,instance,value):
            if isinstance(value,str):
                print('类型正确!')
                self.__dict__[instance]=value
            else:
                print('类型错误!没能成功设置值!')
    class info:
        name = des()
        def __init__(self,name):
            self.name=name
        def __setattr__(self,key,value):
            super().__setattr__(key,value)
        def __getattr__(self,item):
            return None
        def __getattribute__(self,item):
            return super().__getattribute__(item)
    student = info('aaa')  # 类型正确!
    student.name = 3  # 类型错误!没能成功设置值!
    
    以上程序的执行顺序是:
    首先,构造函数执行到self.name=name这步,必然找__setattr__,无论是否重写了。然后,找属性name准备修改。但是发现此时并不能在对象的属性字典中找到name。再在类的定义中找共享属性,找到了name。name是一个被des类实例化过的对象,本来应该把这个对象直接改成name,但是注意内置方法__set__代理了此行为!
    在__set__中,将student对象和name值,传入函数,然后把name值存到了类共享属性name对象的属性字典中,key值为student对象。student对象确实可以当做key值,因为它是可hash的。
    为什么这么做?因为下次实例另一个学生:student_2 = info('aaa')的时候,这个类共享属性name对象的属性字典中key值就是student_2对象。因此虽然name是它们共有的属性,但是name这个对象的属性字典里,已经把每个student全部区分开了,不同的student,对应不同的name值!
    
    接下来,要访问student的name值:
    print(student.name)
    
    显然不行!这样打印出来的,其实是info类里的共享属性:对象name。联想到上面实现__set__时,是将name值存在了对象name的字典里。因此,正确的访问方式是:
    print(student.name.__dict__[student])
    
    这也太麻烦了!这时,联想到__get__方法:__get__方法有机会代理__attribute__属性查找中找到类共享属性后的部分行为。所以,应当把__get__和__set__一起用,放在des类的定义里面:
    class des:
        def __get__(self,instance,owner):
            return self.__dict__[instance]
        def __set__(self,instance,value):
            if isinstance(value,str):
                print('类型正确!')
                self.__dict__[instance]=value
            else:
                print('类型错误!没能成功设置值!')
    class info:
        name = des()
        def __init__(self,name):
            self.name=name
        def __setattr__(self,key,value):
            super().__setattr__(key,value)
        def __getattr__(self,item):
            return None
        def __getattribute__(self,item):
            return super().__getattribute__(item)
    student = info('aaa')  # 类型正确!
    student.name = 3  # 类型错误!没能成功设置值!
    print(student.name)  # 打印出了aaa
    
    这就是很简单的一种理解描述符作用机制的方式。所谓描述符,就是实现了__get__或__set__或__delete__的类。描述符一定要在另一个类的共享属性中实例化,这样就可以作用于另一个类的对象的属性操作。
    只实现__get__的,是非数据描述符,当属性字典查无时,会跳转到该非数据描述符的__get__中。
    实现__set__或__delete__的,是数据描述符,但是一般至少同时实现__set__和__get__。这样做出的描述符,对值的修改或查询,都会触发。
    注意,如果存在删除属性的动作,记得实现__delete__,否则它会找不到属性删。因为属性并非在__dict__里。 数据描述符是把属性值存到另一个类中,而不是属性字典中,所以,直接使用__dict__来查看对象的属性和属性值会出问题,因为属性字典的查找先于__get__。对吗? 实际上,对于数据描述符,一但实现,属性查找的优先级就被悄悄的提升了。原来__get__是在字典里找不到才跳转到它,现在是先于字典查找就要跳转到它了! 优先级:类属性(这里应指直接以类名.属性名来操纵的属性,例如info.des
    =None就直接把描述符删了。) > 数据描述符 > 实例属性 > 非数据描述符 > __getattr__ 使用描述符的优势:可以观察到,描述符定义清晰,可重用,“只做一件事”,甚至还有机会把构造函数引入描述符中,从而实现更深层次定制。所以,描述符已经被大量使用在很多场景。 Learning about descriptors not only provides access to a larger toolset, it creates a deeper understanding of how Python works and an appreciation for the elegance of its design. 使用描述符的劣势:直接调用对象的__dict__,发现并不存在name属性,与常见范式不是很统一。当然,可以利用描述符内定制性极强这一优势解决:在__set__中增加一行代码: instance.__dict__['name'] = value # 保持使用描述符后,__dict__中属性的统一性。当然,__get__并不会去查找它。 杀器祭出: 定制化类型限制: class Type: def __init__(self, attrname, typename): self.attrname = attrname self.typename = typename def __set__(self, instance, value): if not isinstance(value, self.typename): raise TypeError( '{}必须为{}类型,而接收到的{}却是{}类型。'.format( self.attrname, self.typename, value, type(value))) else: instance.__dict__[self.attrname] = value print('赋值属性{}值为{}'.format(self.attrname, value)) def __get__(self, instance, cls): if instance is None: return self else: return instance.__dict__[self.attrname] def __delete__(self, instance): del instance.__dict__[self.attrname] def typeassert(**kwargs): def decorator(cls): def wrapper(*kw_): for attrname, typename in kwargs.items(): setattr(cls, attrname, Type(attrname, typename)) return cls(*kw_) return wrapper return decorator @typeassert(name=str, age=int, salary=float) class Info: def __init__(self, name, age, salary): self.name = name self.age = age self.salary = salary def __str__(self): return 'name:{},age:{},salary:{}'.format( self.name, self.age, self.salary) dai = Info('poincare', 23, 1000.0) 描述符实现绑定到类的方法: class Classmethod: # 类装饰器写法 def __init__(self, funcname): self.funcname = funcname def __get__(self, instance, cls): def wrappers(*kw, **kwargs): k = self.funcname(cls, *kw, **kwargs) return k return wrappers class Info: info = [1, 2, 3] def __init__(self, name): self.name = name @Classmethod def sayinfo(self, alladd): self.info = [k + alladd for k in self.info] return self.info p = Info('poin') print(p.sayinfo(1)) print(Info.sayinfo(1)) 描述符实现静态方法: class Staticmethod: def __init__(self, funcname): self.funcname = funcname def __get__(self, instance, cls): def wrappers(*kw, **kwargs): k = self.funcname(*kw, **kwargs) return k return wrappers class Info: info = [1, 2, 3] def __init__(self, name): self.name = name @Staticmethod def add(x, y): print(x + y) return x + y p = Info('dai') # 实例化 # p.add(1) # 报错,缺少一个位置参数 p.add(1, 2) # 可以返回结果 # Info.add(1) # 报错,缺少一个位置参数 Info.add(1, 2) # 可以返回结果 实际上,实例.add 和 类.add 是同一函数,有完全一样的地址。 print(p.add) print(Info.add)
  • 相关阅读:
    web api 设置允许跨域,并设置预检请求时间
    T4模板
    DDD模式
    Vue watch用法
    第三章--第五节:集合
    简单的Python API爬虫与数据分析教程--目录
    第三章--第四节:字典
    第三章--第三节(补充):列表排序
    汇总张小龙在知乎上的问答
    第三章--第三节:列表
  • 原文地址:https://www.cnblogs.com/poincare/p/9091452.html
Copyright © 2011-2022 走看看