Why dynamic classes?
The short answer:
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:
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 (hopefuly) can again be achieved by inheritance:
class MyPermutation(UniqueRepresentation, PermutationCycleType):
...
Finaly, we may want to endow the permutations in \(S\) with further operations coming from the (algebraic) structure of \(S\):
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:
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 dynamicaly:
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.
Bases: sage.structure.dynamic_class.DynamicMetaclass, sage.misc.classcall_metaclass.ClasscallMetaclass
This invokes the nested_pickle on construction.
sage: from sage.misc.nested_class import NestedClassMetaclass sage: class A(object): ... __metaclass__ = NestedClassMetaclass ... class B(object): ... pass ... sage: A.B <class ‘__main__.A.B’> sage: getattr(sys.modules[‘__main__’], ‘A.B’, ‘Not found’) <class ‘__main__.A.B’>
Bases: type
A metaclass implementing an appropriate reduce-by-construction method
A class used for checking that introspection works
bla ...
INPUT:
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 instrospection 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: 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(object):
... "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
...
We now create a class FooBar which is a copy of Foo, except that it also inherits from Bar:
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__
(<type 'object'>,)
sage: FooBar.__bases__
(<type 'object'>, <class __main__.Bar at ...>)
sage: Foo.mro()
[<class '__main__.Foo'>, <type 'object'>]
sage: FooBar.mro()
[<class '__main__.FooBar'>, <type 'object'>, <class __main__.Bar at ...>]
Pickling
Dynamic classes are pickled by construction. Namely, upon unpickling, the class will be reconstructed by recalling dynamic_class with the same arguments:
sage: type(FooBar).__reduce__(FooBar)
(<function dynamic_class at ...>, ('FooBar', (<class __main__.Bar at ...>,), <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: type(FooBar)
<class 'sage.structure.dynamic_class.DynamicMetaclass'>
The following (meaningless) example illustrates how to customize the result of the reduction:
sage: BarFoo = dynamic_class("BarFoo", (Foo,), Bar, reduction = (str, (3,)))
sage: type(BarFoo).__reduce__(BarFoo)
(<type 'str'>, (3,))
sage: loads(dumps(BarFoo))
'3'
Caching
By default, the built class is cached:
sage: dynamic_class("FooBar", (Bar,), Foo) is FooBar
True
sage: dynamic_class("FooBar", (Bar,), Foo, cache=True) is FooBar
True
and the result depends on the reduction:
sage: dynamic_class("BarFoo", (Foo,), Bar, reduction = (str, (3,))) is BarFoo
True
sage: dynamic_class("BarFoo", (Foo,), Bar, reduction = (str, (2,))) is BarFoo
False
With cache=False, a new class is created each time:
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
With cache="ignore_reduction", the class does not depend on the reduction:
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
In particular, the reduction used is that provided upon creating the first class:
sage: dynamic_class("BarFoo", (Foo,), Bar, reduction = (str, (2,)), cache="ignore_reduction")._reduction
(<type '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.
TESTS:
sage: import __main__
sage: __main__.Foo = Foo
sage: __main__.Bar = Bar
sage: x = FooBar(3)
sage: x.__dict__ # Breaks without the __dict__ deletion in dynamic_class_internal
{'_x': 3}
sage: type(FooBar).__reduce__(FooBar)
(<function dynamic_class at ...>, ('FooBar', (<class __main__.Bar at ...>,), <class '__main__.Foo'>, None, None))
sage: import cPickle
sage: cPickle.loads(cPickle.dumps(FooBar)) == FooBar
True
We check that instrospection works reasonably:
sage: sage.misc.sageinspect.sage_getdoc(FooBar)
'The Foo class\n'
Finally, we check that classes derived from UniqueRepresentation are handled gracefuly (despite them also using a metaclass):
sage: FooUnique = dynamic_class("Foo", (Bar, UniqueRepresentation))
sage: loads(dumps(FooUnique)) is FooUnique
True
See sage.structure.dynamic_class.dynamic_class? for indirect doctests.
TESTS:
sage: Foo1 = sage.structure.dynamic_class.dynamic_class_internal("Foo", (object,))
sage: Foo2 = sage.structure.dynamic_class.dynamic_class_internal("Foo", (object,), doccls = sage.structure.dynamic_class.TestClass)
sage: Foo3 = sage.structure.dynamic_class.dynamic_class_internal("Foo", (object,), cls = sage.structure.dynamic_class.TestClass)
sage: all(Foo.__name__ == 'Foo' for Foo in [Foo1, Foo2, Foo3])
True
sage: all(Foo.__bases__ == (object,) for Foo in [Foo1, Foo2, Foo3])
True
sage: Foo1.__module__ == object.__module__
True
sage: Foo2.__module__ == sage.structure.dynamic_class.TestClass.__module__
True
sage: Foo3.__module__ == sage.structure.dynamic_class.TestClass.__module__
True
sage: Foo1.__doc__ == object.__doc__
True
sage: Foo2.__doc__ == sage.structure.dynamic_class.TestClass.__doc__
True
sage: Foo3.__doc__ == sage.structure.dynamic_class.TestClass.__doc__
True
We check that instrospection works reasonably:
sage: import inspect
sage: inspect.getfile(Foo2)
'.../sage/structure/dynamic_class.pyc'
sage: inspect.getfile(Foo3)
'.../sage/structure/dynamic_class.pyc'
sage: sage.misc.sageinspect.sage_getsourcelines(Foo2)
(['class TestClass:...'], ...)
sage: sage.misc.sageinspect.sage_getsourcelines(Foo3)
(['class TestClass:...'], ...)
sage: sage.misc.sageinspect.sage_getsourcelines(Foo2())
(['class TestClass:...'], ...)
sage: sage.misc.sageinspect.sage_getsourcelines(Foo3())
(['class TestClass:...'], ...)
sage: sage.misc.sageinspect.sage_getsourcelines(Foo3().bla)
([' def bla():...'], ...)