Dynamic classes - Parents and Elements

Skip to content
Back to top
View this page
Edit this page

# Dynamic classes

Why dynamic classes?

The short answer:

- Multiple inheritance is a powerful tool for constructing new classes by combining preexisting building blocks.

- There is a combinatorial explosion in the number of potentially useful classes that can be produced this way.

- The implementation of standard mathematical constructions calls for producing such combinations automatically.

- Dynamic classes, i.e. classes created on the fly by the Python interpreter, are a natural mean to achieve this.

The long answer:

Say we want to construct a new class `MyPermutation` for permutations in a given set \(S\) (in Sage, \(S\) will be modelled by a parent, but we won’t discuss this point here). First, we have to choose a data structure for the permutations, typically among the following:

- Stored by cycle type

- Stored by code

- Stored in list notation - C arrays of short ints (for small permutations) - python lists of ints (for huge permutations) - …

- Stored by reduced word

- Stored as a function

- …

Luckily, the Sage library provides (or will provide) classes implementing each of those data structures. Those classes all share a common interface (or possibly a common abstract base class). So we can just derive our class from the chosen one:

    class MyPermutation(PermutationCycleType):
        ...

Then we may want to further choose a specific memory behavior (unique representation, copy-on-write) which (hopefully) can again be achieved by inheritance:

    class MyPermutation(UniqueRepresentation, PermutationCycleType):
         ...

Finally, we may want to endow the permutations in \(S\) with further operations coming from the (algebraic) structure of \(S\):

- group operations

- or just monoid operations (for a subset of permutations not stable by inverse)

- poset operations (for left/right/Bruhat order)

- word operations (searching for substrings, patterns, …)

Or any combination thereof. Now, our class typically looks like:

    class MyPermutation(UniqueRepresentation, PermutationCycleType, PosetElement, GroupElement):
         ...

Note the combinatorial explosion in the potential number of classes which can be created this way.

In practice, such classes will be used in mathematical constructions like:

    SymmetricGroup(5).subset(... TODO: find a good example in the context above ...)

In such a construction, the structure of the result, and therefore the operations on its elements can only be determined at execution time. Let us take another standard construction:

    A = cartesian_product( B, C )

Depending on the structure of \(B\) and \(C\), and possibly on further options passed down by the user, \(A\) may be:

- an enumerated set

- a group

- an algebra

- a poset

- …

Or any combination thereof.

Hardcoding classes for all potential combinations would be at best tedious. Furthermore, this would require a cumbersome mechanism to lookup the appropriate class depending on the desired combination.

Instead, one may use the ability of Python to create new classes dynamically:

    type("class name", tuple of base classes, dictionary of methods)

This paradigm is powerful, but there are some technicalities to address. The purpose of this library is to standardize its use within Sage, and in particular to ensure that the constructed classes are reused whenever possible (unique representation), and can be pickled.

Combining dynamic classes and Cython classes

Cython classes cannot inherit from a dynamic class (there might be some partial support for this in the future). On the other hand, such an inheritance can be partially emulated using `__getattr__()`. See `sage.categories.examples.semigroups_cython` for an example.

class sage.structure.dynamic_class.DynamicClasscallMetaclass

  Bases: `DynamicMetaclass`, `ClasscallMetaclass`

class sage.structure.dynamic_class.DynamicInheritComparisonClasscallMetaclass

  Bases: `DynamicMetaclass`, `InheritComparisonClasscallMetaclass`

class sage.structure.dynamic_class.DynamicInheritComparisonMetaclass

  Bases: `DynamicMetaclass`, `InheritComparisonMetaclass`

class sage.structure.dynamic_class.DynamicMetaclass

  Bases: `type`

  A metaclass implementing an appropriate reduce-by-construction method

sage.structure.dynamic_class.M

  alias of `DynamicInheritComparisonClasscallMetaclass`

class sage.structure.dynamic_class.TestClass

  Bases: `object`

  A class used for checking that introspection works

  bla()

    bla …

