提升类编写效率的库——attr

attrs is the Python package that will bring back the joy of writing classes by relieving you from the drudgery of implementing object protocols (aka dunder methods).

attr可看作是一个类装饰器但又不仅仅是简单的装饰器,他使得我们编写类变得更加简单轻松。下面先通过一个简单的例子来见识下attr的强大吧。
现在我们需要编写一个类,有a,b,c两个属性,正常的写法:

class A(object):
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

看起来也没那么复杂嘛。好了,现在领导说类的打印输出不好看,又不能进行比较,行吧,有需求就加吧。

from functools import total_ordering

@total_ordering
class A(object):
    def __init__(self, a, b, c):
        self.a = a
        self.b = b

    def __repr__(self):
        return "ArtisanalClass(a={}, b={})".format(self.a, self.b)

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return NotImplemented
        return (self.a, self.b) == (other.a, other.b)

    def __lt__(self, other):
        if not isinstance(other, self.__class__):
            return NotImplemented
        return (self.a, self.b) < (other.a, other.b)

虽然看起来有点复杂,但是借助total_ordering,也算是以尽量少的代码实现了所需功能了吧。
但是,有没有更简洁的方式呢?让我们来看看借助于attr的实现:

import attr

@attr.s
class A(object):
    a = attr.ib()
    b = attr.ib()

有没有觉得,用attr,生活更清新?
下面我们来看看attr的一些常用的方式或API。

1、attr.s

attr.sattr库的核心API之一,他是一个类装饰器。他的参数如下:

attr.s(these=None, repr_ns=None, repr=True, cmp=True, 
       hash=None, init=True,slots=False, frozen=False, str=False)

在开头的例子中我们见到的借助于attr实现的类编写如此简洁,就是因为装饰器中的默认参数repr=Truecmp=True帮助我们实现了美化打印以及比较的功能。

1.1 these

these(dict of str to attr.ib())与其他的几个参数不同,这个参数是直接用来定义属性的,用的较少。

>>> class SomethingFromSomeoneElse(object):
...     def __init__(self, x):
...         self.x = x
>>> SomethingFromSomeoneElse = attr.s(
...     these={
...         "x": attr.ib()
...     }, init=False)(SomethingFromSomeoneElse)
>>> SomethingFromSomeoneElse(1)
SomethingFromSomeoneElse(x=1)
1.2 repr_ns

在Python2中无法__repr__无法正确显示出嵌套类,因此需要这个参数。在python3中则无此需求。

>>> @attr.s
... class C(object):
...     @attr.s(repr_ns="C")
...     class D(object):
...         pass
>>> C.D()
C.D()
1.3 repr

这里的__repr__参数与一般类中的__repr__函数功能相同,都是使类的打印更加美观易理解和debug。

1.4 str

与一般类中的__str__函数功能相同。

1.5 cmp

此参数为True时相当于创建了__eq__, __ne__, __lt__, __le__, __gt____ge__等方法。

1.6 hash

默认为None,此时__hash__方法由frozencmp,这两个参数决定。具体见http://www.attrs.org/en/stable/api.html

1.7 init

创造一个初始化方法,相当于__init__。除了在这里设置初始化外,还可使用__attrs_post_init__方法来初始化类,只是__attrs_post_init__方法是在__init__执行完之后执行的。

1.8 slots

相当于创建属性受限的类,相当于__slots__

>>> @attr.s(slots=True)
... class Coordinates(object):
...     x = attr.ib()
...     y = attr.ib()
...
>>> c = Coordinates(x=1, y=2)
>>> c.z = 3
Traceback (most recent call last):
    ...
AttributeError: 'Coordinates' object has no attribute 'z'
1.9 frozen

创造在实例化后不可变的类。若为True,则上面的__attrs_post_init__方法无法执行。可使用object.__setattr__(self, "attribution_name", value)改变,也可通过attr.evolve()方法进行改变。attr.evolve()相当于重新执行了一次init,因此对于那些init=False的参数是无法通过此方法进行改变的。

>>> @attr.s(frozen=True)
... class C(object):
...     x = attr.ib()
>>> i = C(1)
>>> i.x = 2
Traceback (most recent call last):
   ...
attr.exceptions.FrozenInstanceError: can't set attribute
>>> i.x
1

>>> @attr.s(frozen=True)
... class C(object):
...     x = attr.ib()
...     y = attr.ib()
>>> i1 = C(1, 2)
>>> i1
C(x=1, y=2)
>>> i2 = attr.evolve(i1, y=3)
>>> i2
C(x=1, y=3)
>>> i1 == i2
False
1.10 auto_attribs

使用注释属性:

>>> import typing
>>> @attr.s(auto_attribs=True)
... class AutoC:
...     cls_var: typing.ClassVar[int] = 5  # this one is ignored
...     l: typing.List[int] = attr.Factory(list)
...     x: int = 1
...     foo: str = attr.ib(
...          default="every attrib needs a type if auto_attribs=True"
...     )
...     bar: typing.Any = None
>>> attr.fields(AutoC).l.type
typing.List[int]
>>> attr.fields(AutoC).x.type
<class 'int'>
>>> attr.fields(AutoC).foo.type
<class 'str'>
>>> attr.fields(AutoC).bar.type
typing.Any
>>> AutoC()
AutoC(l=[], x=1, foo='every attrib needs a type if auto_attribs=True', bar=None)
>>> AutoC.cls_var
5

2、attr.ib

attr.s是类装饰器,而attr.ib则是快速对属性进行定义的方法。其中有很多参数与attr.s的参数重合,可以在这里对每个属性进行特殊设置。

attr.ib(default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, 
        convert=None, metadata=None, type=None, converter=None)
2.1 default

顾名思义,即为此属性的默认值。当无参数传入或无需传参或init=False时需要对此参数进行设置。

2.2 validator

validator是用来进行属性检验的的一个参数。attr提供了多种属性检验的方法。

# 使用装饰器进行属性检验
>>> @attr.s
... class C(object):
...     x = attr.ib()
...     @x.validator
...     def check(self, attribute, value):
...         if value > 42:
...             raise ValueError("x must be smaller or equal to 42")
>>> C(42)
C(x=42)
>>> C(43)
Traceback (most recent call last):
   ...
ValueError: x must be smaller or equal to 42

# 传入一个列表,需通过列表中所有的检验方为通过
>>> def x_smaller_than_y(instance, attribute, value):
...     if value >= instance.y:
...         raise ValueError("'x' has to be smaller than 'y'!")
>>> @attr.s
... class C(object):
...     x = attr.ib(validator=[attr.validators.instance_of(int),
...                            x_smaller_than_y])
...     y = attr.ib()
>>> C(x=3, y=4)
C(x=3, y=4)
>>> C(x=4, y=3)
Traceback (most recent call last):
   ...
ValueError: 'x' has to be smaller than 'y'!

# attr.validators.instance_of(type)
>>> @attr.s
... class C(object):
...     x = attr.ib(validator=attr.validators.instance_of(int))
>>> i = C(1)
>>> i.x = "1"
>>> attr.validate(i)
Traceback (most recent call last):
   ...
TypeError: ("'x' must be <type 'int'> (got '1' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, repr=True, cmp=True, hash=None, init=True, type=None), <type 'int'>, '1')

# attr.validators.optional(validator)
# validator (callable or list of callables.) – A validator (or a list of validators) that is used for non-`None` values. 
>>> @attr.s
... class C(object):
...     x = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(int)))
>>> C(42)
C(x=42)
>>> C("42")
Traceback (most recent call last):
   ...
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None), <type 'int'>, '42')
>>> C(None)
C(x=None)

# attr.validators.in_(options)
>>> import enum
>>> class State(enum.Enum):
...     ON = "on"
...     OFF = "off"
>>> @attr.s
... class C(object):
...     state = attr.ib(validator=attr.validators.in_(State))
...     val = attr.ib(validator=attr.validators.in_([1, 2, 3]))
>>> C(State.ON, 1)
C(state=<State.ON: 'on'>, val=1)
>>> C("on", 1)
Traceback (most recent call last):
   ...
ValueError: 'state' must be in <enum 'State'> (got 'on')
>>> C(State.ON, 4)
Traceback (most recent call last):
   ...
ValueError: 'val' must be in [1, 2, 3] (got 4)
2.3 repr

产生__repr__方法。

2.4 cmp

此参数为True时相当于创建了__eq__, __ne__, __lt__, __le__, __gt____ge__等方法。

2.5 hash

可用来进行对类的实例进行去重,功能可通过下面这个例子进行领会:

