Add support for default_factory on Struct types - #274
Merged
Conversation
Previously we allowed any msgspec-compatible value to be used as a
default value in a struct. When initialized, the default value would be
deepcopied (with some optimizations for common types) to ensure mutable
state wasn't shared. This was nice and readable (IMO), and let us avoid
implementing `default_factory` support. However, it also had a few
problems:
- Some custom types used as default values are effectively immutable
(e.g. UUIDs, ipaddress, ...). These shouldn't need to be deepcopied,
but there was no way to tell msgspec that.
- Deepcopying is expensive. We had optimizations for common cases (empty
mutable collections like `[]`, `{}`, ...), but the general case had a
large performance cost.
- This only supports "static" default values. Sometimes a user may want
to autogenerate a UUID for a field if one isn't provided, which isn't
possible with the current system.
All of these problems can be solved by dropping the current deepcopying
behavior and adding support for a configurable `default_factory` on
Struct fields. This commit only does the first half.
The new behavior has the following rules:
- Common empty mutable collections (`[]`, `{}`, `set()`, and
`bytearray()`) may be used directly as default values (as a shorthand
for `field(default_factory=list)`. This is purely syntactic sugar,
behind the scenes these are converted to `default_factory`.
- Using common *nonempty* mutable collections (list, dict, set, and
bytearray) as a default value is now an error. We can't check for all
mutable types, so we only try to provide error messages for common
mistakes. To handle these use cases the user should use a
`default_factory`, or switch to an immutable type.
- Using `frozen` struct instances as default values is allowed.
- Using non-frozen struct instances as default values is now an error.
To handle these use cases the user should use a `default_factory`, or
set `frozen=True`.
- Every other type used as a default value is used directly (meaning we
assume they're immutable values).
An added benefit of these changes is that `Struct.__init__` with default
values now has less overhead (although it was already fast).
This moves `msgspec.inspect.UNSET` to `msgspec.UNSET` (leaving the original import as well). It also moves the singleton implementation to C, to make it easier to work with when adding a new `field` construct in a follow-up commit.
This adds a new `msgspec.field` function, which returns an opaque config
object for configuring fields in a `msgspec.Struct` type. In the future
this will support more config options, but for now it only supports:
- `default` (the same as providing a default value directly)
- `default_factory` (a 0-argument callable for generating a default
value at `__init__` time.
Example:
```python
import msgspec
import uuid
class Test(msgspec.Struct):
id: uuid.UUID = msgspec.field(default_factory=uuid.uuid4)
```
jcrist
force-pushed
the
default-factory
branch
from
January 23, 2023 07:45
484f247 to
b5cb86e
Compare
Member
Author
|
A quick benchmark of |
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR does a few things:
msgspec.fieldfunction, which returns an opaque object used for configuring fields. So far this only exposesdefaultanddefault_factory, both of which match thedataclasseskwargs of the same name.msgspec.Structtypes, through use of the newdefault_factorysetting (fixes Support default factories for Struct types #259).Example:
Breaking Change: mutable default values in structs are now handled differently
This also changes how mutable default values are handled. Previously msgspec's semantics were that a default value for a field would be deepcopied on
__init__if that field wasn't provided (the implementation would avoid a deepcopy for most common types, but semantically this was the same). This proved problematic in the face of custom types, led to some unnecessary slowdowns, and didn't match the behavior of other similar libraries likedataclassesorattrs. Since we now can support arbitrarily complex default values through thedefault_factoryfunction, we drop the olddeepcopybehavior.A default value on a
Structnow has the following rules:default_factoryinstead.[],{},set(),bytearray()) are accepted as default values. These are treated as syntactic sugar, and are automatically converted to the equivalentdefault_factorynotation ([]->field(default_factory=list)).default_factoryinstead.Since most mutable default values are empty collections, I don't expect this breaking change to affect most users. To reiterate, the following type definition is still valid:
while this one will error, and should use a
default_factoryinstead:Todo: