Python描述符

本文概述

  • 描述符的目的
  • 总结
Python描述符或更一般地, 描述符为你提供了一种强大的技术来编写可在类之间共享的可重用代码。它们看起来可能类似于继承的概念, 但从技术上讲, 它们却并非如此。它们是拦截属性访问的通用方法。描述符是属性的静态方法, 类方法, 超级方法等背后的机制。
描述符是在Python 2.2版中添加的, 从那时起, 它们被视为神奇的事物, 为传统类赋予了新的样式。它们是允许你在另一个类中执行托管属性的类。具体来说, 它们为__get __(), __set __()和__delete __()方法实现接口, 这出于许多原因而使它们变得有趣。例如, 你之前在Python中可能看到的类装饰器和属性装饰器。
简单地说, 为对象实现__get __(), __set()__或__delete()__方法的类称为” 描述符” 。直接从Python的官方文档引用, 描述符是具有绑定行为的对象属性, 该对象属性的访问已被描述符协议中的方法所覆盖。这些方法是__get __(), __set __()和__delete __()(源)。
关于描述符的绑定行为意味着可以为给定的变量或对象或数据集设置, 查询(获取)或删除值的方式绑定。这种完全的交互作用绑定到该数据段, 因为它仅适用于你对其设置了哪些数据, 因此仅将其绑定到该数据的特定部分。
描述符可以进一步分类为数据和非数据描述符。如果编写的描述符仅具有__get __()方法, 则它是一个非数据描述符, 另一方面, 涉及__set __()和__delete __()方法的实现称为数据描述符。非数据描述符仅可读, 而数据描述符既可读又可写。
重要的是要注意, 描述符是分配给类的, 而不是分配给类的实例的。修改类将覆盖或删除描述符本身, 而不是触发其代码(IBM Developer)。
最后, 描述符类不仅限于这三种方法, 这意味着它除了get, set和delete方法之外, 还可以包含任何其他属性。
让我们更详细地了解受此IBM Developer页面启发的get, set和delete方法:
  • self是你创建的描述符的实例(Real Python)。
  • object是你的描述符附加的对象的实例(Real Python)。
  • type是描述符附加到的对象的类型(Real Python)。
  • value是分配给描述符属性的值。 get(自我, 对象, 类型)set(自我, 对象, 值)delete(自我, 对象)
  • __get __()访问属性或当你要提取某些信息时。它返回属性的值, 或者如果不存在所请求的属性, 则引发AttributeError异常。
  • 在设置属性值的属性分配操作中调用__set __()。不返回任何内容。但是会引发AttributeError异常。
  • __delete __()控制删除操作, 即, 当你想从对象中删除属性时。不返回任何内容。
现在, 借助一些示例来了解描述符的目的!
描述符的目的让我们定义一个具有三个属性的类车, 即make, model和fuel_cap。你将使用__init __()方法初始化该类的属性。然后, 你将使用魔术函数__str __(), 该函数将简单地返回创建对象时将传递给类的三个属性的输出。
【Python描述符】请注意, __str __()方法返回对象的字符串表示形式。在类的对象上调用print()或str()函数时, 将调用它。
class Car: def __init__(self, make, model, fuel_cap): self.make = make self.model = model self.fuel_cap = fuel_capdef __str__(self): return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make, self.model, self.fuel_cap)

car1 = Car("BMW", "X7", 40) print(car1)

BMW model X7 with a fuel capacity of 40 ltr.

因此, 从上述输出中你可以看到, 一切看起来都很棒!
现在让我们将汽车的燃油容量更改为负40。
car2 = Car("BMW", "X7", -40) print(car2)

BMW model X7 with a fuel capacity of -40 ltr.

等等, 有什么不对吗?汽车的燃油容量永远不能为负。但是, Python接受它作为输入没有任何错误。这是因为Python是一种动态编程语言, 不明确支持类型检查。
为避免此问题, 让我们在__init __()方法中添加一个if条件, 并检查输入的燃料容量是否有效。如果输入的燃油容量无效, 则引发ValueError异常。
class Car: def __init__(self, make, model, fuel_cap): self.make = make self.model = model self.fuel_cap = fuel_cap if self.fuel_cap < 0: raise ValueError("Fuel Capacity can never be less than zero")def __str__(self): return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make, self.model, self.fuel_cap)

car1 = Car("BMW", "X7", 40) print(car1)

BMW model X7 with a fuel capacity of 40 ltr.

car2 = Car("BMW", "X7", -40) print(car2)

