-
-
Notifications
You must be signed in to change notification settings - Fork 374
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add unsafe_hash alias for class-wide hash
Fixes #1003
- Loading branch information
Showing
6 changed files
with
113 additions
and
92 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
# Hashing | ||
|
||
## Hash Method Generation | ||
|
||
:::{warning} | ||
The overarching theme is to never set the `@attrs.define(unsafe_hash=X)` parameter yourself. | ||
Leave it at `None` which means that *attrs* will do the right thing for you, depending on the other parameters: | ||
|
||
- If you want to make objects hashable by value: use `@define(frozen=True)`. | ||
- If you want hashing and equality by object identity: use `@define(eq=False)` | ||
|
||
Setting `unsafe_hash` yourself can have unexpected consequences so we recommend to tinker with it only if you know exactly what you're doing. | ||
::: | ||
|
||
Under certain circumstances, it's necessary for objects to be *hashable*. | ||
For example if you want to put them into a {class}`set` or if you want to use them as keys in a {class}`dict`. | ||
|
||
The *hash* of an object is an integer that represents the contents of an object. | ||
It can be obtained by calling `hash` on an object and is implemented by writing a `__hash__` method for your class. | ||
|
||
*attrs* will happily write a `__hash__` method for you [^fn1], however it will *not* do so by default. | ||
Because according to the [definition](https://docs.python.org/3/glossary.html#term-hashable) from the official Python docs, the returned hash has to fulfill certain constraints: | ||
|
||
[^fn1]: The hash is computed by hashing a tuple that consists of a unique id for the class plus all attribute values. | ||
|
||
1. Two objects that are equal, **must** have the same hash. | ||
This means that if `x == y`, it *must* follow that `hash(x) == hash(y)`. | ||
|
||
By default, Python classes are compared *and* hashed by their `id`. | ||
That means that every instance of a class has a different hash, no matter what attributes it carries. | ||
|
||
It follows that the moment you (or *attrs*) change the way equality is handled by implementing `__eq__` which is based on attribute values, this constraint is broken. | ||
For that reason Python 3 will make a class that has customized equality unhashable. | ||
Python 2 on the other hand will happily let you shoot your foot off. | ||
Unfortunately, *attrs* still mimics (otherwise unsupported) Python 2's behavior for backward compatibility reasons if you set `hash=False`. | ||
|
||
The *correct way* to achieve hashing by id is to set `@define(eq=False)`. | ||
Setting `@define(unsafe_hash=False)` (which implies `eq=True`) is almost certainly a *bug*. | ||
|
||
:::{warning} | ||
Be careful when subclassing! | ||
Setting `eq=False` on a class whose base class has a non-default `__hash__` method will *not* make *attrs* remove that `__hash__` for you. | ||
|
||
It is part of *attrs*'s philosophy to only *add* to classes so you have the freedom to customize your classes as you wish. | ||
So if you want to *get rid* of methods, you'll have to do it by hand. | ||
|
||
The easiest way to reset `__hash__` on a class is adding `__hash__ = object.__hash__` in the class body. | ||
::: | ||
|
||
2. If two objects are not equal, their hash **should** be different. | ||
|
||
While this isn't a requirement from a standpoint of correctness, sets and dicts become less effective if there are a lot of identical hashes. | ||
The worst case is when all objects have the same hash which turns a set into a list. | ||
|
||
3. The hash of an object **must not** change. | ||
|
||
If you create a class with `@define(frozen=True)` this is fulfilled by definition, therefore *attrs* will write a `__hash__` function for you automatically. | ||
You can also force it to write one with `hash=True` but then it's *your* responsibility to make sure that the object is not mutated. | ||
|
||
This point is the reason why mutable structures like lists, dictionaries, or sets aren't hashable while immutable ones like tuples or `frozenset`s are: | ||
point 1 and 2 require that the hash changes with the contents but point 3 forbids it. | ||
|
||
For a more thorough explanation of this topic, please refer to this blog post: [*Python Hashes and Equality*](https://hynek.me/articles/hashes-and-equality/). | ||
|
||
|
||
## Hashing and Mutability | ||
|
||
Changing any field involved in hash code computation after the first call to `__hash__` (typically this would be after its insertion into a hash-based collection) can result in silent bugs. | ||
Therefore, it is strongly recommended that hashable classes be `frozen`. | ||
Beware, however, that this is not a complete guarantee of safety: | ||
if a field points to an object and that object is mutated, the hash code may change, but `frozen` will not protect you. | ||
|
||
|
||
## Hash Code Caching | ||
|
||
Some objects have hash codes which are expensive to compute. | ||
If such objects are to be stored in hash-based collections, it can be useful to compute the hash codes only once and then store the result on the object to make future hash code requests fast. | ||
To enable caching of hash codes, pass `@define(cache_hash=True)`. | ||
This may only be done if *attrs* is already generating a hash function for the object. |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters