Skip to content

functools.cached_property support? #204

Closed

Description

@scarf005
from msgspec import Struct
from functools import cached_property

class Foo(Struct):
	bar: tuple[str]

	@cached_property
	def bar_inner(self) -> str:
		return self.bar[0]

will fail because Struct does not implement __dict__. could it be possible to implement cached_property for Struct?

Traceback (most recent call last):
  File "/home/scarf/repo/cata/tileset-tools/learnpillow.py", line 26, in <module>
    print(meta.fallback.ascii)
          ^^^^^^^^^^^^^
  File "/home/scarf/.asdf/installs/python/3.11.0rc2/lib/python3.11/functools.py", line 994, in __get__
    raise TypeError(msg) from None
TypeError: No '__dict__' attribute on 'TileConfig' instance to cache 'fallback' property.

@property decorator does work, but would be nice to have a cached one too (if possible)

Activity

  1. jcrist commented on Jan 26, 2023

    @jcrist
    Member

    Apologies for the late reply here. msgspec.Struct types will never work with the builtin functools.cached_property decorator, since that relies on a __dict__ and structs have no __dict__ allocated. But we could provide our own equivalent utility. This is currently blocked by #199; once that's in there'd be two ways we could spell this:

    Option 1

    from msgspec import Struct
    from msgspec.structs import cached_property
    
    class Foo(Struct):
        bar: tuple[str]
        _bar_inner: str
    
        # This version of cached_property requires:
        # - explicitly annotating a new private field to store the cached value (_bar_inner here)
        # - passing that field name to the decorator
        @cached_property("_bar_inner")
        def bar_inner(self) -> str:
            return self.bar[0]

    Option 2

    from msgspec import Struct
    from msgspec.structs import cached_property
    
    class Foo(Struct):
        bar: tuple[str]
    
        # This version of cached_property automatically allocates
        # the slot and marks it as private, no extra configuration needed
        @cached_property
        def bar_inner(self) -> str:
            return self.bar[0]

    Option 1 is nice because once #199 is resolved, that version can be implemented in pure Python. The downside is it's more verbose and error prone to use.

    Option 2 is nice because its usage is identical to that of functools.cached_property, except it would work on struct types. The downside is that it would require integration with the Struct metaclass to work properly. More work, but a slightly nicer API.

    I'm not sure how often a cached_property decorator would be used with a msgspec.Struct type. I've never really needed a feature like this, but my needs aren't reflective of everyone. If the need is extremely rare, then I'd lean towards the first one as it's easier to do. If the need is less rare, then it may be worth doing some upfront work here in msgspec to make user's code simpler. Need to find the right balance.

  2. jcrist commented on Feb 17, 2023

    @jcrist
    Member

    Actually, thinking further I think we could just add an option to re-enable __dict__ access on struct types. So you'd declare your type as:

    from msgspec import Struct
    from functools import cached_property
    
    class Foo(Struct, dict=True):  # probably a better option name
        bar: tuple[str]
    
        @cached_property
        def bar_inner(self) -> str:
            return self.bar[0]

    Since this is a more general issue I've opened #324 to track it.

  3. jcrist commented on Feb 24, 2023

    @jcrist
    Member

    This is now supported by setting dict=True on your struct class definition:

    In [1]: from msgspec import Struct
       ...: from functools import cached_property
       ...:                                        
       ...: class Foo(Struct, dict=True):
       ...:     bar: tuple[str]
       ...:                                        
       ...:     @cached_property
       ...:     def bar_inner(self) -> str:
       ...:         return self.bar[0]
       ...:                                        
    
    In [2]: f = Foo(("one", "two"))
    
    In [3]: f.bar_inner                            
    Out[3]: 'one'                                                           
    
    In [4]: f.__dict__
    Out[4]: {'bar_inner': 'one'}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions