Python程序员都该用的一个库

本文的作者来自知名 Python 库 Twisted 开发团队,首先举例说明了在 Python 中定义类是多么的麻烦,然后给出了自己的解决方案:attrs 库。从介绍来看,确实方便很多。

你写 Python 程序吗?那你应该使用 attrs。

你问为什么?我只能说,不要问,直接用就好了。

好吧,我还是解释一下。

我热爱 Python,这十多年来一直是我的主力编程语言。尽管期间也出现过一些有意思的语言(指的是 Haskell 和 Rust),但我还不打算换到其他语言。

这不是说 Python 没有本身没有任何问题。在某些情况下,Python 会让你更容易犯错。尤其是一些库大量使用类继承,以及 God-object 反面模式。

导致该情况的一个原因可能是 Python 是一种非常方便的语言,所以经验欠缺的程序员犯错误后,他们就得继续忍受下去。

但我想,更重要的原因也许是,有时你努力做正确的事,但 Python 却会因此惩罚你。

在对象设计的大背景下,“正确的事“是指设计体量小并且独立的类,只做一件事,并且把这件事做好。例如,如果你的对象开始累积大量的私有方法,也许你应该将它们变成私有属性的公有方法。但是,这种事处理起来非常乏味,你可能就不会理会这些。

如果你有一些相关的数据,而且数据之间的关系和行为是需要进行解释的,那么应该定义为对象。在 Python 中定义元组和列表非常方便。刚开始把 address = ... 写成 host, port = ... ,可能觉得没什么关系,但很快你就会到处写 [(family, socktype, proto, canonname, sockaddr)] = ... 这样的语句,这时就该后悔了。这还是算你走运的情况。如果倒霉的话,你可能得维护 values[0][7][4][HOSTNAME][“canonical”] 这样的代码,这时你的心情是痛苦,而不仅仅是后悔了。



这就提出了一个问题:在 Python 中使用类是否是麻烦?我们来看一个简单的数据结构:一个三维直角坐标。从最简单的开始:


  1. class Point3D(object): 

到现在为止还挺好。我们已经有了一个三维点。 接下来呢?


  1. class Point3D(object): 
  2.     def __init__(self, x, y, z): 

其实,这是有点可惜。我只想对数据的打包,但却不得不覆盖一个 Python 运行时中的特殊方法,而且命名还是约定俗成的。但还不算太坏;毕竟所有的编程语言都是按照某种形式组成的怪异符号而已。

至少可以看到属性名了,还能说得通。


  1. class Point3D(object): 
  2.     def __init__(self, x, y, z): 
  3.         self.x 

我已经说过,我想一个 x,但现在必须把它指定为一个属性...


  1. class Point3D(object): 
  2.     def __init__(self, x, y, z): 
  3.         self.x = x 

绑定到 x ?呃,很明显...


  1. class Point3D(object): 
  2.     def __init__(self, x, y, z): 
  3.         self.x = x 
  4.         self.y = y 
  5.         self.z = z 

每个属性都得这么做一次,所以这相当糟糕?每个属性名都得敲 3 次?!?

好吧。至少定义完了。


  1. class Point3D(object): 
  2.     def __init__(self, x, y, z): 
  3.         self.x = x 
  4.         self.y = y 
  5.         self.z = z 
  6.     def __repr__(self): 

什么,难道还没结束吗?


  1. class Point3D(object): 
  2.     def __init__(self, x, y, z): 
  3.         self.x = x 
  4.         self.y = y 
  5.         self.z = z 
  6.     def __repr__(self): 
  7.         return (self.__class__.__name__ + 
  8.                 ("(x={}, y={}, z={})".format(self.x, self.y, self.z))) 

