Python属性访问和描述符,property装饰器

Python属性访问和描述符,property装饰器

一、属性访问

1.一个实例

看下面这个示例代码

class Animal(object):
    run = True
class Dog(Animal):
    fly = False
    def __init__(self, age):
        self.age = age
    def sound(self):
        return "wang wang~"
if __name__ == '__main__':
    # 实例化一个对象dog
    dog = Dog(1)
    # 查看dog对象的属性
    print('dog.__dict__:', dog.__dict__)
    # 查看类Dog的属性
    print('Dog.__dict__:', Dog.__dict__)
    # 查看类Animal的属性
    print('Animal.__dict__:', Animal.__dict__)

以下为控制台输出结果

dog.__dict__: {'age': 1}
Dog.__dict__: {'__module__': '__main__', 'fly': False, '__init__': <function Dog.__init__ at 0x000002586A2FF9D0>, 'sound': <function Dog.sound at 0x000002586A2FFA60>, '__doc__': None}
Animal.__dict__: {'__module__': '__main__', 'run': True, '__dict__': <attribute '__dict__' of 'Animal' objects>, '__weakref__': <attribute '__weakref__' of 'Animal' objects>, '__doc__': None}
  • 下面对这些结果作一些总结:
    • 类Animal定义的属性run只出现在自身而不出现在其子类dog中。
    • 类Dog中定义的两个函数和定义的fly字段只出现在自身而不出现在具体实例dog中。
    • 实例对象dog在初始化时(调用Dog类的构造函数)加上了一个age字段,但这个字段并不属于Dog类。除此之外,实例对象dog本身并不具有其类中的方法。

2.__getattribute__方法

我们想要在访问对象属性的同时打印一些信息。在类中对于对象属性的访问都会调用__getattribute__方法。我们先进行下面的尝试:

class Animal(object):
    run = True
class Dog(Animal):
    fly = False
    def __init__(self, age):
        self.age = age
    def sound(self):
        return "wang wang~"

    def __getattribute__(self, item):
        print("这里在访问狗的年龄")
        return self.item

if __name__ == '__main__':
    # 实例化一个对象dog
    dog = Dog(1)
    print(dog.age)

发现程序居然爆栈了!原因何在?

  • 我们是重写了该方法。调用__getattribute__方法就相当于执行self.字段形式的代码。在返回self.item时,实际上还是调用的self.__getattribute__(item)方法。然后再次调用,造成了爆栈。也就是说由于重写了该方法,每次对象调用的都是调用的重写以后的方法造成无限递归。
  • 因此考虑不能够使用点运算符来访问。因此考虑采用下面的代码形式
class Animal(object):
    run = True
class Dog(Animal):
    fly = False
    def __init__(self, age):
        self.age = age
    def __getattribute__(self, key):
        print  ("calling __getattribute__")
        return super(Dog, self).__getattribute__(key)
    def sound(self):
        return "wang wang~"

以下为测试代码和控制台输出结果:

# 实例化对象dog
dog = Dog(1)
# 访问dog对象的age属性
print ('dog.age:',dog.age)
# 访问dog对象的fly属性
print ('dog.fly:',dog.fly)
# 访问dog对象的run属性
print ('dog.run:',dog.run)
# 访问dog对象的sound方法
print ('dog.sound:',dog.sound)
calling __getattribute__
dog.age: 1
calling __getattribute__
dog.fly: False
calling __getattribute__
dog.run: True
calling __getattribute__
dog.sound: <bound method Dog.sound of <__main__.Dog object at 0x00000253FA4CB7B8

在访问具体属性之前都进行了相对应的打印操作。

3.对象属性控制

3.1 __getattr__方法

  • 如果在第2节中介绍的__getattribute__方法找不到相应的属性,可以用来定义类的行为,增强了程序的健壮性。

看下面这个实例:

class Animal(object):
    run = True
class Dog(Animal):
    fly = False
    def __init__(self, age):
        self.age = age
    def __getattr__(self, name):
        print ("calling __getattr__")
        if name == 'adult':
            return True if self.age >= 2 else False
        else:
            raise AttributeError