def __hash__(self):
    return hash((self.id, self.author_id, self.category_id, self.brand_id))

In : p1 = Product(1, 100001, 2003, 20, 1002393002, '这是一个测试商品1', 2000001, 100, None, 1)

In : p2 = Product(1, 100001, 2003, 20, 1002393002, '这是一个测试商品2', 2000001, 100, None, 2)

In : {p1, p2}
Out: {Product(id=1, author_id=100001, category_id=2003, brand_id=20)}
2.6 init

相当于__init__方法。

2.7 converter

实例化时传入的属性值转化为目的类型。需要注意的是这个参数是在validator参数检验之前进行的。

>>> @attr.s
... class C(object):
...     x = attr.ib(converter=int)
>>> o = C("1")
>>> o.x
1
2.8 metadata

元数据,使属性带有元数据,目前还不知道这个参数具体干嘛用的。

>>> @attr.s
... class C(object):
...    x = attr.ib(metadata={'my_metadata': 1})
>>> attr.fields(C).x.metadata
mappingproxy({'my_metadata': 1})
>>> attr.fields(C).x.metadata['my_metadata']
1
2.9 type

属性的类型?目前没发现用处。

3、attr.make_class

attr.make_class(name, attrs, bases=(<class 'object'>, ), **attributes_arguments)

这个函数可用来快速的创建类:

>>> C1 = attr.make_class("C1", ["x", "y"])
>>> C1(1, 2)
C1(x=1, y=2)
>>> C2 = attr.make_class("C2", {"x": attr.ib(default=42),
...                             "y": attr.ib(default=attr.Factory(list))})
>>> C2()
C2(x=42, y=[])

4、attr.fields(cls)

用来查看类的一些信息。

>>> @attr.s
... class C(object):
...     x = attr.ib()
...     y = attr.ib()
>>> attr.fields(C)
(Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None), Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None))
>>> attr.fields(C)[1]
Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None)
>>> attr.fields(C).y is attr.fields(C)[1]
True

5、attr.asdict

attr.asdict(inst, recurse=True, filter=None, dict_factory=<class 'dict'>, 
            retain_collection_types=False)

inst是实例化对象名字,recurse显示attrs装饰类的嵌套结构,filter表示选择性的显示哪些属性,dict_factory,字典类型,可设置为collections.OrderedDictretain_collection_types,当属性类型(type)为tuple或set时不转化为list。

# 通过匿名函数的方式筛选属性
>>> @attr.s
... class UserList(object):
...     users = attr.ib()
>>> @attr.s
... class User(object):
...     email = attr.ib()
...     password = attr.ib()
>>> attr.asdict(UserList([User("jane@doe.invalid", "s33kred"),
...                       User("joe@doe.invalid", "p4ssw0rd")]),
...             filter=lambda attr, value: attr.name != "password")
{'users': [{'email': 'jane@doe.invalid'}, {'email': 'joe@doe.invalid'}]}
# 通过exclude或include这两个函数来选择属性
>>> @attr.s
... class User(object):
...     login = attr.ib()
...     password = attr.ib()
...     id = attr.ib()
>>> attr.asdict(
...     User("jane", "s33kred", 42),
...     filter=attr.filters.exclude(attr.fields(User).password, int))
{'login': 'jane'}
>>> @attr.s
... class C(object):
...     x = attr.ib()
...     y = attr.ib()
...     z = attr.ib()
>>> attr.asdict(C("foo", "2", 3),
...             filter=attr.filters.include(int, attr.fields(C).x))
{'x': 'foo', 'z': 3}

6、attr.validate(inst)

在实例化后再次更改属性值时进行检验实例属性值的方法:

>>> @attr.s
... class C(object):
...     x = attr.ib(validator=attr.validators.instance_of(int))
>>> i = C(1)
>>> i.x = "1"
>>> attr.validate(i)
Traceback (most recent call last):
   ...
TypeError: ("'x' must be <type 'int'> (got '1' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, repr=True, cmp=True, hash=None, init=True, type=None), <type 'int'>, '1')

attr.set_run_validators(run), Set whether or not validators are run. By default, they are run.
attr.get_run_validators(), Return whether or not validators are run.

7、最后

Python3.7中新加入了一个标准库dataclass,作用与attr库类似。

参考:
attr官方文档
attrs 和 Python3.7 的 dataclasses
Python 程序员都该用的一个库

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容