拜托。现在我得每个属性名敲 5 次了,如果我想在调试时知道属性到底指的是什么的话。如果定义元组的话,就不用这一步了?!?!?


  1. class Point3D(object): 
  2.     def __init__(self, x, y, z): 
  3.         self.x = x 
  4.         self.y = y 
  5.         self.z = z 
  6.     def __repr__(self): 
  7.         return (self.__class__.__name__ + 
  8.                 ("(x={}, y={}, z={})".format(self.x, self.y, self.z))) 
  9.     def __eq__(self, other): 
  10.         if not isinstance(other, self.__class__): 
  11.             return NotImplemented 
  12.         return (self.x, self.y, self.z) == (other.x, other.y, other.z) 

敲 7 次?!?!?!?


  1. class Point3D(object): 
  2.     def __init__(self, x, y, z): 
  3.         self.x = x 
  4.         self.y = y 
  5.         self.z = z 
  6.     def __repr__(self): 
  7.         return (self.__class__.__name__ + 
  8.                 ("(x={}, y={}, z={})".format(self.x, self.y, self.z))) 
  9.     def __eq__(self, other): 
  10.         if not isinstance(other, self.__class__): 
  11.             return NotImplemented 
  12.         return (self.x, self.y, self.z) == (other.x, other.y, other.z) 
  13.     def __lt__(self, other): 
  14.         if not isinstance(other, self.__class__): 
  15.             return NotImplemented 
  16.         return (self.x, self.y, self.z) < (other.x, other.y, other.z) 

敲 9 次?!?!?!?!?


  1. from functools import total_ordering 
  2. @total_ordering 
  3. class Point3D(object): 
  4.     def __init__(self, x, y, z): 
  5.         self.x = x 
  6.         self.y = y 
  7.         self.z = z 
  8.     def __repr__(self): 
  9.         return (self.__class__.__name__ + 
  10.                 ("(x={}, y={}, z={})".format(self.x, self.y, self.z))) 
  11.     def __eq__(self, other): 
  12.         if not isinstance(other, self.__class__): 
  13.             return NotImplemented 
  14.         return (self.x, self.y, self.z) == (other.x, other.y, other.z) 
  15.     def __lt__(self, other): 
  16.         if not isinstance(other, self.__class__): 
  17.             return NotImplemented 
  18.         return (self.x, self.y, self.z) < (other.x, other.y, other.z) 

好了,擦汗 - 尽管多了 2 行代码不是很好,但至少现在我们不用定义其他比较方法了。现在一切搞定了,对吧?


  1. from unittest import TestCase 
  2. class Point3DTests(TestCase): 

你知道吗? 我受够了。一个类码了 20 行,却还什么事都没做;我们这样做是想解四元方程,而不是定义“可以打印和比较的数据结构”。我陷入了大量无用的垃圾元组、列表和字典中;用 Python 定义合适的数据结构是非常麻烦的

命名元组 namedtuple

为解决这个难题,标准库给出的解决方案是使用 namedtuple 。然而不幸的是初稿(在许多方面与我自己的处理方式有相似的尴尬的和过时之处)namedtuple 仍然无法挽救这个现象。它引入了大量没有必要的公共函数,这对于兼容性维护来说简直就是一场噩梦,并且它连问题的一半都没有解决。这种做法的缺陷太多了,这里只列一些重点:

  • 不管你是否希望如此,它的字段都可以通过数字索引的方式访问。这意味你不能有私有属性,因为所有属性通过公开的 __getitem__ 接口暴露出来。
  • 它等同于有相同值的原始元组,因此很容易发生类型混乱,特别是如果你想避免使用元组和列表。
  • 这是一个元组,所以它总是不可变的。

至于最后一点,你可以像这样使用:


  1. Point3D = namedtuple('Point3D', ['x', 'y', 'z']) 

在这种情况下它看起来并不像一种类;无特殊情况下,简单的语法分析工具将不能识别它为类。但是这样你不能给它添加任何其他方法,因为没有地方放任何的方法。更别提你必须输入类的名字两次。

或者你可以使用继承:


  1. class Point3D(namedtuple('_Point3DBase', 'x y z'.split())): 
  2.     pass 

