• guxh的python笔记六:类的属性


    1,私有属性

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

    类的属性在实例化之后是可以更改的:

    f = Foo(1)
    print(f.x)   # 1
    f.x = 2
    print(f.x)   # 2

    如果想禁止访问属性,即让属性私有,可以用“双下划线” 或者“单下划线”:

    class Foo:
    
        def __init__(self, x, y):
            self._x = x
            self.__y = y
    
        def __repr__(self):
            return 'f._x = {}, f.__y = {}'.format(self._x, self.__y)

    区别是“双下划线”解释器会对属性就行“名称更改”,而“单下划线”不会有更改,只是约定成俗。

    但是知道规则还是可以轻松访问:

    f = Foo(1, 1)
    f._x = 2
    f._Foo__y = 2
    print(f)   # f._x = 2, f.__y = 2

    所以如果只是为了属性私有,用“单下划线”和“双下划线”没有什么区别。

    但“双下划线”在继承时可以避免基类属性被子类的属性覆盖,即想实现对子类的隐藏时,可以用双下划线。

    2,只读属性

    为类的方法添加@property,但不实现该属性对应property类的setter方法,就可以让该方法变成只读属性。

    2.1,让实例化时的属性变成只读(私有属性 + @property)

    私有属性 + property可以让属性变的只读:

    class Foo:
    
        def __init__(self, x):
            self._x = x
        
        @property
        def x(self):
            return self._x
    

    此时对Foo进行实例化后,可以访问对象的属性x,但无法给x赋值:

    f = Foo(1)
    print(f.x)  # 1
    f.x = 2   # AttributeError: can't set attribute

    因为实例化时的x存在私有属性self._x中,当然可以用1中提到的访问私有属性的方法去修改,但这被认为是粗略的行为:

    f = Foo(1)
    f._x= 2
    print(f.x)  # 2 

    如果__init__函数还是用x属性赋值,会有什么后果呢?

    class Foo:
        def __init__(self, x):
            self.x = x
    
        @property
        def x(self):
            return 123
    
    f = Foo(1)   # AttributeError: can't set attribute

    self.x会无法完成赋值,因为没有对应的x.setter方法。

    2.2,把函数变属性 (@property)

    把函数变属性,可以实现根据需要完成计算,从而无需在实例化时就完成计算。

    class Circle:
        def __init__(self, radius):
            self.radius = radius
    
        @property
        def perimeter(self): 
            print('computing')    # 每次调用都会打印
            return 2 * math.pi * self.radius
    
    c = Circle(10)
    print(c.perimeter)   # ‘computing’  , 62.83185307179586
    print(c.perimeter)   # ‘computing’  , 62.83185307179586
    

    但存在问题是如果多次调用,就会多次计算。

    如果在__init__函数中增加一条self.p = self.perimeter,然后访问实例的p属性,就不会多次计算,但这样又变成实例化时就完成了计算。

    标准的做法是利用描述符将计算结果缓存起来:

    class lazyproperty:
        def __init__(self, func):
            self.func = func
    
        def __get__(self, instance, cls):
            if instance is None:
                return self
            else:
                value = self.func(instance)   # 计算被装饰函数的运算结果
                setattr(instance, self.func.__name__, value)  # 将运算结果缓存,保存为func的name
                return value  # 返回运算结果
    
    class Circle:
        def __init__(self, radius):
            self.radius = radius
    
        @lazyproperty
        def perimeter(self):
            print('computing')    # 每次调用都会打印
            return 2 * math.pi * self.radius
    
    c = Circle(10)
    print(c.perimeter)   # ‘computing’  , 62.83185307179586
    print(c.perimeter)   # 62.83185307179586
    

    3,属性管理

    3.1,属性管理 :init

    可以通过__init__完成属性管理:

    class Foo:
    
        def __init__(self, x):
            if x < 0:
                raise ValueError('x must be >= 0')
            self.x = x

    看看有什么效果?

    f = Foo(-1)  # ValueError: x must be >= 0
    f = Foo(1)  
    print(f.x)  # 1
    f.x = -1
    print(f.x)  # -1

    发现实例化时能对属性进行管理,但是实例化后的对属性的操作就无法管理了。

    3.2,属性管理:property

    对属性的操作有:get,set,del。

    property只是完成了get操作,剩下的get / del分配由setter / delettr完成,组合起来就可以实现对属性的管理:

    class Foo:
    
        def __init__(self, x):
            self.x = x
    
        @property
        def x(self):
            return self._x
    
        @x.setter
        def x(self, val):
            if val < 0:
                raise ValueError('x must be >= 0')
            self._x = val
    
        @x.deleter
        def x(self):
            del self.__dict__['_x']

    再看看对x属性的访问情况:

    f = Foo(1)
    print(f.x)      # 1
    f.x = 2
    print(f.x)      # 2
    f = Foo(-1)   # ValueError: x must be >= 0
    f.x = -1        # ValueError: x must be >= 0
    del f.x
    print(f.x)      # AttributeError: 'Foo' object has no attribute '_x'

    如果__init__中使用self._x = x会出现什么后果?

    class Foo:
    
        def __init__(self, x):
            self._x = x
        ......
    
    f = Foo(-1)
    print(c.x)   #  -1
    f.x = -1   # ValueError: x must be >= 0
    

    self.x = x在实例化时会触发set,如果没有set就无法完成赋值(参考2.只读属性中的例子)。

    而self._x = x在实例化时绕过了set,也就是不会对实例化时的参数做检查。

    另外,del可以不定义,del f.x会触发AttributeError: can't delete attribute,这里pass演示,del f.x没什么效果。

    3.3,属性管理:property工厂函数

    虽然内置的property经常用作装饰器,实际上是个类,python中函数和类可以互换,因为都是可调用对象。

    property类完整的构造方法:property(fget=None, fset=None, fdel=None, doc=None)

    下面不用property装饰器,而用经典形式property:

    class Foo:
    
        def __init__(self, x):
            self.x = x
    
        def qty_get(self):
            return self._x    # 如果return self.x,变无限递归,因为self.x就是从qty_get拿值的
    
        def qty_set(self, val):
            if val >= 0:
                self._x = val
            else:
                raise ValueError('value must be >= 0')
    
        x = property(qty_get, qty_set)   # 经典形式property

     属性很多时,通过@property, setter代码会就大量增加,可以利用property工厂函数实现管理:

    def quantity(attr):
    
        def qty_get(instance):
            return instance.__dict__[attr]
    
        def qty_set(instance, val):
            if val >= 0:
                instance.__dict__[attr] = val
            else:
                raise ValueError('value must be >= 0')
    
        return property(qty_get, qty_set)
    
    
    class Foo:
        x = quantity('x')
    
        def __init__(self, x):
            self.x = x
    

    可以接受参数,并且使用了getattr和setattr(避免使用__dict__)的工厂函数:

    def typed_property(name, expected_type):
        storage_name = '_' + name   # 这里如果不换个名字,后面就不能用getattr和setattr,只能用__dict__,否则无限递归
    
        def prop_get(instance):
            return getattr(instance, storage_name)
    
        def prop_set(instance, value):
            if not isinstance(value, expected_type):
                raise TypeError('{} must be a {}'.format(name, expected_type))
            setattr(instance, storage_name, value)
    
        return property(prop_get, prop_set)
    
    class Person:
        name = typed_property('name', str)
        def __init__(self, name):
            self.name = name
    
    p = Person(123)
    

    3.4,属性管理:描述符

    描述符是个实现了特定协议的类,即实现了一些特殊方法的类,这些特殊方法包括__get__,__set__,__delete__。

    之前使用的property类实现了完整的描述符协议。这里自定义描述符相当于抛弃了property,自己去定义实现一个类似property的类。

    可以只实现一部分协议,大多数描述符只实现了__get__和__set__。

    设置属性可以通过setattr(instance, name, value)或者instance.__dict__[name] = value,但需要注意有时用setattr会无限递归,有时些场景不适用dict,例如使用了__slots__或者描述符的类。

    版本一:描述符声明时需要传入名称(不建议用)

    class Quantity:
    
        def __init__(self, name):
            self.name = name
    
        def __set__(self, instance, value):
            if value >= 0:
                instance.__dict__[self.name] = value # self是描述符Quantity的实例,instance是Foo的实例,所以需要对instance设置
                # setattr(instance, self.name, value)  # 这里如果使用setattr会无限递归
            else:
                raise ValueError('value must be >= 0')
    
    class Foo:
        x = Quantity('x')
    
        def __init__(self, x):
            self.x = x

    版本二:描述符声明时无需传入名称

    声明描述符时,需要明确指明Quantity实例的名称,不仅麻烦,还可能造成覆盖错误,可以对描述符进行改造,支持不需要输入名称。

    class Quantity:
        __counter = 0
    
        def __init__(self):
            cls = self.__class__  # 等效于type(self),获取类的引用
            prefix = cls.__name__
            index = cls.__counter
            self.storage_name = '_{}#{}'.format(prefix, index)
            cls.__counter += 1
    
        def __get__(self, instance, value):
            if instance is None:
                return self  # 通过类访问返回时,返回自身
            else:
                return getattr(instance, self.storage_name)
    
        def __set__(self, instance, value):
            if value >= 0:
                setattr(instance, self.storage_name, value)  # 这里如果使用setattr不会无限递归,因为托管属性和存储属性名称不同
            else:
                raise ValueError('value must be >= 0')
    
    class Foo:
        x = Quantity()
        y = Quantity()
        
        def __init__(self, x, y):
            self.x = x
            self.y = y
    

    版本三:多种属性管理时,有基类的描述符,可以实现描述符的参数输入

    x属性和y属性的管理中,有很多是相同的,例如建立存储属性_counter#0,get访问属性等等,差异部分只是对输入值的验证。

    可以提取相同部分作为基类,差异部分对输入值的验证由子类实现。

    class Descriptor:  # 基类
        __counter = 0  # 类变量会被所有实例共享
    
        def __init__(self, **opts):  # 因为MaxSized需要输入输入键值对,基类需要支持
            cls = self.__class__
            self.name = '_{}#{}'.format(cls.__name__, cls.__counter)
            cls.__counter += 1
            for key, value in opts.items():
                setattr(self, key, value)
    
        def __get__(self, instance, value):
            if instance is None:
                return self  # 通过类访问返回时,返回自身
            else:
                return getattr(instance, self.name)
    
        def __set__(self, instance, value):
            setattr(instance, self.name, value)  # 也可以instance.__dict__[self.storage_name] = value
    
    
    class Typed(Descriptor):  # 基类2:实现type验证
        expected_type = type(None)
    
        def __set__(self, instance, value):
            if not isinstance(value, self.expected_type):
                raise TypeError('Expected ' + str(self.expected_type))
            super().__set__(instance, value)
    
    
    class Unsigned(Descriptor):  # 基类3:实现数值验证
        def __set__(self, instance, value):
            if value < 0:
                raise ValueError('Expected >= 0')
            super().__set__(instance, value)
    
    
    class MaxSized(Descriptor):  # 基类4:实现str长度验证
        def __init__(self, **opts):
            if 'size' not in opts:
                raise TypeError('Missing size option')
            super().__init__(**opts)
    
        def __set__(self, instance, value):
            if len(value) >= self.size:
                raise ValueError('size must be < ' + str(self.size))
            super().__set__(instance, value)
    
    
    class Inter(Typed):
        expected_type = int
    
    
    class String(Typed):
        expected_type = str
    
    
    class UnsignedInteger(Inter, Unsigned):   # 整型 + >=0
        pass
    
    
    class SizedString(String, MaxSized):  # string + 最大长度8
        pass
    
    
    class Foo:
        x = UnsignedInteger()
        y = SizedString(size=8)
    
        def __init__(self, x, y):
            self.x = x
            self.y = y

    对Foo实例化:

    f1 = Foo(-1, 'a')  # ValueError: value must be >= 0
    f2 = Foo(1, 'abcdefghi')  # ValueError: size must be < 8
    

    描述符总结:

    Foo:托管类。把描述符的实例声明为类属性的类(即x = Quantity())。

    Foo的x:托管属性,Foo的类属性,Quantity描述符类的实例。由描述符实例处理的公开属性。

    Quantity:描述符类。实现了描述符的类。

    Quantity的storeage_name:存储属性。

    覆盖型与非覆盖型描述符:

    待补充 

    4,property的本质

    4.1,property是个类

    propery本质上含有装饰器方法的类

    如下例,Foo多了个x属性,并且x属性是个property类:property(fget, fset, fdel)

    class FooNoPro:
        def __init__(self, x):
            self.x = x
    
    class Foo:
        def __init__(self, x):
            self._x = x
    
        @property
        def x(self):
            return self._x
    
    >>>set(dir(Foo)) - set(dir(FooNoPro))   
    {‘x'}
    >>>type(Foo.x)        
    <class 'property'>
    

    4.2,property装饰过程

    装饰前,x是个Foo类的方法:

    class Foo:
        def __init__(self, x):
            self._x = x
    
        def x(self):
            return self._x
    
    >>>s = Foo(10)
    >>>dir(s)  # 实例s包括了属性_x和方法x
    [......,  '_x', 'x']
    >>>Foo.x   # x是个Foo类的方法
    <function Foo.x at 0x005174B0>
    >>>s.x     # 访问实例s.x,返回的也是Foo类的x方法
    <bound method Foo.x of <__main__.Foo object at 0x008CEE90>>
    

     使用猴子补丁,装饰x方法:

    Foo.x = property(Foo.x)  # 打猴子补丁,相当于@property装饰,property类实例化时第一个参数是fget=Foo.x
    

     装饰后,Foo.x变成了property类的实例,再次访问s.x返回的是10而不是函数地址了:

    >>>type(Foo.x)
    type(Foo.x)
    >>>s.x  
    10
    

    为什么访问s.x能拿到10?

    property背后实现了__get__,调用时s.x相当于调用:

    >>>Foo.x.__get__(s, Foo) # 相当于s.x
    10
    

    5,动态存取属性

    setattr:设置属性时会触发,类的实例化时也会触发

    getattr:当访问不存在的属性时会触发,访问已经存在的属性时不会触发

    class Foo:
    
        def __init__(self, x):
            self.x = x
    
        def __setattr__(self, key, value):
            self.__dict__[key] = value + 1  # 如果调用self.key = value + 1 会无限递归
    
        def __getattr__(self, item):
            return 'not valid attribute'  

    调用属性:

    f = Foo(1)
    f.y = 5
    print(f.x)   # 2
    print(f.y)   # 6
    print(f.z)   # not valid attribute

    注意与动态存取分量的区别:__getitem__,__setitem__

    6,处理属性的内置函数和特殊方法

    6.1,处理属性的内置函数

    dir:列出对象大多数属性,静态属性只会列出__dict__属性键值对中的键

    vars:获取对象的__dict__属性键值对,如果定义了__slots__元组形式保存属性,则无法处理该对象,此时可以靠dir可列出

    getattr:获取对象属性

    hasattr:判断对象有无属性

    setattr:设置对象的属性

    delattr:删除对象的属性

    上述get, has, set, del可以动态的实现属性的增删改查。

    6.2,处理属性的特殊方法

    __delattr__:删除属性,通过del触发

    __dir__:供dir调用

    __getattr__:获取指定属性失败后,getattr和hasattr可能会触发

    __getattribute__: 获取指定属性时总会调用,点号 / getattr / hasattr会触发

    __setattr__:设置指定属性时总会调用,点号 / setattr会触发

  • 相关阅读:
    intellij IDEA启动springboot项目报无效的源发行版错误解决方法
    JDBC调用oracle 存储过程
    Java自定义注解学习
    [Python3网络爬虫开发实战] 1.7.1-Charles的安装
    [Python3网络爬虫开发实战] 1.6.1-Flask的安装
    [Python3网络爬虫开发实战] 1.6.2-Tornado的安装
    [Python3网络爬虫开发实战] 1.5.4-RedisDump的安装
    [Python3网络爬虫开发实战] 1.5.3-redis-py的安装
    [Python3网络爬虫开发实战] 1.5.2-PyMongo的安装
    [Python3网络爬虫开发实战] 1.4.3-Redis的安装
  • 原文地址:https://www.cnblogs.com/guxh/p/10236035.html
Copyright © 2020-2023  润新知