以上的Dog类及其实例中并不存在name字段。如果注释掉上述的7-12行代码,并运行下面所示的代码:

if __name__ == '__main__':
    # 实例化一个对象dog
    dog = Dog(1)
    print(dog.adult)

果不其然,抛出了以下的错误:

Traceback (most recent call last):
  File "C:/Users/nth12/Desktop/machine learning/hw5/tester2.py", line 17, in <module>
    print(dog.name)
AttributeError: 'Dog' object has no attribute 'name'

显然,这是由于dog中没有adult字段所致,__getattribute__找不到相应的adult字段。如果去掉7-12行的注释,输出结果:

calling __getattr__
False

于是,推断__getattribute__在找不到字段时将控制转移到了__getattr__的位置,然后执行这个函数中的代码。

3.2 __setattr__(self, name, value)方法

在3.1节中如果执行dog.age = 1时便会执行__setattr__方法,调用dog.fly = True也会执行该方法。在Dog类中重写此方法加以验证。

class Animal(object):
    run = True
class Dog(Animal):
    fly = False
    def __init__(self, age):
        self.age = age

    def __setattr__(self, name, value):
            print("calling __setattr__,name = "+name+" value="+str(value))
            super(Dog, self).__setattr__(name, value)

执行如下代码:

if __name__ == '__main__':
    # 实例化一个对象dog
    dog = Dog(1)
    dog.age = 3
    dog.fly = True
    dog.run = False
  • 同样,这种形式的赋值就等价于调用这个方法。防止无限递归的问题需要注意第十行代码的处理方法。

  • 控制台输出的结果如下:

    calling __setattr__,name = age value=1
    calling __setattr__,name = age value=3
    calling __setattr__,name = fly value=True
    calling __setattr__,name = run value=False
    

但是,一个很微妙的问题是,我们查看下Dog类和dog对象自身的字典,执行如下的代码:

if __name__ == '__main__':
    # 实例化一个对象dog
    dog = Dog(1)
    print(dog.fly)
    dog.age = 3
    dog.fly = True
    dog.run = False
    print(dog.__dict__)
    print(Dog.__dict__)
    print(dog.fly)
    print(Dog.fly)

控制台的输出结果如下:

calling __setattr__,name = age value=1
False
calling __setattr__,name = age value=3
calling __setattr__,name = fly value=True
calling __setattr__,name = run value=False
{'age': 3, 'fly': True, 'run': False}
{'__module__': '__main__', 'fly': False, '__init__': <function Dog.__init__ at 0x0000017C6852E9D0>, '__setattr__': <function Dog.__setattr__ at 0x0000017C6852EA60>, '__doc__': None}
True
False
  • 从以上的输出结果可以看出:
    • 第一次调用fly属性时,输出的是dog类中的false;第二次调用时,方法在具体dog实例中新生成了一个fly属性并赋值为True
    • 所以,实际上我们调用的方法是在dog对象自身中加入了fly属性,run属性。对类中的属性并没有影响。
    • 再次访问时,显然就是以实例中我们设置的属性为准了。
  • 总结:
    • 实例对象的__setattr__方法可以定义属性的赋值行为,不管属性是否存在。
    • 当属性存在时,它会改变其值;
    • 当属性不存在时,它会添加一个对象属性信息到对象的__dict__中,然而这并不改变类的属性。

3.3 __delattr__(self,name)方法

在Dog类中添加如下的方法:

    def __delattr__(self, name):
        print ("calling __delattr__")
        super(Dog, self).__delattr__(name)

在上述程序的基础上执行del dog.fly语句,再观察dog实例的字典:

# 由于上面的例子中我们为dog设置了fly属性,现在删除它触发__delattr__方法
del dog.fly
# 再次查看dog对象的__dict__,发现和fly属性相关的信息被删除
print (dog.__dict__)

控制台输出结果如下:

calling __delattr__
{'age': 1}

但是这对类并无影响。此时如果执行dog.fly还是会显示类中的结果,即false。

二、描述符

1、定义

  • 如果一个类中包含__get__,__set__,__delete__方法之一或全部则称这个类为描述符;
    • 不同于以上介绍的三种方式,描述符作用是对类或对象中某个成员进行详细的管理操作。
    • 而以上三种方法可以管理所有的属性。
  • 数据描述符:同时具备上述三种方法的类(优先级更高);否则称为非数据描述符

2、案例研究

class Email:
   #成员属性
   username = '123'
   passport = '123456'
   phone = 12345678


   #成员方法
   #登陆
   def login(self):
      print("denglu")

   def logout(self):
      print("tuichu")


if __name__ == '__main__':
   e = Email()
   print(e.username)
   e.login()

运行上述代码,控制台输出下列结果

123
denglu

e实例自身并不具备username,于是__getarribute__返回了我们的Email类中的Username;但是若想单独对Username进行管理是很不方便的。因此有了下面的描述符代码:

class Describor:
   #定义描述符的三个成员

   def __init__(self):
      self.username_control = 1234


   def __get__(self, instance, owner):
      print("访问了用户名")
      return self.username_control

   def __set__(self, instance, value):
      pass

   def __delete__(self, instance):
      pass



class Email:
   #成员属性
   username = Describor()
   passport = '123456'
   phone = 12345678


   #成员方法
   #登陆
   def login(self):
      print("denglu")

   def logout(self):
      print("tuichu")


if __name__ == '__main__':
   e = Email()
   print(e.username)
  • 代码的第18行将username交付给一个描述符进行管理。
    • 描述符中定义的username_control用于与原始的username产生关联。
    • 控制台的输出结果如下所示:访问了用户名 1234
  • 显然调用了__get__函数,于是可以在__get__中加入一些额外的东西进行装饰。

将36-38行换成如下的代码:

if __name__ == '__main__':
   e = Email()
   print(e.username)
   e.username = '5678'
   print(e.username)

控制台的输出结果如下:

访问了用户名
1234
访问了用户名
1234

显然我们的设置并没有成功。原因很简单,由于我们将username已经交付给描述符进行管理,执行第4行的语句是,此时描述符会调用__set__方法进行设置。而此时__set__处直接pass,什么都没做。在第5行执行e.username时还是像上面一样执行__get__方法,因此结果会与上面一样。于是,修改上述代码的set模块处的操作:

   def __set__(self, instance, value):
      #设置与被管理成员在该描述符中对应的关联成员。在此例中为username_control;还可附加一些额外的修改信息
      self.username_control = value
      print('设置了用户名')

再次运行上述主函数的代码,可以得到预期的输出结果:

访问了用户名
1234
设置了用户名
访问了用户名
5678

同样,为了测试是否删除操作也是和前面两个类似,将主函数的代码修改成如下的形式:

if __name__ == '__main__':
   e = Email()
   print(e.username)
   e.username = '5678'
   print(e.username)
   del e.username
   print(e.username)

控制台输出结果如下:

访问了用户名
1234
设置了用户名
访问了用户名
5678
访问了用户名
5678

显然并没有删除成功,原因还是因为__delete函数直接pass掉了,删除过程中调用了delete函数。于是,将__delete__函数修改成下面的形式:

   def __delete__(self, instance):
      #删除临时变量即可
      del self.username_control

当然在删除过程中还可以加一些修饰。例如如果不允许该用户名被删除,则在该函数中直接pass掉或是输出一些信息。再例如,由于instance为被管理的实例,可以在instance中设置一些flag,在该函数中访问,以控制删除操作。

再次执行主函数代码观察运行结果:

访问了用户名
1234
设置了用户名
访问了用户名
5678
访问了用户名
Traceback (most recent call last):
  File "C:/Users/nth12/Desktop/machine learning/hw5/tester.py", line 53, in <module>
    print(e.username)
  File "C:/Users/nth12/Desktop/machine learning/hw5/tester.py", line 18, in __get__
    return self.username_control
AttributeError: 'Describor' object has no attribute 'username_control'

最后抛出异常,这是因为我们的self.username_control已经在__delete__函数中被删除了。再次执行__get__函数访问这一字段时抛出访问不到的异常。