sage.structure.dynamic_class.dynamic_class(name, bases, cls=None, reduction=None, doccls=None, prepend_cls_bases=True, cache=True)

  INPUT:

  - `name` – string

  - `bases` – tuple of classes

  - `cls` – a class or `None`

  - `reduction` – tuple or `None`

  - `doccls` – a class or `None`

  - `prepend_cls_bases` – boolean (default: `True`)

  - `cache` – boolean or `'ignore_reduction'` (default: `True`)

  Constructs dynamically a new class `C` with name `name`, and bases `bases`. If `cls` is provided, then its methods will be inserted into `C`, and its bases will be prepended to `bases` (unless `prepend_cls_bases` is `False`).

  The module, documentation and source introspection is taken from `doccls`, or `cls` if `doccls` is `None`, or `bases[0]` if both are `None` (therefore `bases` should be non empty if `cls` is `None`).

  The constructed class can safely be pickled (assuming the arguments themselves can).

  Unless `cache` is `False`, the result is cached, ensuring unique representation of dynamic classes.

  See `sage.structure.dynamic_class` for a discussion of the dynamic classes paradigm, and its relevance to Sage.

  EXAMPLES:

  To setup the stage, we create a class Foo with some methods, cached methods, and lazy attributes, and a class Bar:

  Sage

      sage: from sage.misc.lazy_attribute import lazy_attribute
      sage: from sage.misc.cachefunc import cached_function
      sage: from sage.structure.dynamic_class import dynamic_class
      sage: class Foo():
      ....:     "The Foo class"
      ....:     def __init__(self, x):
      ....:         self._x = x
      ....:     @cached_method
      ....:     def f(self):
      ....:         return self._x^2
      ....:     def g(self):
      ....:         return self._x^2
      ....:     @lazy_attribute
      ....:     def x(self):
      ....:         return self._x
      sage: class Bar:
      ....:     def bar(self):
      ....:         return self._x^2

  Python

      >>> from sage.all import *
      >>> from sage.misc.lazy_attribute import lazy_attribute
      >>> from sage.misc.cachefunc import cached_function
      >>> from sage.structure.dynamic_class import dynamic_class
      >>> class Foo():
      ...     "The Foo class"
      ...     def __init__(self, x):
      ...         self._x = x
      ...     @cached_method
      ...     def f(self):
      ...         return self._x**Integer(2)
      ...     def g(self):
      ...         return self._x**Integer(2)
      ...     @lazy_attribute
      ...     def x(self):
      ...         return self._x
      >>> class Bar:
      ...     def bar(self):
      ...         return self._x**Integer(2)

  We now create a class FooBar which is a copy of Foo, except that it also inherits from Bar:

  Sage

      sage: FooBar = dynamic_class("FooBar", (Bar,), Foo)
      sage: x = FooBar(3)
      sage: x.f()
      9
      sage: x.f() is x.f()
      True
      sage: x.x
      3
      sage: x.bar()
      9
      sage: FooBar.__name__
      'FooBar'
      sage: FooBar.__module__
      '__main__'

      sage: Foo.__bases__
      (<class 'object'>,)
      sage: FooBar.__bases__
      (<class '__main__.Bar'>,)
      sage: Foo.mro()
      [<class '__main__.Foo'>, <class 'object'>]
      sage: FooBar.mro()
      [<class '__main__.FooBar'>, <class '__main__.Bar'>, <class 'object'>]

  [Python]

      >>> from sage.all import *
      >>> FooBar = dynamic_class("FooBar", (Bar,), Foo)
      >>> x = FooBar(Integer(3))
      >>> x.f()
      9
      >>> x.f() is x.f()
      True
      >>> x.x
      3
      >>> x.bar()
      9
      >>> FooBar.__name__
      'FooBar'
      >>> FooBar.__module__
      '__main__'

      >>> Foo.__bases__
      (<class 'object'>,)
      >>> FooBar.__bases__
      (<class '__main__.Bar'>,)
      >>> Foo.mro()
      [<class '__main__.Foo'>, <class 'object'>]
      >>> FooBar.mro()
      [<class '__main__.FooBar'>, <class '__main__.Bar'>, <class 'object'>]

  If all the base classes have a zero `__dictoffset__`, the dynamic class also has a zero `__dictoffset__`. This means that the instances of the class don’t have a `__dict__` (see Issue #23435):

  Sage

      sage: dyn = dynamic_class("dyn", (Integer,))
      sage: dyn.__dictoffset__
      0

  Python

      >>> from sage.all import *
      >>> dyn = dynamic_class("dyn", (Integer,))
      >>> dyn.__dictoffset__
      0

  Pickling

  Dynamic classes are pickled by construction. Namely, upon unpickling, the class will be reconstructed by recalling dynamic_class with the same arguments:

  Sage

      sage: type(FooBar).__reduce__(FooBar)
      (<function dynamic_class at ...>, ('FooBar', (<class '__main__.Bar'>,), <class '__main__.Foo'>, None, None))

  [Python]

      >>> from sage.all import *
      >>> type(FooBar).__reduce__(FooBar)
      (<function dynamic_class at ...>, ('FooBar', (<class '__main__.Bar'>,), <class '__main__.Foo'>, None, None))

  Technically, this is achieved by using a metaclass, since the Python pickling protocol for classes is to pickle by name:

  Sage

      sage: type(FooBar)
      <class 'sage.structure.dynamic_class.DynamicMetaclass'>

  Python

      >>> from sage.all import *
      >>> type(FooBar)
      <class 'sage.structure.dynamic_class.DynamicMetaclass'>

  The following (meaningless) example illustrates how to customize the result of the reduction:

  Sage

      sage: BarFoo = dynamic_class('BarFoo', (Foo,), Bar, reduction = (str, (3,)))
      sage: type(BarFoo).__reduce__(BarFoo)
      (<class 'str'>, (3,))
      sage: loads(dumps(BarFoo))
      '3'

  [Python]

      >>> from sage.all import *
      >>> BarFoo = dynamic_class('BarFoo', (Foo,), Bar, reduction = (str, (Integer(3),)))
      >>> type(BarFoo).__reduce__(BarFoo)
      (<class 'str'>, (3,))
      >>> loads(dumps(BarFoo))
      '3'

  Caching

  By default, the built class is cached:

  Sage

      sage: dynamic_class("FooBar", (Bar,), Foo) is FooBar
      True
      sage: dynamic_class("FooBar", (Bar,), Foo, cache=True) is FooBar
      True

  Python

      >>> from sage.all import *
      >>> dynamic_class("FooBar", (Bar,), Foo) is FooBar
      True
      >>> dynamic_class("FooBar", (Bar,), Foo, cache=True) is FooBar
      True

  and the result depends on the reduction:

  Sage

      sage: dynamic_class('BarFoo', (Foo,), Bar, reduction = (str, (3,))) is BarFoo
      True
      sage: dynamic_class('BarFoo', (Foo,), Bar, reduction = (str, (2,))) is BarFoo
      False

  [Python]

      >>> from sage.all import *
      >>> dynamic_class('BarFoo', (Foo,), Bar, reduction = (str, (Integer(3),))) is BarFoo
      True
      >>> dynamic_class('BarFoo', (Foo,), Bar, reduction = (str, (Integer(2),))) is BarFoo
      False

  With `cache=False`, a new class is created each time:

  Sage

      sage: FooBar1 = dynamic_class("FooBar", (Bar,), Foo, cache=False); FooBar1
      <class '__main__.FooBar'>
      sage: FooBar2 = dynamic_class("FooBar", (Bar,), Foo, cache=False); FooBar2
      <class '__main__.FooBar'>
      sage: FooBar1 is FooBar
      False
      sage: FooBar2 is FooBar1
      False

  Python

      >>> from sage.all import *
      >>> FooBar1 = dynamic_class("FooBar", (Bar,), Foo, cache=False); FooBar1
      <class '__main__.FooBar'>
      >>> FooBar2 = dynamic_class("FooBar", (Bar,), Foo, cache=False); FooBar2
      <class '__main__.FooBar'>
      >>> FooBar1 is FooBar
      False
      >>> FooBar2 is FooBar1
      False

  With `cache="ignore_reduction"`, the class does not depend on the reduction:

  Sage

      sage: BarFoo = dynamic_class('BarFoo', (Foo,), Bar, reduction = (str, (3,)), cache='ignore_reduction')
      sage: dynamic_class('BarFoo', (Foo,), Bar, reduction = (str, (2,)), cache='ignore_reduction') is BarFoo
      True

  [Python]

      >>> from sage.all import *
      >>> BarFoo = dynamic_class('BarFoo', (Foo,), Bar, reduction = (str, (Integer(3),)), cache='ignore_reduction')
      >>> dynamic_class('BarFoo', (Foo,), Bar, reduction = (str, (Integer(2),)), cache='ignore_reduction') is BarFoo
      True

  In particular, the reduction used is that provided upon creating the first class:

  Sage

      sage: dynamic_class('BarFoo', (Foo,), Bar, reduction = (str, (2,)), cache='ignore_reduction')._reduction
      (<class 'str'>, (3,))

  Python

      >>> from sage.all import *
      >>> dynamic_class('BarFoo', (Foo,), Bar, reduction = (str, (Integer(2),)), cache='ignore_reduction')._reduction
      (<class 'str'>, (3,))

  Warning

  The behaviour upon creating several dynamic classes from the same data but with different values for `cache` option is currently left unspecified. In other words, for a given application, it is recommended to consistently use the same value for that option.

sage.structure.dynamic_class.dynamic_class_internal(bases, cls=None, reduction=None, doccls=None, prepend_cls_bases=True)

  See sage.structure.dynamic_class.dynamic_class? for indirect doctests.