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

所有Python程序员都应该使用的库

时间:2023-03-19 17:23:49 科技观察

本文作者来自知名Python库Twisted开发团队。他首先举例说明在Python中定义类是多么的麻烦,然后给出了自己的解决方案:attrs库。从介绍来看,确实方便了很多。你会写Python程序吗?那么你应该使用attrs。你为什么要问?我只能说,别问了,直接用就行了。好吧,让我解释一下。我喜欢Python,十多年来它一直是我的主力编程语言。虽然中间出现了一些有趣的语言(我指的是Haskell和Rust),但我还不打算切换到其他语言。这并不是说Python没有自己的问题。在某些情况下,Python使您更容易犯错误。特别是一些库大量使用类继承和上帝对象反模式。一个原因可能是Python是一种非常方便的语言,所以当没有经验的程序员犯错误时,他们只能忍受它。但我认为更重要的原因是,有时你试图做正确的事情,而Python会因此惩罚你。在对象设计的上下文中,“正确的事情”指的是设计小而独立的类,它们只做一件事并且把它做好。例如,如果您的对象开始积累大量私有方法,也许您应该将它们设为具有私有属性的公共方法。不过,这种事情处理起来很繁琐,你可能不会理会这些。如果你有一些相关的数据,并且需要解释数据之间的关系和行为,那么应该将它定义为一个对象。在Python中定义元组和列表非常方便。一开始写address=...ashost,port=...,你可能觉得无所谓,但很快你就会写成[(family,socktype,proto,canonname,sockaddr)]=...这样的言论比比皆是,那你就该后悔了。那还是看运气了。如果你运气不好,你可能不得不维护像values[0][7][4][HOSTNAME]["canonical"]这样的代码,你会感到痛苦,而不仅仅是后悔。这就提出了一个问题:Python中的类很麻烦吗?让我们看一个简单的数据结构:三维笛卡尔坐标。从最简单的开始:classPoint3D(object):到目前为止一切顺利。我们已经有了一个3D点。下一步是什么?classPoint3D(object):def__init__(self,x,y,z):其实这个有点可惜。只是想把数据打包,但是在Python运行时要重写一个特殊的方法,而且命名还是很约定俗成的。但这还不错;毕竟,所有的编程语言都只是以某种形式组织起来的怪异符号。至少你能看到属性名,而且是有道理的。classPoint3D(object):def__init__(self,x,y,z):self.x我已经说过我想要一个x,但现在我必须将它指定为一个属性...classPoint3D(object):def__init__(self,x,y,z):self.x=x绑定到x?好吧,显然...classPoint3D(object):def__init__(self,x,y,z):self.x=xself.y=yself.z=z必须为每个属性执行一次,所以这很糟糕?每个属性名称必须输入3次?!?好的。至少它是定义的。classPoint3D(object):def__init__(self,x,y,z):self.x=xself.y=yself.z=zdef__repr__(self):什么,还没完?classPoint3D(object):def__init__(self,x,y,z):self.x=xself.y=yself.z=zdef__repr__(self):return(self.__class__.__name__+("(x={},y={},z={})".format(self.x,self.y,self.z)))请。现在,如果我想在调试时知道属性指的是什么,我必须将每个属性名称键入5次。如果确定义元组的话,就不用这一步了?!?!?classPoint3D(object):def__init__(self,x,y,z):self.x=xself.y=yself.z=zdef__repr__(self):return(self.__class__.__name__+("(x={},y={},z={})".format(self.x,self.y,self.z)))def__eq__(self,other):ifnotisinstance(other,self.__class__):returnNotImplementedreturn(self.x,self.y,self.z)==(other.x,other.y,other.z)敲7次?!?!?!?classPoint3D(object):def__init__(self,x,y,z):self.x=xself.y=yself.z=zdef__repr__(self):return(self.__class__.__name__+("(x={},y={},z={})".format(self.x,self.y,self.z)))def__eq__(self,other):ifnotisinstance(other,self.__class__):returnNotImplementedreturn(self.x,self.y,self.z)==(other.x,other.y,other.z)def__lt__(self,other):ifnotisinstance(other,self.__class__):returnNotImplementedreturn(self.x,self.y,self.z)<(other.x,other.y,other.z)敲9次?!?!?!?!?fromfunctoolsimporttotal_ordering@total_orderingclassPoint3D(object):def__init__(self,x,y,z):self.x=xself.y=yself.z=zdef__repr__(self):返回(self.__class__.__name__+("(x={},y={},z={})".format(self.x,self.y,self.z)))def__eq__(self,other):ifnotisinstance(other,self.__class__):returnNotImplementedreturn(self.x,self.y,self.z)==(other.x,other.y,other.z)def__lt__(self,other):ifnotisinstance(other,self.__class__)):returnNotImplementedreturn(self.x,self.y,self.z)<(other.x,other.y,other.z)好吧,努力吧——多2行代码不是很好,但至少现在是这样我们不定义其他比较方法现在我们完成了,对吧?fromunittestimportTestCaseclassPoint3DTests(TestCase):你知道吗?我受够了。一个20行代码的类,还没有做任何事情;我们正在尝试求解四元数方程,而不是定义“可以打印和比较的数据结构”。我陷入了大量无用的垃圾元组、列表和字典中;在Python中定义合适的数据结构很麻烦。命名元组namedtuple为了解决这个问题,标准库给出的解决方案是使用namedtuple。不幸的是,namedtuple的初稿(在很多方面与我自己的方法类似,在很多方面都很笨拙和过时)仍然无法挽救这种现象。引入了很多不必要的公共函数,是兼容性维护的噩梦,解决不了一半的问题。这种方式有太多的缺陷,这里有一些关键点:无论你是否愿意,它的字段都可以通过数字索引访问。这意味着您不能拥有私有属性,因为所有属性都通过公共__getitem__接口公开。它相当于一个具有相同值的原始元组,因此很容易发生类型混淆,特别是如果你想避免使用元组和列表。这是一个元组,所以它总是不可变的。至于最后一点,您可以这样使用它:Point3D=namedtuple('Point3D',['x','y','z'])在这种情况下,它看起来不像一个类;没有什么特殊情况,简单的解析器工具不会将其识别为一个类。但是这样你就不能给它添加任何其他的方法,因为没有地方可以放任何方法。更不用说您必须输入两次类名。或者你可以使用继承:classPoint3D(namedtuple('_Point3DBase','xyz'.split())):pass虽然这样增加了方法和docstrings,但它看起来也像一个类,但是内部名称(在repr内容中显示,不是班级的真实姓名)变得很奇怪。此外,您在不知不觉中使未列出的属性可变,这是添加类声明的奇怪副作用;除非您在类body中添加__slots__='XYz'.split(),但这可以追溯到必须键入每个属性名称两次。而且,我们没有提到科学已经证明不应该使用继承。因此,如果只能选择namedtuples,就选择namedtuples,这也是一种改进,虽然只是在某些情况下。使用attrs这是我最喜欢的Python库发挥作用的地方。pipinstallattrs让我们重新审视上面的问题。如何使用attrs库编写Point3D?importattr@attr.s必须从上面两行开始,因为它还没有内置到Python中:导入包然后使用类装饰器。importattr@attr.sclassPoint3D(object):你看,没有继承!通过使用类装饰器,Point3D仍然是一个普通的Python类(尽管我们稍后会看到一些双下划线方法)。importattr@attr.sclassPoint3D(object):x=attr.ib()添加属性x。importattr@attr.sclassPoint3D(object):x=attr.ib()y=attr.ib()z=attr.ib()分别添加属性y和z。这个做完了。没关系?ETC。您不需要定义字符串表示形式吗?如何比较>>>Point3D(1,2,3)Point3D(x=1,y=2,z=3)?>>>Point3D(1,2,3)==Point3D(1,2,3)True>>>Point3D(3,2,1)==Point3D(1,2,3)False>>>Point3D(3,2,3)>Point3D(1,2,3)True但是,如果我想将具有明确定义属性的数据提取为适合JSON序列化的格式怎么办?>>>attr.asdict(Point3D(1,2,3)){'y':2,'x':1,'z':3}上面的可能更准确一点。即便如此,很多事情都因为attrs而变得更容易,它允许您在类上声明字段以及关联的元数据。pprint>>>pprint.pprint(attr.fields(Point3D))(Attribute(name='x',default=NOTHING,validator=None,repr=True,cmp=True,hash=True,init=True,convert=无),属性(名称='y',默认=无,验证器=无,repr=True,cmp=True,散列=真,init=True,转换=无),属性(名称='z',默认=NOTHING,validator=None,repr=True,cmp=True,hash=True,init=True,convert=None))我不打算在这里讨论attrs的每一个有趣的特性;你可以阅读它的文档。另外,项目会经常更新,每隔一段时间就会出现新的东西,所以我也可能会错过一些重要的功能。但是在使用attrs之后,你会发现它做的正是Python以前所缺乏的:它让你可以简洁地定义类型,而不是手动输入def__init__。它可以让你直接说出你的陈述的意思,而不是四舍五入。不要说:“我有一个名为MyType的类型,它有一个构造函数,该构造函数使用参数‘A’为属性‘A’赋值”,你应该说:“我有一个类型,它叫做MyType,它有一个属性称为a,以及与之关联的方法”,而不必通过逆向工程猜测其方法(例如,在实例上运行dir,或查看self.__class__.__dict__)。它提供了有用的默认方法,这与Python中的默认行为不同,后者有时很有用,但大多数情况下没有。它开始很简单,但为以后添加更严格的实现留出了空间。我们详细说明最后一点。渐进式改进虽然我不打算涵盖每一个功能,但如果我不提及以下功能,那就是我的失职。你可以在上面这些特别长的属性的repr()中看到一些有趣的东西。示例:您通过使用@attr.s装饰类来验证属性。例如:Point3D类应包含数字。为简单起见,我们可以说这些数字是float类型的,如下所示:(float))z=attr.ib(validator=instance_of(float))因为我们使用了attrs,这意味着以后有机会进行验证:您可以只向每个必需的属性添加类型信息。其中一些功能使我们能够避免常见的错误。例如,这是一个非常常见的“bug发现”面试问题:classBag:def__init__(self,contents=[]):self._contents=contentsdefadd(self,something):self._contents.append(something)defget(self):returnself._contents[:]修复它,正确的代码应该是这样的:classBag:def__init__(self,contents=None):ifcontentsisNone:contents=[]self._contents=contents添加了2行额外的代码。这样一来,contents无意中变成了一个全局变量,导致所有不提供列表的Bag对象共享一个列表。如果你使用attrs,它会变成这样:returnsself._contents[:]attrs还提供了一些其他的特性,让你的类构建更加方便和正确。另一个很好的例子?如果您严格控制对象的属性(或者CPython在内存使用方面更高效),您可以在类级别使用slots=True-例如@attr.s(slots=True)-自动与attrs声明的__slots__属性匹配。所有这些特性使通过attr.ib()声明的属性更好、更强大。Python的未来有些人普遍对Python3编程的未来感到高兴。而我最期待的是在用Python编程的时候能够一直使用attrs。据我所知,它对其使用的每个代码库都产生了积极而微妙的影响。尝试一下:您可能会惊讶地发现,您现在可以使用带有清晰解释的类,而以前使用的是未记录的元组、列表或字典。由于编写结构良好的类型非常简单方便,因此将来应该经常使用attrs。这对您的代码来说是件好事;我的就是一个很好的例子。本译文由PythonTG翻译团队制作,译者:linkcheng,校对:EarlGrey。