----------------------------------------ValueErrorTraceback (most recent call last)< ipython-input-22-1c3d23be72f7> in < module> ----> 1 car2 = Car("BMW", "X7", -40) 2 print(car2)< ipython-input-20-1e154783588d> in __init__(self, make, model, fuel_cap) 5self.fuel_cap = fuel_cap 6if self.fuel_cap < 0: ----> 7raise ValueError("Fuel Capacity can never be less than zero") 8 9def __str__(self):ValueError: Fuel Capacity can never be less than zero

从上面的输出中, 你可以观察到目前一切正常, 因为如果燃油容量低于零, 则程序会引发ValueError。
但是, 可能会有另一个问题, 即, 如果输入的燃料容量是浮点值或字符串, 该怎么办。不仅燃料容量, 而且汽车的品牌和型号都可以是整数值。在所有这些情况下, 程序都不会引发异常。
class Car: def __init__(self, make, model, fuel_cap): self.make = make self.model = model self.fuel_cap = fuel_cap if self.fuel_cap < 0: raise ValueError("Fuel Capacity can never be less than zero")def __str__(self): return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make, self.model, self.fuel_cap)

car2 = Car(-40, "X7", 40) print(car2)

-40 model X7 with a fuel capacity of 40 ltr.

为了同样处理上述情况, 你可能会考虑添加另一个if条件, 或者可能会使用isinstance方法进行类型检查。
这次让我们使用isinstance内置方法来处理错误。
class Car: def __init__(self, make, model, fuel_cap): self.make = make self.model = model self.fuel_cap = fuel_cap if isinstance(self.make, str): print(self.make) else: raise ValueError("Make of the car can never be an integer")if self.fuel_cap < 0: raise ValueError("Fuel Capacity can never be less than zero")def __str__(self): return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make, self.model, self.fuel_cap)

car2 = Car("BMW", "X7", 40) print(car2)

BMW BMW model X7 with a fuel capacity of 40 ltr.

car2 = Car(-40, "X7", 40) print(car2)

----------------------------------------ValueErrorTraceback (most recent call last)< ipython-input-34-75b08cba454f> in < module> ----> 1 car2 = Car(-40, "X7", 40) 2 print(car2)< ipython-input-31-175690bf3b98> in __init__(self, make, model, fuel_cap) 7print(self.make) 8else: ----> 9raise ValueError("Make of the car can never be an integer") 10 11if self.fuel_cap < 0:ValueError: Make of the car can never be an integer

大!因此, 你也可以处理此错误。
但是, 如果你以后想要将燃料容量属性明确更改为负40怎么办。在这种情况下, 它将不起作用, 因为类型检查将仅在__init __()方法中进行一次。如你所知, __init __()方法是一个构造函数, 当你创建类的对象时, 它仅被调用一次。因此, 自定义类型检查将在以后失败。
让我们通过一个例子来理解它。
class Car: def __init__(self, make, model, fuel_cap): self.make = make self.model = model self.fuel_cap = fuel_cap if isinstance(self.make, str): print(self.make) else: raise ValueError("Make of the car can never be an integer")if self.fuel_cap < 0: raise ValueError("Fuel Capacity can never be less than zero")def __str__(self): return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make, self.model, self.fuel_cap)

car2 = Car("BMW", "X7", 40) print(car2)

BMW BMW model X7 with a fuel capacity of 40 ltr.

car2.make = -40

print(car2)

-40 model X7 with a fuel capacity of 40 ltr.

然后你去了!你可以摆脱类型检查。
现在以这种方式思考, 如果你还拥有汽车的许多其他属性(例如里程, 价格, 配件等), 该属性也需要类型检查, 并且你还希望其中某些属性仅具有读取权限的功能。那不是很烦吗?
好吧, 为解决上述所有问题, Python提供了可解救的描述符!
如上所学, 将为描述符协议对象实现__get __(), __set()__或__delete()__魔术方法的任何类称为描述符。它们还使你可以进一步控制属性的工作方式, 例如它应该具有读取或写入访问权限。
现在, 通过添加Python Descriptor方法来扩展上述示例。
class Descriptor: def __init__(self): self.__fuel_cap = 0 def __get__(self, instance, owner): return self.__fuel_cap def __set__(self, instance, value): if isinstance(value, int): print(value) else: raise TypeError("Fuel Capacity can only be an integer")if value < 0: raise ValueError("Fuel Capacity can never be less than zero")self.__fuel_cap = valuedef __delete__(self, instance): del self.__fuel_cap

class Car: fuel_cap = Descriptor() def __init__(self, make, model, fuel_cap): self.make = make self.model = model self.fuel_cap = fuel_capdef __str__(self): return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make, self.model, self.fuel_cap)

car2 = Car("BMW", "X7", 40) print(car2)

40 BMW model X7 with a fuel capacity of 40 ltr.

