classCar:def__init__(self,make,model,fuel_cap):
self.make = make
self.model = model
self.fuel_cap = fuel_cap
def__str__(self):return"{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)技术支持
为避免该问题,我们在 __init__() 方法中添加一个 if 条件,检查输入的油箱容量是否合法。如果无效,则抛出 ValueError 异常。
classCar: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)技术支持
----------------------------------------ValueErrorTraceback(most recent call last)<ipython-input-22-1c3d23be72f7>in<module>---->1 car2 =Car("BMW","X7",-40)2print(car2)<ipython-input-20-1e154783588d>in__init__(self, make, model, fuel_cap)5 self.fuel_cap = fuel_cap
6if self.fuel_cap <0:---->7 raise ValueError("Fuel Capacity can never be less than zero")89 def __str__(self):ValueError: Fuel Capacity can never be less than zero
技术支持
从上面的输出可以看到,目前一切正常,因为当油箱容量小于零时,程序会抛出 ValueError。
不过,还可能有另一个问题:如果输入的油箱容量是浮点数或字符串怎么办?不仅是油箱容量,汽车的 make 和 model 也可能被传入整数。在这些情况下,程序将无法抛出异常。
classCar: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)技术支持
classCar:def__init__(self,make,model,fuel_cap):
self.make = make
self.model = model
self.fuel_cap = fuel_cap
ifisinstance(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)技术支持
----------------------------------------ValueErrorTraceback(most recent call last)<ipython-input-34-75b08cba454f>in<module>---->1 car2 =Car(-40,"X7",40)2print(car2)<ipython-input-31-175690bf3b98>in__init__(self, make, model, fuel_cap)7print(self.make)8else:---->9 raise ValueError("Make of the car can never be an integer")1011if self.fuel_cap <0:ValueError: Make of the car can never be an integer
技术支持
classCar:def__init__(self,make,model,fuel_cap):
self.make = make
self.model = model
self.fuel_cap = fuel_cap
ifisinstance(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)技术支持
classDescriptor:def__init__(self):
self.__fuel_cap =0def__get__(self, instance, owner):return self.__fuel_cap
def__set__(self, instance, value):ifisinstance(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 = value
def__delete__(self, instance):del self.__fuel_cap
技术支持
classCar:
fuel_cap = Descriptor()def__init__(self,make,model,fuel_cap):
self.make = make
self.model = model
self.fuel_cap = fuel_cap
def__str__(self):return"{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)技术支持
-40----------------------------------------ValueErrorTraceback(most recent call last)<ipython-input-115-1c3d23be72f7>in<module>---->1 car2 =Car("BMW","X7",-40)2print(car2)<ipython-input-107-3e1f3e97d3c7>in__init__(self, make, model, fuel_cap)4 self.make = make
5 self.model = model
---->6 self.fuel_cap = fuel_cap
78 def __str__(self):<ipython-input-106-0b252695aeed>in__set__(self, instance, value)1112if value <0:--->13 raise ValueError("Fuel Capacity can never be less than zero")1415 self.__fuel_cap = value
ValueError: Fuel Capacity can never be less than zero
技术支持
-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)1112if value <0:--->13 raise ValueError("Fuel Capacity can never be less than zero")1415 self.__fuel_cap = value
ValueError: Fuel Capacity can never be less than zero
技术支持
----------------------------------------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:--->10 raise TypeError("Fuel Capacity can only be an integer")1112if value <0:TypeError: Fuel Capacity can only be an integer
技术支持
Python 的描述符(Descriptors),更广义地说,描述符为您提供了一种强大的技术,用于编写可在类之间共享的可复用代码。它们看起来可能与继承类似,但从技术上讲并非如此。描述符是一种拦截属性访问的通用方式。属性、静态方法、类方法、super 方法等特性背后用到的正是描述符机制。
描述符在 Python 2.2 版本中被引入,此后常被视为为传统类带来新风格的“魔法”。它们是一些类,允许您在另一个类中对属性进行受控管理。具体来说,它们实现了
__get__()、__set__()和__delete__()接口方法,这让它们在许多情境下都很有用。例如,您在 Python 中见过的类装饰器和属性装饰器。简单来说,实现了对象的
__get__()、__set()__或__delete()__方法的类被称为“描述符”。直接引用 Python 官方文档的说法,描述符是具有绑定行为的对象属性,其属性访问被描述符协议中的方法所重写。这些方法是__get__()、__set__()和__delete__()(来源)。对于描述符而言,
绑定行为指的是绑定对某个变量、对象或数据集的赋值(set)、查询(get)或删除(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(self, object, type) set(self, object, value) delete(self, object)__get__()用于访问属性或提取某些信息。它返回属性的值,或在请求的属性不存在时抛出 AttributeError 异常。__set__()在进行属性赋值操作时调用,用于设置属性的值。不返回任何内容,但可以抛出 AttributeError 异常。__delete__()控制删除操作,即当您想从对象中删除属性时调用。不返回任何内容。现在让我们通过一些示例来理解描述符的用途!
描述符的用途
先定义一个类
car,它有三个属性:make、model和fuel_cap。您将使用__init__()方法初始化类的属性。然后使用魔术方法__str__(),它会简单返回创建对象时传入类的这三个属性的输出。请注意,
__str__()方法返回对象的字符串表示形式。当对类的对象调用print()或str()函数时会触发该方法。如上面的输出所示,一切看起来都很好!
现在把汽车的油箱容量改为负 40。
等等,这有问题,对吧?汽车的油箱容量不可能为负数。然而,Python 接受了它且没有报错。这是因为 Python 是动态语言,不会显式进行类型检查。
为避免该问题,我们在
__init__()方法中添加一个if条件,检查输入的油箱容量是否合法。如果无效,则抛出 ValueError 异常。从上面的输出可以看到,目前一切正常,因为当油箱容量小于零时,程序会抛出
ValueError。不过,还可能有另一个问题:如果输入的
油箱容量是浮点数或字符串怎么办?不仅是油箱容量,汽车的make和model也可能被传入整数。在这些情况下,程序将无法抛出异常。为处理上述情况,您可能会考虑再加一个
if条件,或使用isinstance方法进行类型检查。这次我们使用内置的
isinstance方法来处理错误。很好!这个错误也被妥善处理了。
但是,如果您想在之后将油箱容量属性显式改为负 40,会怎样?此时就不起作用了,因为类型检查只会在
__init__()中执行一次。您知道,__init__()是构造函数,只在创建对象时调用一次。因此,之后的自定义类型检查会失效。我们举个例子来理解。
看到了吗?您成功绕过了类型检查。
再想一想,如果汽车还有很多其他属性,如油耗、价格、配件等,也需要类型检查,而且您还希望其中一些属性只有
读取权限。这难道不会很烦吗?为了解决以上所有问题,Python 的描述符就派上用场了!
如上所述,任何为描述符协议对象实现了
__get__()、__set()__或__delete()__魔术方法的类,都是描述符。它们还能为属性的行为提供额外控制,例如是否应具备读写权限。现在让我们通过添加 Python 描述符方法来扩展上面的示例。
别担心如果 Descriptor 类看起来有点晦涩。让我们把它拆开来看看每个方法都在做什么。
__init__()方法将局部变量__fuel_cap置为 0。开头的dunder(双下划线)表示该变量是私有的。这里使用双下划线只是为了将描述符类的油箱容量属性与 Car 类的区分开来。__get__()方法用于获取属性,并返回油箱容量变量。它接收三个参数:描述符对象、包含该描述符对象实例的类的实例(即car2),以及 owner,即该实例所属的类(Car类)。在这个方法中,您只需返回由__set__()方法设置的值属性fuel_cap。__set__()方法在为属性设置值时被调用,与__get__()不同,它不返回任何内容。除描述符对象本身外,它还有两个参数:与__get__()方法相同的实例,以及您要赋给属性的值。在该方法中,您检查要赋给fuel_cap的值是否为整数;若不是,则抛出TypeError异常。随后还检查该值是否小于 0;若小于 0,则抛出ValueError异常。完成检查后,将fuel_cap属性更新为该值。__delete__()方法,当从对象中删除属性时调用,与__set__()一样不返回任何内容。Car类保持与之前相同。但唯一的变化是添加了Descriptor()类的实例fuel_cap。请注意,如前所述,描述符类的实例必须作为类属性添加到类上,而不是作为实例属性。一旦您在
__init__()中将局部变量fuel_cap赋给实例的fuel_cap,就会触发 Descriptor 类的__set__()方法。现在把油箱容量改为负值,看看程序是否会抛出 ValueError 异常。
如果您还记得这里,当您后来把属性值改为负数时,类型检查并未生效,因为它仅在
__init__()中执行一次。让我们把 fuel_cap 的值更新为字符串,看看是否会报错。完美!如您所见,在之后更新油箱容量属性时,检查也能正常工作。
不过,描述符有一个小问题:当您创建一个新实例或第二个实例时,先前实例的值会被覆盖。原因在于描述符绑定在类上,而不是实例上。
我们通过下面的例子来理解。
当您打印实例
car2时,会发现其值已被car3覆盖。结论
恭喜您完成本教程。
本教程适合已经熟悉 Python 并希望进阶到高级水平的读者。作为一个不错的练习,您或许可以思考如何解决今天教程中提到的实例值被覆盖的问题。
如有与本教程相关的问题,欢迎在下方评论区提问。
如果您想进一步学习 Python,请查看 DataCamp 的完整 Python Programming 技能路径。
也来看看我们的 Python 函数教程。