尽管这样可以添加方法和文档字符串,看起来也像一个类,但是内部名称(在 repr 中显示的内容,并不是类的真实名称)变的很怪了。同时,你还不知不觉中把没列出的属性变成了可变的,这是添加 class 声明的一个奇怪的副作用;除非你在类主体中添加 __slots__='X Y z'.split(),但这样又回到了每个属性名必须敲两次的情况。

而且,我们还没提科学已经证明不应该使用继承呢。

因此,如果你只能选命名元组,那就选命名元组吧,也算是改进,虽然只是在部分情况下如此。

使用 attrs

这时该我最喜欢的 Python 库出场了。

pip install attrs

我们重新审视一下上述问题。如何使用 attrs 库编写 Point3D


  1. import attr 
  2. @attr.s 

由于它还没有内置到 Python 中,所以必须用以上 2 行开始:导入包然后使用类装饰器。


  1. import attr 
  2. @attr.s 
  3. class Point3D(object): 

你看,没有继承!通过使用类装饰器,Point3D 仍然是一个普通的 Python 类(尽管我们一会会看到一些双下划线方法)。


  1. import attr 
  2. @attr.s 
  3. class Point3D(object): 
  4.     x = attr.ib() 

添加属性 x


  1. import attr 
  2. @attr.s 
  3. class Point3D(object): 
  4.     x = attr.ib() 
  5.     y = attr.ib() 
  6.     z = attr.ib() 

再分别添加属性 yz。这样就完成了。

这就 OK 了? 等等。不用定义字符串表示吗?


  1. >>> Point3D(1, 2, 3) 
  2. Point3D(x=1, y=2, z=3) 

怎么进行比较?


  1. >>> Point3D(1, 2, 3) == Point3D(1, 2, 3) 
  2. True 
  3. >>> Point3D(3, 2, 1) == Point3D(1, 2, 3) 
  4. False 
  5. >>> Point3D(3, 2, 3) > Point3D(1, 2, 3) 
  6. True 

好的。但如果我想将有明确属性定义的数据提取为适合 JSON 序列化的格式呢?


  1. >>> attr.asdict(Point3D(1, 2, 3)) 
  2. {'y': 2, 'x': 1, 'z': 3} 

也许上边有一点点准确。即使如此,因为使用了 attrs 后,很多事情都变得更简单了,它允许你在类上声明字段,以及相关的元数据。


  1. pprint 
  2. >>> pprint.pprint(attr.fields(Point3D)) 
  3. (Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None), 
  4.  Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None), 
  5.  Attribute(name='z', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None)) 

我不打算在这里深入介绍 attrs 的每一个有趣的功能;你可以阅读它的文档。另外,项目会经常更新,每隔一段时间都会有新的东西出现,因此我也可能会漏掉一些重要的功能。但是用上 attrs 之后 ,你会发现它所做的正式此前 Python 所缺乏的:

  1. 它让你简洁地定义类型,而不是通过手动键入 def __init __ 的方式来定义。
  2. 它让你直接地说出你声明的意思,而不是拐弯抹角的表达它。与其这样说:“我有一个类型,它被称为 MyType ,它有一个构造函数,在构造函数中用参数 'A' 给属性 'A' 赋值”,而是应该这样说:“我有一个类型,它被称为 MyType ,它有一个属性叫做 a,以及跟它相关的方法“,而不必通过逆向工程猜测它的方法(例如,在一个实例中运行 dir ,或查看 self.__ class__. __dict__)。
  3. 它提供了有用的默认方法,而不像 Python 中的默认行为有时有用,大部分时候没用。
  4. 它从简单的开始,但是提供了后续添加更严谨实现的空间。

我们详细说明最后一点。

逐步改善

虽然我不打算谈及每一个功能,但如果我没有提到以下几个特点,那我就太不负责任了。你可以从上面这些特别长的 Attributerepr() 中看到一些有趣的东西。

例如:你通过用 @attr.s 修饰类来验证属性。比如:Point3D 这个类,应该包含数字。为简单起见,我们可以说这些数字为 float 类型,像这样:


  1. import attr 
  2. from attr.validators import instance_of 
  3. @attr.s 
  4. class Point3D(object): 
  5.     x = attr.ib(validator=instance_of(float)) 
  6.     y = attr.ib(validator=instance_of(float)) 
  7.     z = attr.ib(validator=instance_of(float)) 

