-
Notifications
You must be signed in to change notification settings - Fork 74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Why is Tensorclass implemented as a decorator? #663
Comments
Hey thanks for proposing this! We thought about that initially, but there are a couple of reasons that made us go for the Note that we could implement a class & metaclass that implement the isinstance if that makes things easier? from tensordict import tensorclass, is_tensorclass
import torch
class SomeClassMeta(type):
def __instancecheck__(self, instance):
if is_tensorclass(instance):
return True
return False
def __subclasscheck__(self, subclass):
if is_tensorclass(subclass):
return True
return False
class TensorClass(metaclass=SomeClassMeta):
pass
@tensorclass
class MyDC:
x: torch.Tensor
c = MyDC(1, batch_size=[])
assert isinstance(c, TensorClass)
assert issubclass(type(c), TensorClass) That only partially fixes your issues like type checking though. I'm personally not a huge fan of dataclass but I understand why people like them. I think that most of the time it has to do with typing (i.e., the content is more explicit than with a dictionary). Your solution still offers that so I wouldn't put it aside straight away, but if we consider this we must make sure that it won't harm adoption of the feature for the target audience (ie, users who are accustomed to @DataClass decorator). RE this point
I don't see how that relates to the four points you raised about the benefits of subclassing. To me cc @shagunsodhani @apbard @tcbegley who may have some other thoughts to share on this |
Thanks for your swift reply @vmoens! This clears up my main questions on the design choices made. While I did not intend to advocate immediate alterations to the status quo, I am curious to learn what the intended role of tensorclasses is exactly. From the user perspective, the main reasons listed for using a tensorclass come from the keys being explicitly defined. In that case, weighting the library design in a way that prefers users' code aesthetics (i.e. being similar to Second, was a solution that specifically addresses typing a la RE:
It is my perception that 'it not inheriting from anything' in this context is more relevant to dataclasses than it is to tensorclasses. I would argue:
Consequently, two tensorclasses will have a larger degree of shared functionality than two dataclasses. This is difficult to define in an exact manner, though. RE:
It does indeed. Let me further elaborate on why I identify this property of init signatures. It is mostly related to problematic typing:
|
Thank you for your comments, a lot of good points there. To provide some context on typing: tensordict, like pytorch, is not strongly typed. We do not currently use mypy as part of our CI and it's unlikely that we will in the future. Proposed SolutionYour suggestion of Given this, we would find ourselves in the following scenarios based on user requirements:
Would this status quo be acceptable to you? Is the dual usage (as a decorator or subclass) even desirable? It's worth noting that offering two methods to achieve the same result can lead to confusion. Many users are currently familiar with using We now need to evaluate the feasibility and implications of these options... |
Hey @kurt-stolle I was wondering if you had thoughts about this? #1067 >>> from typing import Any
>>> import torch
>>> from tensordict import TensorClass
>>> class Foo(TensorClass):
... tensor: torch.Tensor
... non_tensor: Any
... nested: Any = None
>>> foo = Foo(tensor=torch.randn(3), non_tensor="a string!", nested=None, batch_size=[3])
>>> print(foo)
Foo(
non_tensor=NonTensorData(data=a string!, batch_size=torch.Size([3]), device=None),
tensor=Tensor(shape=torch.Size([3]), device=cpu, dtype=torch.float32, is_shared=False),
nested=None,
batch_size=torch.Size([3]),
device=None,
is_shared=False) The only problem I see atm is that autocomplete doesn't really work (you can have either the tensordict args or the dataclass fields but can't make both). |
I am not sure how to replicate autocomplete behavior in interactive python, as my experience is limited there. I am also using Python 3.10, however. I suppose it would work if you remove the Practically, the _: KW_ONLY
batch_size: Sequence[int] | torch.Size | int | None = None
device: DeviceType | None = None
names: Sequence[str] | None = None
non_blocking: bool | None = None
lock: bool = False These fields represent the keyword arguments passed to the constructor. This has the added side-effect that I am a bit limited in time currently, but if desired I could investigate this and submit an alternative PR to #1067 if I manage to replicate and address the autocomplete issue you desribe. |
Upon further inspection, I also see that |
Same here, I was just using that to quickly experiment autocompletion. Writing a script isn't different though.
I'm happy with anything as long as existing tests pass!
Yeah I tried all combinations (around TensorClass, around the metaclass and around the class in the stub file) and none helped with autocomplete... By the way, how do you get |
I will submit a PR once I find some time to have a closer look.
That does not work! I see that I made an error while cleaning an excerpt from my own code. It has been corrected. |
Current Tensorclass interface
To define a Tensorclass, we could write:
The
@tensorclass
decorator then generates a dataclass-like container class, adding various methods to implement (parts of) theTensorDictBase
abstraction. Notice that unlike dataclasses, where the__init__
signature may entirely be inferred from the class attribute annotations, the current decorator also augments__init__
with extra keyword argumentsbatch_size
,device
andnames
.Considering that the decorator has significant (side-)effects to the resulting class, I am confused on why the API has been defined in this way. In this issue, I ask for clarification on this design choice.
Simplified interface (conceptual)
Specifically, would a simplified representation using subclassing not suffice? For example, consider the following notation:
The
Tensorclass
baseclass then has the same effects as@tensorclass
, and also:tensordict
issubclass
andisinstance
batch_size
etc.)Polyfill
To further clarify the effects of the subclass above, here's a quick polyfill that allows the definition of tensorclasses using the subclass-based API above in the current decorator-based paradigm:
The text was updated successfully, but these errors were encountered: