| # *attrs* by Example | |
| ## Basics | |
| The simplest possible usage is: | |
| ```{doctest} | |
| >>> from attrs import define, field | |
| >>> @define | |
| ... class Empty: | |
| ... pass | |
| >>> Empty() | |
| Empty() | |
| >>> Empty() == Empty() | |
| True | |
| >>> Empty() is Empty() | |
| False | |
| ``` | |
| So in other words: *attrs* is useful even without actual {term}`fields <field>`! | |
| But you'll usually want some data on your classes, so let's add some: | |
| ```{doctest} | |
| >>> @define | |
| ... class Coordinates: | |
| ... x: int | |
| ... y: int | |
| ``` | |
| By default, all features are added, so you immediately have a fully functional data class with a nice `repr` string and comparison methods. | |
| ```{doctest} | |
| >>> c1 = Coordinates(1, 2) | |
| >>> c1 | |
| Coordinates(x=1, y=2) | |
| >>> c2 = Coordinates(x=2, y=1) | |
| >>> c2 | |
| Coordinates(x=2, y=1) | |
| >>> c1 == c2 | |
| False | |
| ``` | |
| As shown, the generated `__init__` method allows for both positional and keyword arguments. | |
| --- | |
| Unlike Data Classes, *attrs* doesn't force you to use type annotations. | |
| So, the previous example could also have been written as: | |
| ```{doctest} | |
| >>> @define | |
| ... class Coordinates: | |
| ... x = field() | |
| ... y = field() | |
| >>> Coordinates(1, 2) | |
| Coordinates(x=1, y=2) | |
| ``` | |
| :::{caution} | |
| If a class body contains a field that is defined using {func}`attrs.field` (or {func}`attr.ib`), but **lacks a type annotation**, *attrs* switches to a no-typing mode and ignores fields that have type annotations but are not defined using {func}`attrs.field` (or {func}`attr.ib`). | |
| ::: | |
| --- | |
| For private attributes, *attrs* will strip the leading underscores for keyword arguments: | |
| ```{doctest} | |
| >>> @define | |
| ... class C: | |
| ... _x: int | |
| >>> C(x=1) | |
| C(_x=1) | |
| ``` | |
| If you want to initialize your private attributes yourself, you can do that too: | |
| ```{doctest} | |
| >>> @define | |
| ... class C: | |
| ... _x: int = field(init=False, default=42) | |
| >>> C() | |
| C(_x=42) | |
| >>> C(23) | |
| Traceback (most recent call last): | |
| ... | |
| TypeError: __init__() takes exactly 1 argument (2 given) | |
| ``` | |
| If you prefer to expose your privates, you can use keyword argument aliases: | |
| ```{doctest} | |
| >>> @define | |
| ... class C: | |
| ... _x: int = field(alias="_x") | |
| >>> C(_x=1) | |
| C(_x=1) | |
| ``` | |
| An additional way of defining attributes is supported too. | |
| This is useful in times when you want to enhance classes that are not yours (nice `__repr__` for Django models anyone?): | |
| ```{doctest} | |
| >>> class SomethingFromSomeoneElse: | |
| ... def __init__(self, x): | |
| ... self.x = x | |
| >>> SomethingFromSomeoneElse = define( | |
| ... these={ | |
| ... "x": field() | |
| ... }, init=False)(SomethingFromSomeoneElse) | |
| >>> SomethingFromSomeoneElse(1) | |
| SomethingFromSomeoneElse(x=1) | |
| ``` | |
| [Subclassing is bad for you](https://www.youtube.com/watch?v=3MNVP9-hglc) (except when doing [strict specialization](https://hynek.me/articles/python-subclassing-redux/)), but *attrs* will still do what you'd hope for: | |
| ```{doctest} | |
| >>> @define(slots=False) | |
| ... class A: | |
| ... a: int | |
| ... def get_a(self): | |
| ... return self.a | |
| >>> @define(slots=False) | |
| ... class B: | |
| ... b: int | |
| >>> @define(slots=False) | |
| ... class C(B, A): | |
| ... c: int | |
| >>> i = C(1, 2, 3) | |
| >>> i | |
| C(a=1, b=2, c=3) | |
| >>> i == C(1, 2, 3) | |
| True | |
| >>> i.get_a() | |
| 1 | |
| ``` | |
| {term}`Slotted classes <slotted classes>`, which are the default for the new APIs, don't play well with multiple inheritance so we don't use them in the example. | |
| The order of the attributes is defined by the [MRO](https://www.python.org/download/releases/2.3/mro/). | |
| ### Keyword-only Attributes | |
| You can also add [keyword-only](https://docs.python.org/3/glossary.html#keyword-only-parameter) attributes: | |
| ```{doctest} | |
| >>> @define | |
| ... class A: | |
| ... a: int = field(kw_only=True) | |
| >>> A() | |
| Traceback (most recent call last): | |
| ... | |
| TypeError: A() missing 1 required keyword-only argument: 'a' | |
| >>> A(a=1) | |
| A(a=1) | |
| ``` | |
| `kw_only` may also be specified at decorator level, and will apply to all attributes: | |
| ```{doctest} | |
| >>> @define(kw_only=True) | |
| ... class A: | |
| ... a: int | |
| ... b: int | |
| >>> A(1, 2) | |
| Traceback (most recent call last): | |
| ... | |
| TypeError: __init__() takes 1 positional argument but 3 were given | |
| >>> A(a=1, b=2) | |
| A(a=1, b=2) | |
| ``` | |
| If you create an attribute with `init=False`, the `kw_only` argument is ignored. | |
| Keyword-only attributes allow subclasses to add attributes without default values, even if the base class defines attributes with default values: | |
| ```{doctest} | |
| >>> @define | |
| ... class A: | |
| ... a: int = 0 | |
| >>> @define | |
| ... class B(A): | |
| ... b: int = field(kw_only=True) | |
| >>> B(b=1) | |
| B(a=0, b=1) | |
| >>> B() | |
| Traceback (most recent call last): | |
| ... | |
| TypeError: B() missing 1 required keyword-only argument: 'b' | |
| ``` | |
| If you don't set `kw_only=True`, then there is no valid attribute ordering, and you'll get an error: | |
| ```{doctest} | |
| >>> @define | |
| ... class A: | |
| ... a: int = 0 | |
| >>> @define | |
| ... class B(A): | |
| ... b: int | |
| Traceback (most recent call last): | |
| ... | |
| ValueError: No mandatory attributes allowed after an attribute with a default value or factory. Attribute in question: Attribute(name='b', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, converter=None, metadata=mappingproxy({}), type=int, kw_only=False) | |
| ``` | |
| (asdict)= | |
| ## Converting to Collections Types | |
| When you have a class with data, it often is very convenient to transform that class into a {class}`dict` (for example if you want to serialize it to JSON): | |
| ```{doctest} | |
| >>> from attrs import asdict | |
| >>> asdict(Coordinates(x=1, y=2)) | |
| {'x': 1, 'y': 2} | |
| ``` | |
| Some fields cannot or should not be transformed. | |
| For that, {func}`attrs.asdict` offers a callback that decides whether an attribute should be included: | |
| ```{doctest} | |
| >>> @define | |
| ... class User: | |
| ... email: str | |
| ... password: str | |
| >>> @define | |
| ... class UserList: | |
| ... users: list[User] | |
| >>> 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'}]} | |
| ``` | |
| For the common case where you want to [`include`](attrs.filters.include) or [`exclude`](attrs.filters.exclude) certain types, string name or attributes, *attrs* ships with a few helpers: | |
| ```{doctest} | |
| >>> from attrs import asdict, filters, fields | |
| >>> @define | |
| ... class User: | |
| ... login: str | |
| ... password: str | |
| ... email: str | |
| ... id: int | |
| >>> asdict( | |
| ... User("jane", "s33kred", "jane@example.com", 42), | |
| ... filter=filters.exclude(fields(User).password, "email", int)) | |
| {'login': 'jane'} | |
| >>> @define | |
| ... class C: | |
| ... x: str | |
| ... y: str | |
| ... z: int | |
| >>> asdict(C("foo", "2", 3), | |
| ... filter=filters.include(int, fields(C).x)) | |
| {'x': 'foo', 'z': 3} | |
| >>> asdict(C("foo", "2", 3), | |
| ... filter=filters.include(fields(C).x, "z")) | |
| {'x': 'foo', 'z': 3} | |
| ``` | |
| :::{note} | |
| Though using string names directly is convenient, mistyping attribute names will silently do the wrong thing and neither Python nor your type checker can help you. | |
| {func}`attrs.fields()` will raise an `AttributeError` when the field doesn't exist while literal string names won't. | |
| Using {func}`attrs.fields()` to get attributes is worth being recommended in most cases. | |
| ```{doctest} | |
| >>> asdict( | |
| ... User("jane", "s33kred", "jane@example.com", 42), | |
| ... filter=filters.exclude("passwd") | |
| ... ) | |
| {'login': 'jane', 'password': 's33kred', 'email': 'jane@example.com', 'id': 42} | |
| >>> asdict( | |
| ... User("jane", "s33kred", "jane@example.com", 42), | |
| ... filter=fields(User).passwd | |
| ... ) | |
| Traceback (most recent call last): | |
| ... | |
| AttributeError: 'UserAttributes' object has no attribute 'passwd'. Did you mean: 'password'? | |
| ``` | |
| ::: | |
| Other times, all you want is a tuple and *attrs* won't let you down: | |
| ```{doctest} | |
| >>> import sqlite3 | |
| >>> from attrs import astuple | |
| >>> @define | |
| ... class Foo: | |
| ... a: int | |
| ... b: int | |
| >>> foo = Foo(2, 3) | |
| >>> with sqlite3.connect(":memory:") as conn: | |
| ... c = conn.cursor() | |
| ... c.execute("CREATE TABLE foo (x INTEGER PRIMARY KEY ASC, y)") #doctest: +ELLIPSIS | |
| ... c.execute("INSERT INTO foo VALUES (?, ?)", astuple(foo)) #doctest: +ELLIPSIS | |
| ... foo2 = Foo(*c.execute("SELECT x, y FROM foo").fetchone()) | |
| <sqlite3.Cursor object at ...> | |
| <sqlite3.Cursor object at ...> | |
| >>> foo == foo2 | |
| True | |
| ``` | |
| For more advanced transformations and conversions, we recommend you look at a companion library (such as [*cattrs*](https://catt.rs/)). | |
| ## Defaults | |
| Sometimes you want to have default values for your initializer. | |
| And sometimes you even want mutable objects as default values (ever accidentally used `def f(arg=[])`?). | |
| *attrs* has you covered in both cases: | |
| ```{doctest} | |
| >>> import collections | |
| >>> @define | |
| ... class Connection: | |
| ... socket: int | |
| ... @classmethod | |
| ... def connect(cls, db_string): | |
| ... # ... connect somehow to db_string ... | |
| ... return cls(socket=42) | |
| >>> @define | |
| ... class ConnectionPool: | |
| ... db_string: str | |
| ... pool: collections.deque = Factory(collections.deque) | |
| ... debug: bool = False | |
| ... def get_connection(self): | |
| ... try: | |
| ... return self.pool.pop() | |
| ... except IndexError: | |
| ... if self.debug: | |
| ... print("New connection!") | |
| ... return Connection.connect(self.db_string) | |
| ... def free_connection(self, conn): | |
| ... if self.debug: | |
| ... print("Connection returned!") | |
| ... self.pool.appendleft(conn) | |
| ... | |
| >>> cp = ConnectionPool("postgres://localhost") | |
| >>> cp | |
| ConnectionPool(db_string='postgres://localhost', pool=deque([]), debug=False) | |
| >>> conn = cp.get_connection() | |
| >>> conn | |
| Connection(socket=42) | |
| >>> cp.free_connection(conn) | |
| >>> cp | |
| ConnectionPool(db_string='postgres://localhost', pool=deque([Connection(socket=42)]), debug=False) | |
| ``` | |
| More information on why class methods for constructing objects are awesome can be found in this insightful [blog post](https://web.archive.org/web/20210130220433/http://as.ynchrono.us/2014/12/asynchronous-object-initialization.html). | |
| Default factories can also be set using the `factory` argument to {func}`~attrs.field`, and using a decorator. | |
| The method receives the partially initialized instance which enables you to base a default value on other attributes: | |
| ```{doctest} | |
| >>> @define | |
| ... class C: | |
| ... x: int = 1 | |
| ... y: int = field() | |
| ... @y.default | |
| ... def _any_name_except_a_name_of_an_attribute(self): | |
| ... return self.x + 1 | |
| ... z: list = field(factory=list) | |
| >>> C() | |
| C(x=1, y=2, z=[]) | |
| ``` | |
| Please keep in mind that the decorator approach *only* works if the attribute in question has a {func}`~attrs.field` assigned to it. | |
| As a result, annotating an attribute with a type is *not* enough if you use `@default`. | |
| (examples-validators)= | |
| ## Validators | |
| Although your initializers should do as little as possible (ideally: just initialize your instance according to the arguments!), it can come in handy to do some kind of validation on the arguments. | |
| *attrs* offers two ways to define validators for each attribute and it's up to you to choose which one suits your style and project better. | |
| You can use a decorator: | |
| ```{doctest} | |
| >>> @define | |
| ... class C: | |
| ... x: int = field() | |
| ... @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 | |
| ``` | |
| ...or a callable... | |
| ```{doctest} | |
| >>> from attrs import validators | |
| >>> def x_smaller_than_y(instance, attribute, value): | |
| ... if value >= instance.y: | |
| ... raise ValueError("'x' has to be smaller than 'y'!") | |
| >>> @define | |
| ... class C: | |
| ... x: int = field(validator=[validators.instance_of(int), | |
| ... x_smaller_than_y]) | |
| ... y: int | |
| >>> 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'! | |
| ``` | |
| ...or both at once: | |
| ```{doctest} | |
| >>> @define | |
| ... class C: | |
| ... x: int = field(validator=validators.instance_of(int)) | |
| ... @x.validator | |
| ... def fits_byte(self, attribute, value): | |
| ... if not 0 <= value < 256: | |
| ... raise ValueError("value out of bounds") | |
| >>> C(128) | |
| C(x=128) | |
| >>> C("128") | |
| Traceback (most recent call last): | |
| ... | |
| TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=int, converter=None, kw_only=False), <class 'int'>, '128') | |
| >>> C(256) | |
| Traceback (most recent call last): | |
| ... | |
| ValueError: value out of bounds | |
| ``` | |
| Please note that the decorator approach only works if -- and only if! -- the attribute in question has a {func}`~attrs.field` assigned. | |
| Therefore if you use `@validator`, it is *not* enough to annotate said attribute with a type. | |
| *attrs* ships with a bunch of validators, make sure to [check them out](api-validators) before writing your own: | |
| ```{doctest} | |
| >>> @define | |
| ... class C: | |
| ... x: int = field(validator=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, factory=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None, kw_only=False), <type 'int'>, '42') | |
| ``` | |
| If using the old-school {func}`attr.s` decorator, validators only run on initialization by default. | |
| If using the newer {func}`attrs.define` and friends, validators run on initialization *and* on attribute setting. | |
| This behavior can be changed using the *on_setattr* argument. | |
| Check out {ref}`validators` for more details. | |
| ## Conversion | |
| Attributes can have a `converter` function specified, which will be called with the attribute's passed-in value to get a new value to use. | |
| This can be useful for doing type-conversions on values that you don't want to force your callers to do. | |
| ```{doctest} | |
| >>> @define | |
| ... class C: | |
| ... x: int = field(converter=int) | |
| >>> o = C("1") | |
| >>> o.x | |
| 1 | |
| >>> o.x = "2" | |
| >>> o.x | |
| 2 | |
| ``` | |
| If using the old-school {func}`attr.s` decorator, converters only run on initialization by default. | |
| If using the newer {func}`attrs.define` and friends, converters run on initialization *and* on attribute setting. | |
| This behavior can be changed using the *on_setattr* argument. | |
| Check out {ref}`converters` for more details. | |
| ## Running Custom Code Before/After Setting Attributes | |
| If your `__setattr__` hook needs to run custom code before **and** after setting attributes, you can pass a generator to the `on_setattr` argument: | |
| ```{doctest} | |
| >>> from typing import Generator | |
| >>> def gen_hook(instance: Any, attribute: attr.Attribute[int], value: Any) -> Generator[int]: | |
| ... print(f"value coming in: {value!r}") | |
| ... new_value = int(value) * 2 + 286 | |
| ... yield new_value | |
| ... print(f"set {attribute.name} to {new_value!r}") | |
| >>> @define | |
| ... class C: | |
| ... x: int = field(on_setattr=gen_hook) | |
| >>> c = C(42) # doesn't run on __init__ | |
| >>> c.x = "67" | |
| value coming in: '67' | |
| set x to 420 | |
| >>> c.x | |
| 420 | |
| ``` | |
| (metadata)= | |
| ## Metadata | |
| All *attrs* attributes may include arbitrary metadata in the form of a read-only dictionary. | |
| ```{doctest} | |
| >>> from attrs import fields | |
| >>> @define | |
| ... class C: | |
| ... x = field(metadata={'my_metadata': 1}) | |
| >>> fields(C).x.metadata | |
| mappingproxy({'my_metadata': 1}) | |
| >>> fields(C).x.metadata['my_metadata'] | |
| 1 | |
| ``` | |
| Metadata is not used by *attrs*, and is meant to enable rich functionality in third-party libraries. | |
| The metadata dictionary follows the normal dictionary rules: | |
| Keys need to be hashable, and both keys and values are recommended to be immutable. | |
| If you're the author of a third-party library with *attrs* integration, please see [*Extending Metadata*](extending-metadata). | |
| ## Types | |
| *attrs* also allows you to associate a type with an attribute using either the *type* argument to using {pep}`526`-annotations or {func}`attrs.field`/{func}`attr.ib`: | |
| ```{doctest} | |
| >>> @define | |
| ... class C: | |
| ... x: int | |
| >>> fields(C).x.type | |
| <class 'int'> | |
| >>> import attr | |
| >>> @attr.s | |
| ... class C: | |
| ... x = attr.ib(type=int) | |
| >>> fields(C).x.type | |
| <class 'int'> | |
| ``` | |
| If you don't mind annotating *all* attributes, you can even drop the `attrs.field` and assign default values instead: | |
| ```{doctest} | |
| >>> import typing | |
| >>> @define | |
| ... class AutoC: | |
| ... cls_var: typing.ClassVar[int] = 5 # this one is ignored | |
| ... l: list[int] = Factory(list) | |
| ... x: int = 1 | |
| ... foo: str = "every attrib needs a type if auto_attribs=True" | |
| ... bar: typing.Any = None | |
| >>> fields(AutoC).l.type | |
| list[int] | |
| >>> fields(AutoC).x.type | |
| <class 'int'> | |
| >>> fields(AutoC).foo.type | |
| <class 'str'> | |
| >>> 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 | |
| ``` | |
| The generated `__init__` method will have an attribute called `__annotations__` that contains this type information. | |
| If your annotations contain strings (for example, forward references), | |
| you can resolve these after all references have been defined by using {func}`attrs.resolve_types`. | |
| This will replace the *type* attribute in the respective fields. | |
| ```{doctest} | |
| >>> from attrs import resolve_types | |
| >>> @define | |
| ... class A: | |
| ... a: 'list[A]' | |
| ... b: 'B' | |
| ... | |
| >>> @define | |
| ... class B: | |
| ... a: A | |
| ... | |
| >>> fields(A).a.type | |
| 'list[A]' | |
| >>> fields(A).b.type | |
| 'B' | |
| >>> resolve_types(A, globals(), locals()) | |
| <class 'A'> | |
| >>> fields(A).a.type | |
| list[A] | |
| >>> fields(A).b.type | |
| <class 'B'> | |
| ``` | |
| :::{note} | |
| If you find yourself using string type annotations to handle forward references, wrap the entire type annotation in quotes instead of only the type you need a forward reference to (so `'list[A]'` instead of `list['A']`). | |
| This is a limitation of the Python typing system. | |
| ::: | |
| :::{warning} | |
| *attrs* itself doesn't have any features that work on top of type metadata. | |
| However it's useful for writing your own validators or serialization frameworks. | |
| ::: | |
| ## Slots | |
| {term}`Slotted classes <slotted classes>` have several advantages on CPython. | |
| Defining `__slots__` by hand is tedious, in *attrs* it's just a matter of using {func}`attrs.define` or passing `slots=True` to {func}`attr.s`: | |
| ```{doctest} | |
| >>> @define | |
| ... class Coordinates: | |
| ... x: int | |
| ... y: int | |
| >>> import attr | |
| >>> @attr.s(slots=True) | |
| ... class Coordinates: | |
| ... x: int | |
| ... y: int | |
| ``` | |
| {func}`~attrs.define` sets `slots=True` by default. | |
| ## Immutability | |
| Sometimes you have instances that shouldn't be changed after instantiation. | |
| Immutability is especially popular in functional programming and is generally a very good thing. | |
| If you'd like to enforce it, *attrs* will try to help: | |
| ```{doctest} | |
| >>> from attrs import frozen | |
| >>> @frozen | |
| ... class C: | |
| ... x: int | |
| >>> i = C(1) | |
| >>> i.x = 2 | |
| Traceback (most recent call last): | |
| ... | |
| attrs.exceptions.FrozenInstanceError: can't set attribute | |
| >>> i.x | |
| 1 | |
| ``` | |
| Please note that true immutability is impossible in Python but it will [get](how-frozen) you 99% there. | |
| By themselves, immutable classes are useful for long-lived objects that should never change; like configurations for example. | |
| In order to use them in regular program flow, you'll need a way to easily create new instances with changed attributes. | |
| In Clojure that function is called [*assoc*](https://clojuredocs.org/clojure.core/assoc) and *attrs* shamelessly imitates it: {func}`attrs.evolve`: | |
| ```{doctest} | |
| >>> from attrs import evolve, frozen | |
| >>> @frozen | |
| ... class C: | |
| ... x: int | |
| ... y: int | |
| >>> i1 = C(1, 2) | |
| >>> i1 | |
| C(x=1, y=2) | |
| >>> i2 = evolve(i1, y=3) | |
| >>> i2 | |
| C(x=1, y=3) | |
| >>> i1 == i2 | |
| False | |
| ``` | |
| On Python 3.13 and later, you can also use {func}`copy.replace` from the standard library: | |
| ```{doctest} | |
| >>> import copy | |
| >>> @frozen | |
| ... class C: | |
| ... x: int | |
| ... y: int | |
| >>> i = C(1, 2) | |
| >>> copy.replace(i, y=3) | |
| C(x=1, y=3) | |
| ``` | |
| ## Other Goodies | |
| When building systems that have something resembling a plugin interface, you may want to have a registry of all classes that implement a certain interface: | |
| ```{doctest} | |
| >>> REGISTRY = [] | |
| >>> class Base: # does NOT have to be an attrs class! | |
| ... @classmethod | |
| ... def __attrs_init_subclass__(cls): | |
| ... REGISTRY.append(cls) | |
| >>> @define | |
| ... class Impl(Base): | |
| ... pass | |
| >>> REGISTRY | |
| [<class 'Impl'>] | |
| ``` | |
| Sometimes you may want to create a class programmatically. | |
| *attrs* gives you {func}`attrs.make_class` for that: | |
| ```{doctest} | |
| >>> from attrs import make_class | |
| >>> @define | |
| ... class C1: | |
| ... x = field(type=int) | |
| ... y = field() | |
| >>> C2 = make_class("C2", {"x": field(type=int), "y": field()}) | |
| >>> fields(C1) == fields(C2) | |
| True | |
| >>> fields(C2).x.type | |
| <class 'int'> | |
| ``` | |
| You can still have power over the attributes if you pass a dictionary of name: {func}`~attrs.field` mappings and can pass the same arguments as you can to `@attrs.define`: | |
| ```{doctest} | |
| >>> C = make_class("C", {"x": field(default=42), | |
| ... "y": field(default=Factory(list))}, | |
| ... repr=False) | |
| >>> i = C() | |
| >>> i # no repr added! | |
| <__main__.C object at ...> | |
| >>> i.x | |
| 42 | |
| >>> i.y | |
| [] | |
| ``` | |
| If you need to dynamically make a class with {func}`~attrs.make_class` and it needs to be a subclass of something else than {class}`object`, use the `bases` argument: | |
| ```{doctest} | |
| >>> class D: | |
| ... def __eq__(self, other): | |
| ... return True # arbitrary example | |
| >>> C = make_class("C", {}, bases=(D,), cmp=False) | |
| >>> isinstance(C(), D) | |
| True | |
| ``` | |
| Sometimes, you want to have your class's `__init__` method do more than just | |
| the initialization, validation, etc. that gets done for you automatically when | |
| using `@define`. | |
| To do this, just define a `__attrs_post_init__` method in your class. | |
| It will get called at the end of the generated `__init__` method. | |
| ```{doctest} | |
| >>> @define | |
| ... class C: | |
| ... x: int | |
| ... y: int | |
| ... z: int = field(init=False) | |
| ... | |
| ... def __attrs_post_init__(self): | |
| ... self.z = self.x + self.y | |
| >>> obj = C(x=1, y=2) | |
| >>> obj | |
| C(x=1, y=2, z=3) | |
| ``` | |
| You can exclude single attributes from certain methods: | |
| ```{doctest} | |
| >>> @define | |
| ... class C: | |
| ... user: str | |
| ... password: str = field(repr=False) | |
| >>> C("me", "s3kr3t") | |
| C(user='me') | |
| ``` | |
| Alternatively, to influence how the generated `__repr__()` method formats a specific attribute, specify a custom callable to be used instead of the `repr()` built-in function: | |
| ```{doctest} | |
| >>> @define | |
| ... class C: | |
| ... user: str | |
| ... password: str = field(repr=lambda value: '***') | |
| >>> C("me", "s3kr3t") | |
| C(user='me', password=***) | |
| ``` |