当前位置: 首页 > 科技观察

理解Python的数据类(一)

时间:2023-03-20 17:31:48 科技观察

如果您正在阅读本文,那么您已经了解Python3.7及其包含的新功能。就个人而言,我对Dataclasses感到非常兴奋,因为我已经等待了一段时间。本系列分为两部分:Dataclass特性概述下篇Dataclass字段概述简介Dataclass是Python类(LCTT译注:更准确地说是模块),适用于存储数据对象。你可能会问什么是数据对象?以下是定义数据对象的属性的不详尽列表:它们存储数据并表示数据类型。例如:一个数字。对于那些熟悉ORM的人来说,模型实例就是一个数据对象。它代表一种特定的实体。它包含定义或表示实体的那些属性。它们可以与相同类型的其他对象进行比较。例如:一个数可以大于(大于)、小于(小于)或等于(等于)另一个数。当然还有更多的特性,但是这个列表足以帮助你理解问题的症结所在。为了理解数据类,我们将实现一个简单的类来保存数字并允许我们执行上述操作。首先,我们将使用普通类,然后我们将使用数据类来实现相同的结果。不过在开始之前,先说说Dataclasses的用法。Python3.7提供了一个装饰器数据类,可以将类转换为数据类。您所要做的就是将类包装在装饰器中:fromdataclassesimportdataclass@dataclassclassA:...现在,让我们深入了解dataclass给我们带来的变化和用途。初始化通常是这样的:classNumber:def__init__(self,val):self.val=val>>>one=Number(1)>>>one.val>>>1withdataclasslikethis:@dataclassNumber:val:int>>>one=Number(1)>>>one.val>>>1以下是dataclass装饰器带来的变化:无需定义__init__,然后给self赋值,dataclass负责ofit(LCTT译注:此处原文可能有误,指的是一个不存在的d)我们以更易读的方式预定义了成员属性和类型提示。我们现在立即知道val是int类型。这肯定比通常定义类成员的方式更具可读性。Python之禅:可读性很重要还可以定义一个默认值:@dataclassclassNumber:val:int=0表示对象表示指的是对象有意义的字符串表示,这在调试时非常有用。默认的Python对象表示不是很直观:classNumber:def__init__(self,val=0):self.val=val>>>a=Number(1)>>>a>>>0x7ff395b2ccc0>这使我们无法了解对象的作用并导致糟糕的调试体验。可以通过在类上定义__repr__方法来实现有意义的表示。def__repr__(self):returnself.val现在我们得到这个对象的有意义的表示:>>>a=Number(1)>>>a>>>1数据类会自动添加一个__repr__函数,所以我们没有手动完成让它发生。@dataclassclassNumber:val:int=0>>>a=Number(1)>>>a>>>Number(val=1)数据比较通常,数据对象需要相互比较。两个对象a和b之间的比较通常涉及以下操作:aba==ba>=ba<=b在Python中,可以在类中定义可以执行上述操作的方法。为了简单起见并且不让本文过于冗长,我将只展示==和<的实现。通常写成这样:.val>>importrandom>>>a=[Number(random.randint(1,10))for_inrange(10)]#generatelistofrandomnumbers>>>a>>>[Number(val=2),数字(val=7),数字(val=6),数字(val=5),数字(val=10),数字(val=9),数字(val=1),数字(val=10),Number(val=1),Number(val=7)]>>>sorted_a=sorted(a)#按升序排列数字>>>[Number(val=1),Number(val=1),Number(val=2),数字(val=5),数字(val=6),数字(val=7),数字(val=7),数字(val=9),数字(val=10),数字(val=10)]>>>reverse_sorted_a=sorted(a,reverse=True)#按降序排列数字>>>reverse_sorted_a>>>[Number(val=10),Number(val=10),Number(val=9),数字(val=7),数字(val=7),数字(val=6),数字(val=5),数字(val=2),数字(val=1),数字(val=1)]Dataclass作为一个可调用的装饰器来定义所有dunder(LCTT译注:这里指的是双下划线方法,即magicmethod)方法并不总是值得的。您的用例可能只包含存储值和检查相等性。因此,您只需要定义__init__和__eq__方法。如果我们可以告诉装饰器不要生成其他方法,它将减少一些开销,并且我们可以对数据对象进行正确的操作。幸运的是,这可以通过将数据类装饰器作为可调用对象来实现。根据官方文档,装饰器可以用作可调用对象,其参数如下:@dataclass(init=True,repr=True,eq=True,order=False,unsafe_hash=False,frozen=False)classC:…init:__init__方法会默认生成。如果传入False,则该类将没有__init__方法。repr:默认生成__repr__方法。如果传入False,则该类将没有__repr__方法。eq:默认情况下会生成一个__eq__方法。如果传递了False,那么数据类将不会添加__eq__方法,而是默认为object.__eq__。order:__gt__,__ge__,__lt__,__le__方法将默认生成。如果传入False,则忽略它们。接下来我们将讨论冷冻。由于unsafe_hash参数的复杂用例,它值得单独写一篇文章。现在回到我们的用例,下面是我们需要的:1.__init__2.__eq__这些函数是默认生成的,所以我们需要的是不生成其他函数。那么我们该怎么做呢?很简单,只需将相关参数作为false传递给生成器即可。@dataclass(repr=False)#order,unsafe_hash和frozen都是FalseclassNumber:val:int=0>>>a=Number(1)>>>a>>><__main__.Numberobjectat0x7ff395afe898>>>>b=Number(2)>>>c=Number(1)>>>a==b>>>False>>>a>>Traceback(最近调用最后):文件“”,行1、inTypeError:'<'notsupportedbetweeninstancesof'Number'and'Number'Frozen(immutable)InstancesFrozen实例是对象,其属性在对象初始化后不能修改。无法创建真正不可变的Python对象在Python中创建对象的不可变属性是一项艰巨的任务,我不会在本文中深入探讨。以下是我们期望不可变对象执行的操作:>>>a=Number(10)#AssumingNumberclassisimmutable>>>a.val=10#RaisesErrorWithdataclass,youcanusethedataclassdecoratorasCallableobjectwith参数frozen=True定义一个冻结的对象。实例化冻结对象时,任何修改对象属性的尝试都会引发FrozenInstanceError。@dataclass(frozen=True)classNumber:val:int=0>>>a=Number(1)>>>a.值>>>1>>>a。val=2>>>Traceback(最近调用最后):文件“”,第1行,在文件“”,第3行,在__setattr__dataclasses.FrozenInstanceError:无法分配给字段“val”所以冻结实例是一个很好的方式来存储:常量设置这些通常不会在应用程序的生命周期内改变,并且应该禁止任何修改它们的尝试。初始化后处理使用dataclass,需要定义一个__init__方法来给self赋值变量。这个初始化操作已经处理过了。但是我们失去了在变量赋值后立即进行函数调用或处理的灵活性。让我们讨论一个用例,我们定义一个Float类来包含浮点数,然后在初始化后立即计算整数和小数部分。通常是这样的:importmathclassFloat:def__init__(self,val=0):self.val=valself.process()defprocess(self):self.decimal,self.integer=math.modf(self.val)>>>a=Float(2.2)>>>a.decimal>>>0.2000>>>a.integer>>>2.0幸运的是,已经使用post_init方法处理了后初始化。生成的__init__方法在返回之前调用__post_init__。因此,可以在函数中进行任何处理。importmath@dataclassclassFloatNumber:val:float=0.0def__post_init__(self):self.decimal,self.integer=math.modf(self.val)>>>a=Number(2.2)>>>a.val>>>2.2>>>a.integer>>>2.0>>>a.decimal>>>0.2多方便啊!继承数据类支持继承,就像普通的Python类一样。因此,父类中定义的属性将在子类中可用。@dataclassclassPerson:age:int=0name:str@dataclassclassStudent(Person):grade:int>>>s=Student(20,"JohnDoe",12)>>>s.age>>>20>>>s.name>>>"JohnDoe">>>s.grade>>>12请注意,Student的参数是类中定义的字段顺序。继承期间__post_init__的行为是什么?由于__post_init__只是另一个函数,因此必须以传统方式调用它:@dataclassclassA:a:intdef__post_init__(self):print("A")@dataclassclassB(A):b:intdef__post_init__(self):print("B")>>>a=B(1,2)>>>B在上面的例子中,只调用了B的__post_init__,那么我们如何调用A的__post_init__呢?既然是父类的函数,可以用super调用。@dataclassclassB(A):b:intdef__post_init__(self):super().__post_init__()#调用A的postinitprint("B")>>>a=B(1,2)>>>AB结论因此,这些是数据类使Python开发人员更轻松的几种方式。