Question about factories, representations and multiple classes

132 views
Skip to first unread message

Antoine Leudière

unread,
May 13, 2022, 11:38:41 AM5/13/22
to sage-devel
Hi,

I have a class `MyObject` and a class `MyObject_specific_case` that inherits `MyObject`:

```
class MyObject:
    def __init__(self, arg):
        # Do stuff

class MyObject_specific_case:
    def __init__(self, arg):
        # Do stuff
        super().__init__(arg)

```

It is easy to decide if the object is a 'specific case' by looking at `arg`. Therefore, I would like some sort of mechanism that autonomously decides to instantiate `MyObject` or `MyObject_specific_case` depending on the value of `arg`. The ideal interface would be:

```
sage: my_object = MyObject(gen)
sage: # Assume arg corresponds to the specific case:
sage: isinstance(my_object, MyObject_specific_case)
True
sage: isinstance(my_object, MyObject)
True
sage: # Assume arg does not correspond to the specific case:
sage: isinstance(my_object, MyObject_specific_case)
False
sage: isinstance(my_object, MyObject)
True

```

My question is: what is the idiomatic way to do this?

Naively, one could use a function that takes `arg` as argument, but we would want both the function and the class to be named `MyObject`, so there is a collision. From what I understand, Sage typically uses a class along the lines of `UniqueRepresentation` or `UniqueFactory`. However, those seem to do way more than what I ask (https://doc.sagemath.org/html/en/reference/structure/sage/structure/unique_representation.html, https://doc.sagemath.org/html/en/reference/structure/sage/structure/factory.html), and I am a bit lost.

P.-S. : In real life, `MyObject` is `FiniteDrinfeldModule` and `MyObject_specific_case` is `FiniteDrinfeldModule_rank_two` (see https://trac.sagemath.org/ticket/33713).

David Roe

unread,
May 13, 2022, 6:07:02 PM5/13/22
to sage-devel
I think the following should work:

class MyObject:
    def __classcall__(cls, arg):
         if isinstance(arg, special):
             return typecall(MyObject_specific_case, arg)
         else:
             return typecall(MyObject, arg)

plus the same __init__ you had before.  I haven't checked it though....
David
              

--
You received this message because you are subscribed to the Google Groups "sage-devel" group.
To unsubscribe from this group and stop receiving emails from it, send an email to sage-devel+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/sage-devel/92c5b512-f235-474b-ab18-6f4e26dacbf9n%40googlegroups.com.

Eric Gourgoulhon

unread,
May 14, 2022, 4:42:49 PM5/14/22
to sage-devel
Hi,

Le samedi 14 mai 2022 à 00:07:02 UTC+2, David Roe a écrit :
I think the following should work:

class MyObject:
    def __classcall__(cls, arg):
         if isinstance(arg, special):
             return typecall(MyObject_specific_case, arg)
         else:
             return typecall(MyObject, arg)

plus the same __init__ you had before.  I haven't checked it though....
David
             

An alternative is to use __classcall_private__
For an example, see the class EuclideanSpace in

EuclideanSpace(n) actually returns an instance of the subclass EuclideanPlane if n = 2 or of the subclass Euclidean3dimSpace if n = 3.

Best wishes,

Eric.

Travis Scrimshaw

unread,
May 14, 2022, 11:21:48 PM5/14/22
to sage-devel
For this you want to use __classcall_private__ as otherwise you would likely end up in an infinite loop when you try to construct the subclass. There are lots of examples of this in the Sage library code.

Best,
Travis

Nils Bruin

unread,
May 15, 2022, 12:07:00 AM5/15/22
to sage-devel
It's probably worth pointing out that __classcall_private__ is not a standard python facility. It looks like you need

class Foo(metaclass=ClasscallMetaclass):

to make it work on Foo.

Antoine Leudière

unread,
Jun 7, 2022, 9:18:50 AM6/7/22
to sage-...@googlegroups.com
I thank all of you for your answers. And sorry for taking a bit long to reply!

After many trys and errors, here is something that works for me:

```

from sage.structure.sage_object import SageObject
from sage.structure.unique_representation import CachedRepresentation

class MyInteger(CachedRepresentation, SageObject):

@staticmethod
def __classcall_private__(cls, n):
if n % 2 == 0:
return MyInteger_even(n)
else:
return cls.__classcall__(cls, n)

def __init__(self, n):
self.n = n


class MyInteger_even(MyInteger):

def __init__(self, n):
self.n = n
self.half = n // 2
```

However, this has a major drawback, as all instances that have same data are
references to one another. And I don't find any way to bypass that:

```

sage: from copy import deepcopy
sage: s1 = MyInteger(2)
sage: s2 = MyInteger(2)
sage: s3 = deepcopy(s2)
sage: s1 == s2
True
sage: s1 is s2
True
sage: s1 == s3
True
sage: s1 is s3
True
sage: s1.n
2
sage: s3.n = 5
sage: s1.n
5
sage:
```

I appreciate that this may not be a problem for classes representing sets and
categories (like `EuclideanSpace`). But for classes representing children
elements, this may cause problems. And this is the case for me, as I need this
for `FiniteDrinfeldModule` and `FiniteDrinfeldModule_rank_two`
(https://trac.sagemath.org/ticket/33713).

Is there a way to avoid this?

Kindest regards,
Antoine
> --
> You received this message because you are subscribed to a topic in the Google
> Groups "sage-devel" group.
> To unsubscribe from this topic, visit
> https://groups.google.com/d/topic/sage-devel/PaUReuoxEXI/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to
> sage-devel+...@googlegroups.com.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/sage-devel/a8bc696f-7887-41b2-8fe9-af3ac055eb71n%40googlegroups.com
> .


John H Palmieri

unread,
Jun 7, 2022, 11:41:38 AM6/7/22
to sage-devel
Isn't this because you're using CachedRepresentation? From the documentation:

Instances of a class have a *cached representation behavior* when several
instances constructed with the same arguments share the same memory
representation.

davida...@gmail.com

unread,
Jun 7, 2022, 1:30:46 PM6/7/22
to sage-devel
Hello Antoine,

I'm also relatively new to Sage development, but one thing I can think of is that you could create a constructor module for your implementation of Drinfeld module. In short, the way I see it is that you would have a new module named "constructor" with a class "constructor.FiniteDrinfeldModule" which would be available to the end user. This "constructor class" would take care of formatting the user input and would return a "FiniteDrinfeldModule_rank_two" or a "FiniteDrinfeldModule_generic" depending of the input. This would also give you the opportunity of giving some liberty to the user when creating a Drinfeld module (via a list, an additive polynomial, etc...)

I don't know if this would be the most efficient way, but I've seen such idea for multiple different implementation inside SageMath. Any input from a more experienced Sage developer would be welcome here.

Best,

David A.

Travis Scrimshaw

unread,
Jun 8, 2022, 2:03:09 AM6/8/22
to sage-devel
I'm also relatively new to Sage development, but one thing I can think of is that you could create a constructor module for your implementation of Drinfeld module. In short, the way I see it is that you would have a new module named "constructor" with a class "constructor.FiniteDrinfeldModule" which would be available to the end user. This "constructor class" would take care of formatting the user input and would return a "FiniteDrinfeldModule_rank_two" or a "FiniteDrinfeldModule_generic" depending of the input. This would also give you the opportunity of giving some liberty to the user when creating a Drinfeld module (via a list, an additive polynomial, etc...)

I am generally -1 on this. There can be specific reasons for doing this, most of those involve fairly complicated object creation at the cost of disassociating the construction from the actual object returned. The __classcall_private__() mechanism works well for this kind of dispatching with lots of examples within Sage for this.

A quick general comment: The module should be a parent and vectors should be elements. Maps between them are elements of the Homset, which would be a parent.

Best,
Travis
 

Antoine Leudière

unread,
Jun 8, 2022, 10:49:08 AM6/8/22
to sage-...@googlegroups.com
Thanks John for the answer.

On Tue, 2022-06-07 at 08:41 -0700, John H Palmieri wrote:
> Isn't this because you're using CachedRepresentation? From the documentation:
>
> Instances of a class have a *cached representation behavior* when several
> instances constructed with the same arguments share the same memory
> representation.

Yep, this is precisely the case. Sorry if I was not clear. What I meant is that
this is a downside for me, and that I would like to prevent this behavior.

Antoine
> https://groups.google.com/d/msgid/sage-devel/ba864814-83af-4f4f-b5d7-e08f6108b551n%40googlegroups.com
> .


Antoine Leudière

unread,
Jun 8, 2022, 11:19:06 AM6/8/22
to sage-...@googlegroups.com
On Tue, 2022-06-07 at 23:03 -0700, 'Travis Scrimshaw' via sage-devel wrote:

> > I'm also relatively new to Sage development, but one thing I can think of is
> > that you could create a constructor module for your implementation of
> > Drinfeld module. In short, the way I see it is that you would have a new
> > module named "constructor" with a class "constructor.FiniteDrinfeldModule"
> > which would be available to the end user. This "constructor class" would
> > take care of formatting the user input and would return a
> > "FiniteDrinfeldModule_rank_two" or a "FiniteDrinfeldModule_generic"
> > depending of the input. This would also give you the opportunity of giving
> > some liberty to the user when creating a Drinfeld module (via a list, an
> > additive polynomial, etc...)
> >
>
>
> I am generally -1 on this. There can be specific reasons for doing this, most
> of those involve fairly complicated object creation at the cost of
> disassociating the construction from the actual object returned.

Thanks David for the suggestion. I however agree with Travis. This is too much
complication and bloating.

(On top of that, the pythonic way to have different constructors is to implement
methods decorated with `@classmethod` (or even `@staticmethod`). But this is
another question.)

> The __classcall_private__() mechanism works well for this kind of dispatching
> with lots of examples within Sage for this.

Agreed, and this is fairly standard. However, I am still skeptical about the
fact this mechanism prevents deep copies... This limits the user's abilities.

Kindest regards,
Antoine

>
> A quick general comment: The module should be a parent and vectors should be
> elements. Maps between them are elements of the Homset, which would be a
> parent.
>
> Best,
> Travis
>  
> --
> You received this message because you are subscribed to a topic in the Google
> Groups "sage-devel" group.
> To unsubscribe from this topic, visit
> https://groups.google.com/d/topic/sage-devel/PaUReuoxEXI/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to
> sage-devel+...@googlegroups.com.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/sage-devel/cf9bba5f-be9d-4d68-a2ac-ad0fd41ddd3dn%40googlegroups.com
> .


Travis Scrimshaw

unread,
Jun 9, 2022, 5:19:22 AM6/9/22
to sage-devel
> The __classcall_private__() mechanism works well for this kind of dispatching
> with lots of examples within Sage for this.

Agreed, and this is fairly standard. However, I am still skeptical about the
fact this mechanism prevents deep copies... This limits the user's abilities.

I have no idea what you mean by that. I feel like you are confusing that with UniqueRepresentation, which is a completely different concept (although they are typically paired together since we want to normalize the input). You can have a __classcall_*_() without being a UniqueRepresentation. You might also want to think about why you need to make (deep) copies.

Best,
Travis

Reply all
Reply to author
Forward
0 new messages