好吧, 不用担心类描述符是否晦涩难懂。让我们将其分解为小块, 了解每种方法的本质。
  • Descriptor类的__init __()方法的局部变量__fuel_cap为零。它的开头是连字符或下划线, 表示该变量是私有变量。刚开始时有一个惊叫只是为了区分Descriptor类的燃料容量属性和Car类。
  • 如你现在所知, __get __()方法用于检索属性, 并返回可变的燃料容量。它使用三个参数:描述符对象, 包含描述符对象实例的类的实例(即car2)以及最终所有者(所有者是实例所属的类), 即Car类。在此方法中, 你只需返回value属性, 即在__set __()方法中设置其值的fuel_cap。
  • 将值设置为属性时, 将调用__set __()方法, 并且与__get __()方法不同, 它不返回任何内容。除了描述符对象本身之外, 它还有两个参数, 即, 与__get __()方法相同的实例和值参数, 即你分配给属性的值。在此方法中, 检查要分配给fuel_cap属性的值是否为整数。如果不是, 则引发TypeError异常。然后, 在相同的方法中, 你还检查该值是否小于零(如果为零), 则引发另一个异常, 但这一次是ValueError异常。检查错误后, 更新等于该值的fuel_cap属性。
  • 最后, 当从对象中删除属性时调用__delete __()方法, 该方法类似于__set __()方法, 但不会返回任何内容。
Car类与以前相同。但是, 你所做的唯一更改是添加了Descriptor()类的实例fuel_cap。请注意, 如前所述, 描述符类的实例必须作为类属性而不是实例属性添加到类中。
一旦在__init __()方法中将局部变量fuel_cap设置为实例fuel_cap, 它将立即调用Descriptor类的__set __()方法。
现在, 让我们将燃油容量更改为负值, 看看程序是否引发ValueError异常。
car2 = Car("BMW", "X7", -40) print(car2)

-40----------------------------------------ValueErrorTraceback (most recent call last)< ipython-input-115-1c3d23be72f7> in < module> ----> 1 car2 = Car("BMW", "X7", -40) 2 print(car2)< ipython-input-107-3e1f3e97d3c7> in __init__(self, make, model, fuel_cap) 4self.make = make 5self.model = model ----> 6self.fuel_cap = fuel_cap 7 8def __str__(self):< ipython-input-106-0b252695aeed> in __set__(self, instance, value) 11 12if value < 0: ---> 13raise ValueError("Fuel Capacity can never be less than zero") 14 15self.__fuel_cap = valueValueError: Fuel Capacity can never be less than zero

如果你还记得这里, 则稍后将属性值更改为负数时, 类型检查将失败, 因为类型检查仅适用于__init __()方法中的一次。让我们将fuel_cap值更新为字符串值, 并找出它是否导致错误。
car2.fuel_cap = -1

-1----------------------------------------ValueErrorTraceback (most recent call last)< ipython-input-120-dea9dbe96ebe> in < module> ----> 1 car2.fuel_cap = -1< ipython-input-106-0b252695aeed> in __set__(self, instance, value) 11 12if value < 0: ---> 13raise ValueError("Fuel Capacity can never be less than zero") 14 15self.__fuel_cap = valueValueError: Fuel Capacity can never be less than zero

car2.fuel_cap = "BMW"

----------------------------------------TypeErrorTraceback (most recent call last)< ipython-input-121-0b316a9872c6> in < module> ----> 1 car2.fuel_cap = "BMW"< ipython-input-106-0b252695aeed> in __set__(self, instance, value) 8print(value) 9else: ---> 10raise TypeError("Fuel Capacity can only be an integer") 11 12if value < 0:TypeError: Fuel Capacity can only be an integer

完善!因此, 你可以看到, 稍后更新燃油容量属性时, 它可以工作。
好的, 描述符中存在一个小问题, 那就是当你创建该类的新实例或第二个实例时, 先前的实例值将被覆盖。原因是描述符链接到类而不是实例。
让我们用下面的例子来理解这一点。
car3 = Car("BMW", "X7", 48) #created a new instance 'car3' with different values

48

当你打印实例car2时, 你会发现这些值将被car3覆盖。
print(car2)

BMW model X7 with a fuel capacity of 48 ltr.

总结恭喜你完成了本教程。
本教程适用于那些熟悉Python并且渴望掌握高级水平的人。作为一项很好的练习, 你可能想了解如何解决今天的教程中讨论的实例覆盖问题。
请随时在下面的评论部分中提出与本教程相关的任何问题。
如果你想了解有关Python的更多信息, 请阅读srcmini的完整Python编程技能教程。

    推荐阅读