欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

python类属性访问魔法方法

程序员文章站 2022-07-15 22:05:46
...

python类属性访问魔法方法

本文主要讲述类(python3.6)属性访问的魔法方法: __get__, __getattr__, __getattribute__, ___set__, __setattr__
(本文对类属性和实例属性不加严谨的表述)

1. __getattr__, __setattr__

先定义一个类:

class Person(object):

    name = ''
    
    def __getattr__(self, item):
    # 当获取这个属性不存在就会访问这个方法
        print('__getattr__ is called')
        # raise AttributeError('class Person don\' have the attr {0}'.format(item))
        return 'hello'


if __name__ == '__main__':
    p = Person()
    print(p.name) 
    p.name = 'Mike' 
    print(p.name)  
    print(p.age)  
"""
输出: 
(空)
Mike
__getattr__ is called
hello
"""

可以看出__getattr__只有在属性不存在的时候才会被访问,建议返回值3个种情况:

  • 不返回(None)
  • 返回某种值 但需要注意的是,千万不能返回可能不存在的属性,否则会造成无限递归, 如上面return self.age 。除此之外, p.age虽然输出了’hello’, 但实际上,p实例依然是没有age这个属性的
  • (建议)直接引发异常

接下来我们看看__setattr__, 继续使用上面的类

class Person(object):

    name = ''
    
    def __getattr__(self, item):
    # 当获取这个属性不存在就会访问这个方法
        print('__getattr__ is called')
        # raise AttributeError('class Person don\'t have the attr {0}'.format(item))
        return 'hello'
        
    def __setattr__(self, key, value):
        print('__setattr__ is called')
        value += str(int(time.time()))  # 时间戳
        # setattr(self, key, value)
        super(Person, self).__setattr__(key, value)


if __name__ == '__main__':
    p = Person()
    print(p.name)
    p.name = 'Mike'
    print(p.name)
"""
(空)
__setattr__ is called
Mike1542524409	
"""

看输出结果就很容易明白这个方法的作用,但给属性赋值时,会去调用这个方法,当自己实现了这个方法了要注意以下的点:

  • 必须去给属性赋值,可靠的方法是直接继承父类方法,否则像p.name='Mike’将会失去作用,p.name的值永远是空
  • 千万不要尝试自己去给属性赋值,如上面的注释的setattr(self, key, value),这样会导致无限嵌套调用
  • __setattr__的一个作用,可以限制给类实例动态增加属性,python允许给实例动态增加属性,如上属例子,我们尝试p.name2 = ‘Ben’,一样可以给赋值成功,我们可以通过这个方法进行限制:
def __setattr__(self, key, value):
	if not hasattr(self, key):
		raise AttributeError
    print('__setattr__ is called')
    value += str(int(time.time()))  # 时间戳
	# setattr(self, key, value)
	super(Person, self).__setattr__(key, value)

2. 、__getattribute__

class Person(object):

    name = ''
    def __getattr__(self, item):
        print('__getattr__ is called')
        raise AttributeError('Person don\'t have the attr {0}'.format(item))

    def __getattribute__(self, item):
        print('__getattribute__ is called')
        return super(Person, self).__getattribute__(item)


if __name__ == '__main__':
    p = Person()
    p.name = 'Mike'
    print(p.name)
    print(p.name2)
"""
#     print(p.name)
__getattribute__ is called
Mike
#    print(p.name2)
__getattribute__ is called
__getattr__ is called
...
AttributeError: Person don't have the attr name2
"""

可以看出__getattribute__无论属性存在与否都会被调用, 和之前都一样,要注意无限嵌套调用, 它的优先级在属性访问的时候级别是最高的。

3. __get__、__set__

把这个方法放到最后来说,是因为这个方法的行为十分特别,实现了__get__我们称为Descriptor,直接看例子:


class Person(object):

    name = ''

    def __get__(self, instance, typed):
        print('__get__ is called')
        return self.name


class Blog:
    per = Person()
    per.name = 'Mike'

if __name__ == '__main__':
    b = Blog()
    print(b.per)

"""
输出:
__get__ is called
Mike
"""