因为我们使用了 attrs ,这意味着之后有机会进行验证:可以只给每个需要的属性添加类型信息。其中的一些功能,可以让我们避免常见的错误。例如,这是一个很常见的“找 Bug” 面试题:


  1. class Bag: 
  2.     def __init__(self, contents=[]): 
  3.         self._contents = contents 
  4.     def add(self, something): 
  5.         self._contents.append(something) 
  6.     def get(self): 
  7.         return self._contents[:] 

修正它,正确的代码应该是这个样子:


  1. class Bag: 
  2.     def __init__(self, contents=None): 
  3.         if contents is None: 
  4.             contents = [] 
  5.         self._contents = contents 

额外添加了 2 行代码。

这样,contents 无意间就成了全局变量,这使得所有没有提供列表的 Bag 对象都共享一个列表。使用 attrs 的话,就变成这样:


  1. @attr.s 
  2. class Bag: 
  3.     _contents = attr.ib(default=attr.Factory(list)) 
  4.     def add(self, something): 
  5.         self._contents.append(something) 
  6.     def get(self): 
  7.         return self._contents[:] 

attrs 还提供一些其他的特性,让你在构建类时更方便更正确。另一个很好的例子?如果你严格的管控对象的属性(或在内存使用上更有效率的 CPython ),你可以在类层级上使用 slots=True - 例如 @attr.s(slots=True) - 自动与 attrs 声明的 __slots__属性匹配。所有这些功能会让通过 attr.ib() 声明的属性更好更强大。

未来的 Python

有人为以后能普遍使用 Python 3 编程而感到高兴。而我期待的是,能够在 Python 编程时一直用attrs。就我所知,它对每个使用了的代码库都产生了积极、微妙的影响。

试试看:你可能会惊讶地发现,以前用不方便写文档的元组、列表或字典的地方,现在可以使用具备清晰解释的类了。既然编写结构清晰的类型如此简单方便,以后应该会经常使用 attrs 的。这对你的代码来说是件好事;我就是一个好例子。

作者:linkcheng

来源:51CTO

时间: 2024-12-24 08:26:31

Python程序员都该用的一个库的相关文章

Python 程序员都会喜欢的 6 个库

在编程时,小挫折可能与大难题一样令人痛苦.没人希望在费劲心思之后,只是做到弹出消息窗口或是快速写入数据库.因此,程序员都会喜欢那些能够快速处理这些问题,同时长远来看也很健壮的解决方案. 下面这6个Python库既可以快速解决眼前的棘手问题,同时也能够作为大型项目的基础. Pyglet 是什么:Pyglet是一个纯Python语言编写的跨平台框架,用于开发多媒体和窗口特效应用. 为什么需要它:从头开发图形界面应用所需要的功能模块是十分繁琐的,Pyglet提供了大量现成的模块,省去了很多的时间:窗口

每一个程序员都应该知道的高并发处理技巧、创业公司如何解决高并发问题、互联网高并发问题解决思路、caoz大神多年经验总结分享

原文:每一个程序员都应该知道的高并发处理技巧.创业公司如何解决高并发问题.互联网高并发问题解决思路.caoz大神多年经验总结分享 本文来源于caoz梦呓公众号高并发专辑,以图形化.松耦合的方式,对互联网高并发问题做了详细解读与分析,"技术在短期内被高估,而在长期中又被低估",而不同的场景和人员成本又导致了巨头的方案可能并不适合创业公司,那么如何保证高并发问题不成为创业路上的拦路虎,是每一个全栈工程师.资深系统工程师.有理想的程序员必备的技能,希望本文助您寻找属于自己的"成金之

顽石互动CEO吴刚:程序员都有一个创业梦想

