2

Right now, I have two sets of python mixins, A and B.

Let (A_1, A_2, ..., A_i, ...) and (B_1, B_2, ..., B_i, ...), be the elements in these sets, respectively.

I want to make classes that are the cross between these two mixin sets and be able to references the elements of the cross by name. Is there some nice way in python to do this? Specifically, if say I have mixins

class A_1():
 pass
class A_2():
 pass
class B_1():
 pass
class B_2():
 pass

I want to be able to generate classes:

class A_1B_1(A_1, B_1):
  pass

class A_1B_2(A_1, B_2):
  pass

class A_2B_1(A_2, B_1):
  pass

class A_2B_2(A_2, B_2):
  pass

What's the best way to do that?

Additionally, let's say I want to add a new mixin to set B, call it B_x. Is there a nice way to define some function that I can call on B_x to generate the classes that would result from crossing all of the A_i with B_x? I.e., is there a function f that I can write such that

f(B_x, [A_1, A_2, ..., A_n])

would result in being able to generate

class A_1B_x(A_1, B_x):
  pass
class A_2B_x(A_2, B_x):
  pass

# ... and so on

Notably, I want to be able to references these generated classes in another file to instantiate them by name.

Digging around, I found things like Dynamically mixin a base class to an instance in Python but they change an instance rather than generating classes.

Tarik
  • 10,810
  • 2
  • 26
  • 40

1 Answers1

2

Use type(name, bases, dict) because you're basically working with metaclasses, but in a simplified way.

class X:
    pass

class Y:
    pass

XY = type("xy", (X, Y), {})

print(XY.__mro__)
# (<class '__main__.xy'>, <class '__main__.X'>, <class '__main__.Y'>, <class 'object'>)

(__mro__ + explanation)

And join it with itertools.combinations() to generate all of the possible bases dynamically (or itertools.permutations() or itertools.product() even):

combinations([X, Y], 2)
# <itertools.combinations object at 0x7fbbc2ced4a0>

list(combinations([X, Y], 2))
# [(<class '__main__.X'>, <class '__main__.Y'>)]

Therefore kind of like this, if you have them in a separate package:

import your_package
classes = [
    getattr(your_package, cls)
    for cls in dir(your_package)
    if filter_somehow(getattr(your_package, cls))
]

combined = {}
for comb in combinations(classes, 2):
    name = "".join(cls.__name__ for cls in comb)
    combined[name] = type(name, comb, {})

print(combined)

And for referencing outside of the file either import that dictionary as a constant, or simply use globals() instead of combined and you'll dynamically create such variables in the module's namespace.

For filtering isinstance(<class>, type) should be helpful.

Or you can filter them by e.g. __name__ attribute) or don't have anything else in such a package + ignore all dunder parts:

# empty test.py file 
import test
dir(test)
# ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']

... or create some base class for all of your classes and then use issubclass() to fetch only these from dir(package).

For the common function, this should suffice:

# f(B_x, [A_1, A_2, ..., A_n])
def f(base, mixins):
    classes = []
    for mixin in mixins:
        bases = (mixin, base)
        name = "".join(cls.__name__ for cls in bases)
        classes.append(type(name, bases, {}))
    return classes

# example
class Base: pass
class A: pass
class B: pass
class C: pass

f(Base, [A,B,C])
# [<class '__main__.ABase'>, <class '__main__.BBase'>, <class '__main__.CBase'>]

And let's make it scalable a little bit:

# f(B_x, [A_1, A_2, ..., A_n])
from typing import Generic, Iterable
from itertools import combinations

def f(base: Generic, mixins: Iterable, base_count: int = 1):
    classes = []
    for mixin_comb in combinations(mixins, base_count):
        bases = (*mixin_comb, base)
        name = "".join(cls.__name__ for cls in bases)
        classes.append(type(name, bases, {}))
    return classes

f(Base, [A,B,C], 2)
# [<class '__main__.ABBase'>, <class '__main__.ACBase'>, <class '__main__.BCBase'>]
Peter Badida
  • 11,310
  • 10
  • 44
  • 90
  • `inspect.isclass(something)` is archaic. Just use `isinstance(something, type)` – juanpa.arrivillaga Jul 25 '21 at 06:19
  • @juanpa.arrivillaga yes, that was my first thought, though it still works the same way since pretty much everything is a `type` instance (`isinstance(int, type)`). – Peter Badida Jul 25 '21 at 06:31
  • 1
    Not sure what you mean. Not everything is type instance. In particular, *classes* are instances of `type` – juanpa.arrivillaga Jul 25 '21 at 06:47
  • Back in Python 2, old-style classes were *not* instances of `type`, but in Python 3, there is no "old style" vs "new style" class distinction, *everything*, everything acts like a Python 2 "new style" class, and all classes are instances of `type`] – juanpa.arrivillaga Jul 25 '21 at 06:51
  • @juanpa.arrivillaga ahaha, my bad, I just noticed I went for `isinstance(int, type)` i.e. comparing a **class** to a `type` instead of **instance** to a `type`, thus the old `True` value. Edited. – Peter Badida Jul 25 '21 at 06:58