3

In C++ or other compile/link based OOP languages, it is relatively easy to implement self-registering factory. Is it possible to do it in Python?

For example, I have a base method class: Vehicle @ vehicle.py, which is abstract. I will have a factory for vehicles @ vehicle_factory.py. I will have other concrete vehicles: Class Car(Vehicle) @ car.py, Class Truck(Vehicle) @ truck.py, etc.

I wish I do not have to touch vehicle_factory.py to register all these concrete vehicles. Every time I create a new vehicle class (such as Bus), I only need to work on its own module bus.py using self-registration. Can this be implemented in Python?

Justin
  • 1,006
  • 12
  • 25
Sima
  • 31
  • 1
  • 4
  • You can do this using decorators. Search on here or the web at large about registering classes using decorators in python. – T Burgis May 03 '19 at 15:58
  • Duplicate of: https://stackoverflow.com/questions/51072941/which-way-is-ideal-for-python-factory-registration – JMAA May 03 '19 at 16:01
  • You can use `__new__()` to make subclasses auto-register themselves. See my answer to [Improper use of `__new__` to generate classes?](https://stackoverflow.com/questions/28035685/improper-use-of-new-to-generate-classes). – martineau May 03 '19 at 16:19

2 Answers2

2

Sure. This could be done easily with class decorators:

# vehicle.py
class Vehicle(object):
    pass

_vehicle_types = []

def vehicle_type(cls):
    _vehicle_types.append(cls)

def make_vehicle(type_):
    for vt in _vehicle_types:
        if vt.type_ == type_:
            return vt()
    raise ValueError('No vehicle of type "{}" exists'.format(type_))

# car.py
import vehicle

@vehicle.vehicle_type
class Car(vehicle.Vehicle):
    type_ = 'car'

# bus.py
import vehicle

@vehicle.vehicle_type
class Bus(vehicle.Vehicle):
    type_ = 'bus'

However, you'd have to make sure car.py and bus.py were loaded by the interpreter at some point before calling make_vehicle("car") or make_vehicle("bus") respectively.

Kyle Willmon
  • 709
  • 3
  • 13
  • I wouldn't call having to use a decorator "self registering". – martineau May 03 '19 at 16:09
  • @martineau I thought the question was simply asking how to avoid having to edit `vehicle_factory.py` when adding new classes. Not completely avoiding any additional syntax. – Kyle Willmon May 03 '19 at 16:12
  • I think it's more than that—namely the _automatic_ registration of subclasses. – martineau May 03 '19 at 16:33
  • Seems pretty good. I have a question, how to automatically load these modules? Assume I have 100 such vehicles, but I know where they are. – Sima May 03 '19 at 17:58
  • If they're all in the same folder, you can load them like this: https://stackoverflow.com/questions/1057431/how-to-load-all-modules-in-a-folder – Kyle Willmon May 03 '19 at 18:31
0

Thank you all, especially @Kyle Willmon and @martineau. I compiled your answers into an attempt here: In vehicle.py

class Vehicle(object):
    class NoAccess(Exception): pass
    class Unknown(Exception): pass

    @classmethod
    def _get_all_subclasses(cls):
        for subclass in cls.__subclasses__():
            yield subclass
            for subclass in subclass._get_all_subclasses():
                yield subclass

    @classmethod
    def _get_name(cls, s):
        return s.lower()

    def __new__(cls, name):
        name = cls._get_name(name)
        for subclass in cls._get_all_subclasses():
            if subclass.name == name:
                # Using "object" base class method avoids recursion here.
                return object.__new__(subclass)
        else:  # no subclass with matching name found (and no default defined)
            raise Vehicle.Unknown('name "{}" has no known vehicle type'.format(name))

    def drive(self):
        raise NotImplementedError  

In car.py

from vehicle import *

class Car(Vehicle):
    name = 'car'
    wheels = 4

    def __init__ (self, name):
        pass

    def drive(self):
        print('I am driving a car now, and it has ' + str(self.wheels) + ' wheels')

In truck.py

from vehicle import *

class Truck(Vehicle):
    name = 'truck'
    wheels = 18

    def __init__(self, name):
        pass

    def drive(self):
        print('I am driving a truck now, and it has ' + str(self.wheels) + ' wheels')

In main.py

import pkgutil
import sys

from vehicle import *

def load_all_modules_from_dir(dirname):
    for importer, package_name, _ in pkgutil.iter_modules([dirname]):
        if package_name not in sys.modules and package_name != 'main':
            module = importer.find_module(package_name).load_module(package_name)

load_all_modules_from_dir('.')

v1 = Vehicle('car')
v1.drive()
print(type(v1))

v2 = Vehicle('truck')
v2.drive()
print(type(v2))
Sima
  • 31
  • 1
  • 4