投资界11月3日消息,2011中国http://www.aliyun.com/zixun/aggregation/36233.html">移动开发者大会今日在京召开,顽石互动董事长兼CEO吴刚认为,节奏感掌握是一个公司特别重要的环节,很多公司因为一个市场大机会的来临,经常让自己顶上去马上抓这样的机会,往往在这样的前提下很多公司把自己本身节奏感打破掉.他还预计,到了2013年.3721.html">2014年,移动互联网游戏可能会跟今天的PC网游市场相似的. 以下为演讲实录

Python 程序员最常犯的十个错误

常见错误1:错误地将表达式作为函数的默认参数 在Python中,我们可以为函数的某个参数设置默认值,使该参数成为可选参数.虽然这是一个很好的语言特性,但是当默认值是可变类型时,也会导致一些令人困惑的情况.我们来看看下面这个Python函数定义: >>> def foo(bar=[]):        # bar是可选参数,如果没有提供bar的值,则默认为[],  ...    bar.append("baz")    # 但是稍后我们会看到这行代码会出现问题.  ..

Python 程序员必知必会的开发者工具

Python已经演化出了一个广泛的生态系统,该生态系统能够让Python程序员的生活变得更加简单,减少他们重复造轮的工作.同样的理念也适用于工具开发者的工作,即便他们开发出的工具并没有出现在最终的程序中.本文将介绍Python程序员必知必会的开发者工具. 对于开发者来说,最实用的帮助莫过于帮助他们编写代码文档了.pydoc模块可以根据源代码中的docstrings为任何可导入模块生成格式良好的文档.Python包含了两个测试框架来自动测试代码以及验证代码的正确性:1)doctest模块,该模块可

程序员都不读书,但你应该读

问答网站stackoverflow.com的一个主要功能体现就是:软件开发人员无需再从书本上学习编程,就像Joel所说的: 程序员看起来都不再读书.市场上编程方面书籍的数量和编程从业人数相比来少的可怜. 2004年在<The Shlemiel Way of Software>一书中Joel也表达了相同的观点: 大部分的人都不读点什么或写点什么.大部分的程序员都不读软件开发方面的书籍,他们不去软件开发方面的网站,他们不去Slashdot参与讨论. 既然现在的程序员都不读书,他们如何学习编程?他们

为什么程序员都是夜猫子 电脑屏幕惹的祸?

一种很流行的说法是,程序员是把咖啡因转化成程序代码的机器. 说的是实情,随便问一个程序员,问他什么时候工作最有状态,估计他很有可能说是深夜.有人稍微早一点,有人更晚.有一种流行的趋势是凌晨4点起床,在破晓之前这段时间里做一些事情.而另一些人喜欢凌晨4点才睡觉. 所有这些的主要目的是躲避打搅.但是你把自己反锁在屋里不就行了?为什么对夜晚情有独钟? 我想,这事归纳下来有3点:工人的时间表,疲倦的大脑和明亮的电脑屏幕. 工人的时间表 Paul Graham 在2009年写了一篇关于 工人的时间表的文章

程序员都应该知道的130个vim命令

 从1970年开始,vi和vim 就成为了程序员最喜爱的文本编辑器之一.5年前,我写了一个问自己名为"每个程序员都应该知道的100个vim 命令" 这次算是之前那篇文章的改进版,希望你会喜欢. 基础 :e filename Openfilenamefor edition :w Save file :q Exit Vim :q! Quit without saving :x Write file (if changes has been made) and exit :sav filen

每个程序员都应该知道的基础数论

这篇文章讨论了数论中每个程序员都应该知道的几个重要概念.本文的内容既不是对数论的入门介绍,也不是针对数论中任何特定算法的讨论,而只是想要做为数论的一篇参考.如果读者想要获取关于数论的更多细节,文中也提供了一些外部的参考文献(大多数来自于 Wikipedia 和 Wolfram ). 0. 皮亚诺公理 整个算术规则都是建立在 5 个基本公理基础之上的,这 5 个基本公理被称为皮亚诺公理.皮亚诺公理定义了自然数所具有的特性,具体如下: 0是自然数; 每个自然数都有一个后续自然数; 0不是任何自然数的