当一个类的属性赋予成了Descirptor(类),当这个类再去访问这个属性当时候,就会去访问这个Descriptor的__get__方法,如上面,当去访问b的per属性,会直接返回name的值。
__get__除了self还有必须两个参数:

  • instance: 表示调用Descirptor的类实例或None。如上面当实例访问就是b;如果是直接Blog.per这样访问,那么它的值为None
  • typed: 表示调用Descirptor的类,如上面,就是Blog,但直接使用类访问时

需要注意的时候,如上面,__get__方法直接返回了self.name的值,所以当你想这样去b.per.name是不可行的,因为b.per已经不是Person的实例了。如果需要这样访问,可以直接return self;

同时实现了__get__和__set__则称为Data Descriptor,如果只实现了__get__则称为Non-data Descriptor。

# 省略其他
	def __set__(self, instance, value):
        print('__set__ is called')
        self.name = value + '123'


if __name__ == '__main__':
    b = Blog()
    print(b.per)
    print('-------------')
    b.per = 'Ben'
    print(b.per)

"""
__get__ is called
Mike
-------------
__set__ is called
__get__ is called
Ben123
"""

__set__除了self还有2个必须参数:

  • instance: 表示调用Descirptor的类实例,如上面,就是b
  • value:要赋予的值,如上面的’Ben’

4. __get__、__set__案例

这两个方法再某些场合十分有用,下面是类Flask的Config配置时的一个简化代码,它的设计巧妙运用Descriptor

	
class ConfigAttr:

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

    def __get__(self, obj, typed=None):
        if obj is None:
            return 'None of app'
        return obj.config[self._name]

    def __set__(self, obj, value):
        obj.config[self._name] = value


class App:

    default_config = {
        'name': 'app',
        'date': '2018-11-15',
        'lang': 'zh',
    }
    name = ConfigAttr('name')
    date = ConfigAttr('date')

    def __init__(self, name=None):
        self.config = dict()
        self.config.update(self.default_config)
        if name is not None:
            self.config['name'] = name


if __name__ == '__main__':
    # --- 测试__get__
    app = App('hello')
    print(app.name)
    app2 = App()
    print(app2.name)
    print(App.name)
    
"""
输出
	hello
	app
	None of app
"""
  • 将App的name和date属性和Config的name和date绑定
  • 当访问或者操作app.name实际上是访问或者操作app.config[‘name’],与以下相比:
self. name = self.config['name']

上述代码,如果name只是一个只用于读(访问)的属性,那么与使用Descriptor区别不大,但如果name可以被改变,但执行app.name = ‘xxx’, 那么name属性和config将彻底失去关联。

5 . 总结

  • __getattr__在访问实例属性不存在的时候,会被调用

  • __setattr__当实例属性被赋值时会被调用

  • __getattribute__在访问任何存在与否的属性都会被调用
    谨慎使用这三个魔法方法, 使用不当会产生很多负面后果,如无限嵌套

  • 实现了__get__的类被称为Descriptior,通过其他类(或者实例)去访问这个类实例的时候会被调用

  • __set__, 与__get__一起使用,通过其他类(或者实例)去赋值这个类实例的时候会被调用

  • 通过类访问属性,通过A.attr访问属性的规则为:

    1. 如果MetaClass中有__getattribute__,则直接返回该__getattribute__的结果。
    2. 如果attr是个Descriptor,则直接返回Descriptor的__get__的结果。
    3. 如果attr是通过属性,则直接返回attr的值
    4. 如果类中没有attr,且MetaClass中定义了__getattr__,则调用MetaClass中的__getattr__
      如果类中没有attr,且MetaClass中没有定义__getattr__,则抛出异常AttributeError
  • 通过类设置属性, 通过A.attr=val给属性赋值时:

    1. 如果MetaClass中定义了__setattr__,则执行该__setattr__
    2. 如果该属性是Descriptor,且定义了__set__,则执行Descriptor的__set__
    3. 如果是普通属性或None-data Descriptor,则直接令attr=val
    4. 如果属性不存在,则动态给类添加该属性,然后进行赋值

6. 参考文档