2.1__get__(self, instance, owner)方法
  • 在访问对象成员属性(该成员已经交给描述符管理的时候)进行触发。例如上例中Email类中的username被交付给了描述符Describor进行管理。在主函数中访问username中触发了该方法。

  • 功能:查看当前属性的值。

  • 参数:

    • self:描述符对象自身
    • instance:被管理成员的类的所在实例对象(在上例中为Email类的实例对象e)
    • owner:被管理成员的所在类(在上例中为Email类)
  • 返回值:可有可无。若无则为None

2.2__set__(self, instance, value)方法
  • 在设置对象成员属性(该成员已经交给描述符管理的时候)进行触发。例如上例中Email类中的username被交付给了描述符Describor进行管理。在主函数中设置username中触发了该方法。
  • 参数:
    • self:描述符对象自身
    • instance:被管理成员的类的所在实例对象(在上例中为Email类的实例对象e)
    • value:要设置的值
  • 设置的时候一定要设置关联的成员,否则没有意义。例如上面的实例中设置一定要设置与username关联的成员username_control,否则没有意义。
2.3__delete__(self, instance)方法
  • 在删除对象成员属性(该成员已经交给描述符管理的时候)进行触发。例如上例中Email类中的username被交付给了描述符Describor进行管理。在主函数中删除username中触发了该方法。
  • 参数:
    • self:描述符对象自身
    • instance:被管理成员的类的所在实例对象(在上例中为Email类的实例对象e)
  • 如果要删除,删除的时候一定要删除关联的成员,否则没有意义。

三、区分属性访问和描述符

  • 描述符仅仅针对于某个特定的成员,可以对不同的类进行使用。(例如上述中的instance,可根据Instance中额外的成员或Instance的类型做不同的操作),具有良好的可复用性
  • 属性访问针对当前类和对象所有成员的管理,仅对当前类有效。

四、property函数&property装饰器

1 、property函数提供了描述符类

在第二部分中,将描述符类与被管理的类相互分离,那么能否将两者合二为一呢?

property类是一个专用的描述符类。考虑下面的代码。


class Email:
   # 成员属性
   username = '匿名用户'
   passport = '123456'

   # phone = 12345678
   def __init__(self):
      self.username_control = '123456'  # 用于与Username产生关联。类似于上面Describor类中的username_control
   # 成员方法
   # 登陆
   def login(self):
      print("denglu")

   def logout(self):
      print("tuichu")

   # 委托给描述符部分的描述符的开始区域
   

   def getusername(self):
      pass

   def setusername(self, value):
      pass  #改成和__set__方法类似的形式

   def deleteusername(self):
      pass  #改成和__delete__方法类似的形式

   # 委托给描述符部分的结束区域
   # property是一个特有的描述符类,与上面的用法相同的
   username = property(getusername, setusername, deleteusername, doc='username')

先执行下面的代码:

if __name__ == '__main__':
   e = Email()
   print(e.username)

控制台输出结果如下所示:

None

将getusername改成如下的形式:

def getusername(self):
      return self.username_control

再次运行上述主函数,运行结果如下:

123456
  • 显然在访问时,被property调用了我们的getusername方法。于是,以上定义的getusername,setusername,deleteusername和我们先前看到的get,set和delete应该具有相似的作用。只不过是property中调用了后者三个方法,而在调用后者三个方法的过程中,又委托给了我们定义的三个getusername,setusername,deleteusername进行了实现。
  • 于是,在这个类中应该还有一个与username关联的成员变量
  • 还需要注意三个定义的函数的形式。在代码中已经给出。
    • 其中setusername中的value为欲设置的值

2、property装饰器

property装饰器的用法如下:

class C:
    def __init__(self):
        self._x = None  #用于关联x的变量,类似于上面的username_control

    @property#本质上进行描述符的关联,即定义一个成员变量x并与一个property描述符对象相关联。并类似于getusername方法。
    def x(self):
        """I'm the 'x' property."""
        return self._x 

    @x.setter
    def x(self, value):  #类似于setusername方法
        self._x = value

    @x.deleter
    def x(self):      #类似于deleteusername方法
        del self._x
    #如果 c 是 C 的实例,c.x 将调用getter,c.x = value 将调用setter, del c.x 将调用deleter。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值