pyiron_snippets.factory module
Tools for making dynamically generated classes unique, and their instances pickleable.
Provides two main user-facing tools: classfactory(), which should be used
_exclusively_ as a decorator (this restriction pertains to namespace requirements for
re-importing), and ClassFactory, which can be used to instantiate a new factory from
some existing factory function.
In both cases, the decorated function/input argument should be a pickleable function
taking only positional arguments, and returning a tuple suitable for use in dynamic
class creation via builtins.type() – i.e. taking a class name, a tuple of base
classes, a dictionary of class attributes, and a dictionary of values to be expanded
into kwargs for __subclass_init__.
The resulting factory produces classes that are (a) pickleable, and (b) the same object as any previously built class with the same name. (Note: avoiding class degeneracy with respect to class name is the responsibility of the person writing the factory function.)
These classes are then themselves pickleable, and produce instances which are in turn pickleable (so long as any data they’ve been fed as inputs or attributes is pickleable, i.e. here the only pickle-barrier we resolve is that of having come from a dynamically generated class).
Since users need to build their own class factories returning classes with sensible
names, we also provide a helper function sanitize_callable_name(), which makes
sure a string is compliant with use as a class name. This is run internally on user-
provided names, and failure for the user name and sanitized name to match will give a
clear error message.
Constructed classes can, in turn be used as bases in further class factories.
- class pyiron_snippets.factory.ClassFactory(factory_function)[source]
Bases:
objectA constructor for new class factories.
Use on existing class factory callables, _not_ as a decorator.
Cf. the
classfactory()decorator for more info.
- exception pyiron_snippets.factory.InvalidClassNameError[source]
Bases:
ValueErrorWhen a string isn’t a good class name
- exception pyiron_snippets.factory.InvalidFactorySignature[source]
Bases:
ValueErrorWhen the factory function’s arguments are not purely positional
- pyiron_snippets.factory.classfactory(factory_function: Callable[[...], tuple[str, tuple[type, ...], dict, dict]]) _ClassFactory[source]
A decorator for building dynamic class factories whose classes are unique and whose terminal instances can be pickled.
Under the hood, classes created by factories get dependence on
_FactoryMademixed in. This class leverages__reduce__()and__init_subclass__()and uses up the class namespace_class_factoryand_class_factory_argsto hold data (using up corresponding public variable names in the__init_subclass__()kwargs), so any interference with these fields may cause unexpected side effects. For un-pickling, the dynamic class gets recreated then its__new__()is called using __newargs_ex__; a default implementation returning no arguments is provided on_FactoryMadebut can be overridden.- Parameters:
factory_function (Callable[..., tuple[str, tuple[type, ...], dict, dict]]) – A function returning arguments that would be passed to builtins.type to dynamically generate a class. The function must accept exclusively positional arguments
- Returns:
- A new callable that returns unique classes whose
instances can be pickled.
- Return type:
(type[_ClassFactory])
Notes
If the :param:`factory_function` itself, or any data stored on instances of its resulting class(es) cannot be pickled, then the instances will not be able to be pickled. Here we only remove the trouble associated with pickling dynamically created classes.
If the __init_subclass__ kwargs are exploited, remember that these are subject to all the same “gotchas” as their regular non-factory use; namely, all child classes must specify _all_ parent class kwargs in order to avoid them getting overwritten by the parent class defaults!
Dynamically generated classes can, in turn, be used as base classes for further @classfactory decorated factory functions.
Warning
Use _exclusively_ as a decorator. For an inline constructor for an existing callable, use
ClassFactoryinstead.Examples
>>> import pickle >>> >>> from pyiron_snippets.factory import classfactory >>> from abc import ABC >>> >>> class HasN(ABC): ... '''Some class I want to make dynamically subclass.''' ... def __init_subclass__(cls, /, n=0, s="foo", **kwargs): ... super(HasN, cls).__init_subclass__(**kwargs) ... cls.n = n ... cls.s = s ... ... def __init__(self, x, y=0): ... self.x = x ... self.y = y >>> >>> @classfactory ... def has_n_factory(n, s="wrapped_function", /): ... return ( ... f"{HasN.__name__}{n}{s}", # New class name ... (HasN,), # Base class(es) ... {}, # Class attributes dictionary ... {"n": n, "s": s} ... # dict of `builtins.type` kwargs (passed to `__init_subclass__`) ... ) >>> >>> Has2 = has_n_factory(2, "my_dynamic_class") >>> HasToo = has_n_factory(2, "my_dynamic_class") >>> HasToo is Has2 True
>>> foo = Has2(42, y=-1) >>> print(foo.n, foo.s, foo.x, foo.y) 2 my_dynamic_class 42 -1
>>> reloaded = pickle.loads(pickle.dumps(foo)) >>> print(reloaded.n, reloaded.s, reloaded.x, reloaded.y) 2 my_dynamic_class 42 -1