Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/sage/structure/dynamic_class.py
8814 views
1
"""
2
Dynamic classes
3
4
.. rubric:: Why dynamic classes?
5
6
The short answer:
7
8
- Multiple inheritance is a powerful tool for constructing new classes
9
by combining preexisting building blocks.
10
- There is a combinatorial explosion in the number of potentially
11
useful classes that can be produced this way.
12
- The implementation of standard mathematical constructions calls for
13
producing such combinations automatically.
14
- Dynamic classes, i.e. classes created on the fly by the Python
15
interpreter, are a natural mean to achieve this.
16
17
The long answer:
18
19
Say we want to construct a new class ``MyPermutation`` for
20
permutations in a given set `S` (in Sage, `S` will be modelled by a
21
parent, but we won't discuss this point here). First, we have to
22
choose a data structure for the permutations, typically among the
23
following:
24
25
- Stored by cycle type
26
- Stored by code
27
- Stored in list notation
28
- C arrays of short ints (for small permutations)
29
- python lists of ints (for huge permutations)
30
- ...
31
- Stored by reduced word
32
- Stored as a function
33
- ...
34
35
Luckily, the Sage library provides (or will provide) classes
36
implementing each of those data structures. Those classes all share a
37
common interface (or possibly a common abstract base class). So we can
38
just derive our class from the chosen one::
39
40
class MyPermutation(PermutationCycleType):
41
...
42
43
Then we may want to further choose a specific memory behavior (unique
44
representation, copy-on-write) which (hopefuly) can again be achieved
45
by inheritance::
46
47
class MyPermutation(UniqueRepresentation, PermutationCycleType):
48
...
49
50
Finaly, we may want to endow the permutations in `S` with further
51
operations coming from the (algebraic) structure of `S`:
52
53
- group operations
54
- or just monoid operations (for a subset of permutations not stable by inverse)
55
- poset operations (for left/right/Bruhat order)
56
- word operations (searching for substrings, patterns, ...)
57
58
Or any combination thereof. Now, our class typically looks like::
59
60
class MyPermutation(UniqueRepresentation, PermutationCycleType, PosetElement, GroupElement):
61
...
62
63
Note the combinatorial explosion in the potential number of classes
64
which can be created this way.
65
66
67
In practice, such classes will be used in mathematical constructions
68
like::
69
70
SymmetricGroup(5).subset(... TODO: find a good example in the context above ...)
71
72
In such a construction, the structure of the result, and therefore the
73
operations on its elements can only be determined at execution
74
time. Let us take another standard construction::
75
76
A = cartesian_product( B, C )
77
78
Depending on the structure of `B` and `C`, and possibly on further
79
options passed down by the user, `A` may be:
80
81
- an enumerated set
82
- a group
83
- an algebra
84
- a poset
85
- ...
86
87
Or any combination thereof.
88
89
Hardcoding classes for all potential combinations would be at best
90
tedious. Furthermore, this would require a cumbersome mechanism to
91
lookup the appropriate class depending on the desired combination.
92
93
Instead, one may use the ability of Python to create new classes
94
dynamicaly::
95
96
type("class name", tuple of base classes, dictionary of methods)
97
98
This paradigm is powerful, but there are some technicalities to
99
address. The purpose of this library is to standardize its use within
100
Sage, and in particular to ensure that the constructed classes are
101
reused whenever possible (unique representation), and can be pickled.
102
103
.. rubric:: Combining dynamic classes and Cython classes
104
105
Cython classes cannot inherit from a dynamic class (there might be
106
some partial support for this in the future). On the other hand, such
107
an inheritance can be partially emulated using :meth:`__getattr__`. See
108
``sage.categories.examples.semigroups_cython`` for an example.
109
110
"""
111
112
#*****************************************************************************
113
# Copyright (C) 2008-2009 Nicolas M. Thiery <nthiery at users.sf.net>
114
#
115
# Distributed under the terms of the GNU General Public License (GPL)
116
# http://www.gnu.org/licenses/
117
#*****************************************************************************
118
119
from sage.misc.cachefunc import weak_cached_function
120
from sage.structure.unique_representation import ClasscallMetaclass
121
122
def dynamic_class(name, bases, cls=None, reduction=None, doccls=None,
123
prepend_cls_bases=True, cache=True):
124
r"""
125
INPUT:
126
127
- ``name`` -- a string
128
- ``bases`` -- a tuple of classes
129
- ``cls`` -- a class or ``None``
130
- ``reduction`` -- a tuple or ``None``
131
- ``doccls`` -- a class or ``None``
132
- ``prepend_cls_bases`` -- a boolean (default: ``True``)
133
- ``cache`` -- a boolean or ``"ignore_reduction"`` (default: ``True``)
134
135
Constructs dynamically a new class ``C`` with name ``name``, and
136
bases ``bases``. If ``cls`` is provided, then its methods will be
137
inserted into ``C``, and its bases will be prepended to ``bases``
138
(unless ``prepend_cls_bases`` is ``False``).
139
140
The module, documentation and source instrospection is taken from
141
``doccls``, or ``cls`` if ``doccls`` is ``None``, or ``bases[0]``
142
if both are ``None`` (therefore ``bases`` should be non empty if
143
``cls` is ``None``).
144
145
The constructed class can safely be pickled (assuming the
146
arguments themselves can).
147
148
Unless ``cache`` is ``False``, the result is cached, ensuring unique
149
representation of dynamic classes.
150
151
See :mod:`sage.structure.dynamic_class` for a discussion of the
152
dynamic classes paradigm, and its relevance to Sage.
153
154
EXAMPLES:
155
156
To setup the stage, we create a class Foo with some methods,
157
cached methods, and lazy attributes, and a class Bar::
158
159
sage: from sage.misc.lazy_attribute import lazy_attribute
160
sage: from sage.misc.cachefunc import cached_function
161
sage: from sage.structure.dynamic_class import dynamic_class
162
sage: class Foo(object):
163
... "The Foo class"
164
... def __init__(self, x):
165
... self._x = x
166
... @cached_method
167
... def f(self):
168
... return self._x^2
169
... def g(self):
170
... return self._x^2
171
... @lazy_attribute
172
... def x(self):
173
... return self._x
174
...
175
sage: class Bar:
176
... def bar(self):
177
... return self._x^2
178
...
179
180
We now create a class FooBar which is a copy of Foo, except that it
181
also inherits from Bar::
182
183
sage: FooBar = dynamic_class("FooBar", (Bar,), Foo)
184
sage: x = FooBar(3)
185
sage: x.f()
186
9
187
sage: x.f() is x.f()
188
True
189
sage: x.x
190
3
191
sage: x.bar()
192
9
193
sage: FooBar.__name__
194
'FooBar'
195
sage: FooBar.__module__
196
'__main__'
197
198
sage: Foo.__bases__
199
(<type 'object'>,)
200
sage: FooBar.__bases__
201
(<type 'object'>, <class __main__.Bar at ...>)
202
sage: Foo.mro()
203
[<class '__main__.Foo'>, <type 'object'>]
204
sage: FooBar.mro()
205
[<class '__main__.FooBar'>, <type 'object'>, <class __main__.Bar at ...>]
206
207
.. RUBRIC:: Pickling
208
209
Dynamic classes are pickled by construction. Namely, upon
210
unpickling, the class will be reconstructed by recalling
211
dynamic_class with the same arguments::
212
213
sage: type(FooBar).__reduce__(FooBar)
214
(<function dynamic_class at ...>, ('FooBar', (<class __main__.Bar at ...>,), <class '__main__.Foo'>, None, None))
215
216
Technically, this is achieved by using a metaclass, since the
217
Python pickling protocol for classes is to pickle by name::
218
219
sage: type(FooBar)
220
<class 'sage.structure.dynamic_class.DynamicMetaclass'>
221
222
223
The following (meaningless) example illustrates how to customize
224
the result of the reduction::
225
226
sage: BarFoo = dynamic_class("BarFoo", (Foo,), Bar, reduction = (str, (3,)))
227
sage: type(BarFoo).__reduce__(BarFoo)
228
(<type 'str'>, (3,))
229
sage: loads(dumps(BarFoo))
230
'3'
231
232
.. RUBRIC:: Caching
233
234
By default, the built class is cached::
235
236
sage: dynamic_class("FooBar", (Bar,), Foo) is FooBar
237
True
238
sage: dynamic_class("FooBar", (Bar,), Foo, cache=True) is FooBar
239
True
240
241
and the result depends on the reduction::
242
243
sage: dynamic_class("BarFoo", (Foo,), Bar, reduction = (str, (3,))) is BarFoo
244
True
245
sage: dynamic_class("BarFoo", (Foo,), Bar, reduction = (str, (2,))) is BarFoo
246
False
247
248
With ``cache=False``, a new class is created each time::
249
250
sage: FooBar1 = dynamic_class("FooBar", (Bar,), Foo, cache=False); FooBar1
251
<class '__main__.FooBar'>
252
sage: FooBar2 = dynamic_class("FooBar", (Bar,), Foo, cache=False); FooBar2
253
<class '__main__.FooBar'>
254
sage: FooBar1 is FooBar
255
False
256
sage: FooBar2 is FooBar1
257
False
258
259
With ``cache="ignore_reduction"``, the class does not depend on
260
the reduction::
261
262
sage: BarFoo = dynamic_class("BarFoo", (Foo,), Bar, reduction = (str, (3,)), cache="ignore_reduction")
263
sage: dynamic_class("BarFoo", (Foo,), Bar, reduction = (str, (2,)), cache="ignore_reduction") is BarFoo
264
True
265
266
In particular, the reduction used is that provided upon creating the
267
first class::
268
269
sage: dynamic_class("BarFoo", (Foo,), Bar, reduction = (str, (2,)), cache="ignore_reduction")._reduction
270
(<type 'str'>, (3,))
271
272
.. WARNING::
273
274
The behaviour upon creating several dynamic classes from the
275
same data but with different values for ``cache`` option is
276
currently left unspecified. In other words, for a given
277
application, it is recommended to consistently use the same
278
value for that option.
279
280
TESTS::
281
282
sage: import __main__
283
sage: __main__.Foo = Foo
284
sage: __main__.Bar = Bar
285
sage: x = FooBar(3)
286
sage: x.__dict__ # Breaks without the __dict__ deletion in dynamic_class_internal
287
{'_x': 3}
288
289
sage: type(FooBar).__reduce__(FooBar)
290
(<function dynamic_class at ...>, ('FooBar', (<class __main__.Bar at ...>,), <class '__main__.Foo'>, None, None))
291
sage: import cPickle
292
sage: cPickle.loads(cPickle.dumps(FooBar)) == FooBar
293
True
294
295
We check that instrospection works reasonably::
296
297
sage: sage.misc.sageinspect.sage_getdoc(FooBar)
298
'The Foo class\n'
299
300
Finally, we check that classes derived from UniqueRepresentation
301
are handled gracefuly (despite them also using a metaclass)::
302
303
sage: FooUnique = dynamic_class("Foo", (Bar, UniqueRepresentation))
304
sage: loads(dumps(FooUnique)) is FooUnique
305
True
306
307
"""
308
bases = tuple(bases)
309
#assert(len(bases) > 0 )
310
assert(type(name) is str)
311
# assert(cls is None or issubtype(type(cls), type) or type(cls) is classobj)
312
if cache is True:
313
return dynamic_class_internal(name, bases, cls, reduction, doccls, prepend_cls_bases)
314
elif cache is False:
315
# bypass the cached method
316
return dynamic_class_internal.f(name, bases, cls, reduction, doccls, prepend_cls_bases)
317
else: # cache = "ignore_reduction"
318
result = dynamic_class_internal(name, bases, cls, False, doccls, prepend_cls_bases)
319
if result._reduction is False:
320
result._reduction = reduction
321
return result
322
323
324
@weak_cached_function
325
def dynamic_class_internal(name, bases, cls=None, reduction=None, doccls=None, prepend_cls_bases=True):
326
r"""
327
See sage.structure.dynamic_class.dynamic_class? for indirect doctests.
328
329
TESTS::
330
331
sage: Foo1 = sage.structure.dynamic_class.dynamic_class_internal("Foo", (object,))
332
sage: Foo2 = sage.structure.dynamic_class.dynamic_class_internal("Foo", (object,), doccls = sage.structure.dynamic_class.TestClass)
333
sage: Foo3 = sage.structure.dynamic_class.dynamic_class_internal("Foo", (object,), cls = sage.structure.dynamic_class.TestClass)
334
sage: all(Foo.__name__ == 'Foo' for Foo in [Foo1, Foo2, Foo3])
335
True
336
sage: all(Foo.__bases__ == (object,) for Foo in [Foo1, Foo2, Foo3])
337
True
338
sage: Foo1.__module__ == object.__module__
339
True
340
sage: Foo2.__module__ == sage.structure.dynamic_class.TestClass.__module__
341
True
342
sage: Foo3.__module__ == sage.structure.dynamic_class.TestClass.__module__
343
True
344
sage: Foo1.__doc__ == object.__doc__
345
True
346
sage: Foo2.__doc__ == sage.structure.dynamic_class.TestClass.__doc__
347
True
348
sage: Foo3.__doc__ == sage.structure.dynamic_class.TestClass.__doc__
349
True
350
351
We check that instrospection works reasonably::
352
353
sage: import inspect
354
sage: inspect.getfile(Foo2)
355
'.../sage/structure/dynamic_class.pyc'
356
sage: inspect.getfile(Foo3)
357
'.../sage/structure/dynamic_class.pyc'
358
sage: sage.misc.sageinspect.sage_getsourcelines(Foo2)
359
(['class TestClass:...'], ...)
360
sage: sage.misc.sageinspect.sage_getsourcelines(Foo3)
361
(['class TestClass:...'], ...)
362
sage: sage.misc.sageinspect.sage_getsourcelines(Foo2())
363
(['class TestClass:...'], ...)
364
sage: sage.misc.sageinspect.sage_getsourcelines(Foo3())
365
(['class TestClass:...'], ...)
366
sage: sage.misc.sageinspect.sage_getsourcelines(Foo3().bla)
367
([' def bla():...'], ...)
368
369
"""
370
if reduction is None:
371
reduction = (dynamic_class, (name, bases, cls, reduction, doccls))
372
if cls is not None:
373
methods = dict(cls.__dict__)
374
# Anything else that should not be kept?
375
if methods.has_key("__dict__"):
376
methods.__delitem__("__dict__")
377
if prepend_cls_bases:
378
bases = cls.__bases__ + bases
379
else:
380
methods = {}
381
if doccls is None:
382
if cls is not None:
383
doccls = cls
384
else:
385
assert bases != ()
386
doccls = bases[0]
387
methods['_reduction'] = reduction
388
if not methods.has_key("_sage_src_lines_"):
389
from sage.misc.sageinspect import sage_getsourcelines
390
@staticmethod
391
def _sage_src_lines():
392
return sage_getsourcelines(doccls)
393
methods['_sage_src_lines_'] = _sage_src_lines
394
methods['__doc__'] = doccls.__doc__
395
methods['__module__'] = doccls.__module__
396
#if not methods.has_key("_sage_doc_"):
397
# from sage.misc.sageinspect import sage_getdoc
398
# def _sage_getdoc(obj):
399
# return sage_getdoc(cls)
400
# methods['_sage_src_lines_'] = _sage_getdoc
401
402
metaclass = DynamicMetaclass
403
# The metaclass of a class must derive from the metaclasses of its
404
# bases. The following handles the case where one of the base
405
# class is readilly in the ClasscallMetaclass. This
406
# approach won't scale well if we start using metaclasses
407
# elsewhere in Sage.
408
for base in bases:
409
if type(base) is ClasscallMetaclass:
410
metaclass = DynamicClasscallMetaclass
411
return metaclass(name, bases, methods)
412
413
class DynamicMetaclass(type):
414
"""
415
A metaclass implementing an appropriate reduce-by-construction method
416
"""
417
418
def __reduce__(self):
419
"""
420
See sage.structure.dynamic_class.dynamic_class? for non trivial tests.
421
422
TESTS::
423
424
sage: class Foo: pass
425
sage: class DocClass: pass
426
sage: C = sage.structure.dynamic_class.dynamic_class_internal("bla", (object,), Foo, doccls = DocClass)
427
sage: type(C).__reduce__(C)
428
(<function dynamic_class at ...>,
429
('bla', (<type 'object'>,), <class __main__.Foo at ...>, None, <class __main__.DocClass at ...>))
430
sage: C = sage.structure.dynamic_class.dynamic_class_internal("bla", (object,), Foo, doccls = DocClass, reduction = "blah")
431
sage: type(C).__reduce__(C)
432
'blah'
433
"""
434
return self._reduction
435
436
class DynamicClasscallMetaclass(DynamicMetaclass, ClasscallMetaclass):
437
pass
438
439
# This registers the appropriate reduction methods (depends on #5985)
440
import copy_reg
441
copy_reg.pickle(DynamicMetaclass, DynamicMetaclass.__reduce__)
442
443
import copy_reg
444
copy_reg.pickle(DynamicClasscallMetaclass, DynamicMetaclass.__reduce__)
445
446
class TestClass:
447
"""
448
A class used for checking that introspection works
449
"""
450
def bla():
451
"""
452
bla ...
453
"""
454
pass
455
456