0
\$\begingroup\$

I'm wondering if this is good design, or if the definition of the __init__, __call__, and __repr__ methods should be inside of mesh_class_meta itself instead of inside of the factory, mesh_class.

If it is better to for them to be defined inside of the metaclass definition, please explain specifically why? Or is there another even better way that I haven't thought of?

debugprint = print

##Metaclass for mesh data subclasses
##Receives a list of valid fields in a string (similar to namedtuple)
##Adds those fields to __slots__
class mesh_class_meta(type):
    def __new__(meta, name, bases, fields):
        if isinstance(fields, str): fields = fields.replace(',',' ').split()
        fields = tuple(fields)
        debugprint('\n-------------------------')
        debugprint('Allocating memory for new mesh_class', name)
        debugprint(meta)
        debugprint(bases)
        debugprint(fields)
        debugprint('\n')
        dctn = {}
        dctn['__slots__'] = list(fields)
        return super(mesh_class_meta, meta).__new__(meta, name, bases, dctn)

    def __init__(klass, name, bases, fields):
        debugprint('\n-------------------------')
        debugprint('Initializing class', name)
        debugprint(klass)
        debugprint(bases)
        debugprint(fields)
        debugprint('\n')
        super(mesh_class_meta, klass).__init__(name, bases, {})

##mesh_class_meta factory
##__init__, __call__, and __repr__ for the subclasses are defined here
##__call__ allows the values of slot fields to be changed by calling the class
##with a valid dictionary argument 
def mesh_class(name, fields):
    new_class = mesh_class_meta(name, (object,), fields)
    def new_class__init__(self, *args, **kwargs):
        for slot, arg in enumerate(args):
            setattr(self, str(self.__slots__[slot]), arg)
        for k,v in kwargs.items():
            setattr(self, k, v)
        debugprint('\n-------------------------')
        debugprint('New', name, 'object created using', tuple(({attr: getattr(self, attr)} for attr in self.__slots__)))
        debugprint('\n')
        super(new_class, self).__init__() #Not sure if this is correct; seems to work
    new_class.__init__ = new_class__init__
    def new_class__call__(self, **kwargs):
        for k,v in kwargs.items():
            setattr(self, k, v)
        debugprint('\n-------------------------')
        debugprint('Fields in', name, 'object edited:', tuple(({k: v} for k,v in kwargs.items())))
        debugprint('\n')
    new_class.__call__ = new_class__call__
    def new_class__repr__(self):
        return str(name) + str({attr: getattr(self, attr) for attr in self.__slots__})
    new_class.__repr__ = new_class__repr__
    return new_class

##mesh classes created
Point = mesh_class('Point', 'x y')
Elpoints = mesh_class('Elpoints','i j k l')
Boundpoint = mesh_class('Boundpoint','b')

##Testing procedures##

##Create some instances
Points = [Point(p,p) for p in range(0,4,1)]
e1 = Elpoints(*tuple(p for p in Points))
b1 = Boundpoint(Points[2])

##Do some tests
assert Points[3].x == 3
Points[3](x = 4)
assert e1.l.x == 4
e1.j(y = 5)
assert Points[1].y == 5
e1.k(x=7)
assert b1.b.x == 7
assert Points[2].x == 7
e1(i = Points[3])
b1(b = Points[3])
Points[3](x=-1,y=-1)
assert e1.i.x, e1.i.y == (-1 , -1)
assert b1.b.x, b1.b.y == (-1 , -1)
\$\endgroup\$
  • 1
    \$\begingroup\$ Generally classes are CamelCased, and variables are lowercase_with_underscore. \$\endgroup\$ – Ethan Bierlein Nov 6 '14 at 19:33
  • \$\begingroup\$ Thanks. These are metaclasses, though. Is the same true of those? Even though they are technically classes, I think they are usually lowercase (similar to class descriptors). Will edit my actual classes (e.g. Elpoints) to be CamelCase. \$\endgroup\$ – Rick supports Monica Nov 6 '14 at 20:18
  • \$\begingroup\$ I think whether it's a metaclass or a regular class, I believe it's still camel cased. \$\endgroup\$ – Ethan Bierlein Nov 6 '14 at 20:58
  • 1
    \$\begingroup\$ Can you clarify what this code does that is different from namedtuple? Otherwise my review would be a suggestion to use namedtuple and confusion at the juxtaposition of beginner and a question on metaclasses. After all, the best metaclass is one you don't have to write. \$\endgroup\$ – Michael Urman Nov 7 '14 at 14:21
  • \$\begingroup\$ Beginner because I'm new to Python in general (less than a month of using it), and new to metaclasses specifically. Metaclasses because I needed to generate multiple, similar containers (points, 5 different kinds of elements, boundaries, and 3 different kinds of materials, but all having the same slotted structure skeleton), and don't want to have to maintain them all. And not using namedtuple because I need the structure to be mutable, and because of memory issues since there are thousands of instances at a time. \$\endgroup\$ – Rick supports Monica Nov 7 '14 at 18:35
1
\$\begingroup\$

It would seem to me that a simpler way (without a metaclass) to do more or less the same would be

  1. Define a base class SlottedClass with empty __slots__.
  2. Define __init__, __call__, __repr__ etc. as normal instance methods in SlottedClass.
  3. The class factory only needs to set __slots__ on the subclass it creates:

    def mesh_class(name, fields):
        if isinstance(fields, str): 
            fields = fields.replace(',',' ').split()
        ##Test for repeated fields
        if len(set(fields)) < len(fields): 
            raise Exception
        ##Test for fields beginning with underscore
        for field in fields:
            if field[0] == '_': 
                raise Exception
        new_class = type(name, (SlottedClass,), {'__slots__': fields})
        return new_class
    

Note by the way that list.sort() sorts in place and always returns None, so the code below does not do what it says. I've fixed it the code above.

    ##Test for repeated fields
    if list(set(fields)).sort() != list(fields).sort(): raise Exception
\$\endgroup\$
  • \$\begingroup\$ This is so much better than what I was doing, for so many reasons. I think I like metaclasses too much - I have a tendency to want to use them when another solution (such as this one) is actually more obvious. This is why I tagged this question "beginner": I had a feeling I didn't need to use it but didn't know what else to do. \$\endgroup\$ – Rick supports Monica Nov 9 '14 at 22:46
0
\$\begingroup\$

After trying it both ways I've decided that moving all of the methods into the metaclass definition is definitely an improvement. It allows slotted_class_meta to be generalized, and this way I can create multiple factories based on it that do different things (like a factory that tests for a valid range for the field values, e.g. the x and y value of a Point).

I think it's also less confusing this way; it is obvious what the factory is doing, and the separation of concerns just makes more sense.

debugprint = print

##Metaclass for a mesh data subclasses
##Receives a list of fields in a string (similar to namedtuple)
##Adds those fields to __slots__
class slotted_class_meta(type):
    def __new__(meta, name, bases, fields):
        if isinstance(fields, str): fields = fields.replace(',',' ').split()
        fields = tuple(fields)
        ##Test for repeated fields
        if list(set(fields)).sort() != list(fields).sort(): raise Exception
        ##Test for fields beginner with underscore
        for field in fields:
            if field[0] == '_': raise  Exception
        debugprint('\n-------------------------')
        debugprint('Allocating memory for new slotted_class', name)
        debugprint(meta)
        debugprint(*bases)
        debugprint(*fields)
        debugprint('\n')
        dctn = {}
        dctn['__slots__'] = list(fields)
        return super(slotted_class_meta, meta).__new__(meta, name, bases, dctn)

    def __init__(klass, name, bases, fields):
        klass.mesh_class_name = name
        debugprint('\n-------------------------')
        debugprint('Initializing class', klass.mesh_class_name)
        debugprint(klass)
        debugprint(*bases)
        debugprint(fields)
        debugprint('\n')
        klass.__init__ = slotted_class_meta.instance__init__
        klass.__setslots = slotted_class_meta.__setslots
        klass.__call__ = slotted_class_meta.instance__call__
        klass.__repr__ = slotted_class_meta.instance__repr__
        klass.__iter__ = slotted_class_meta.instance__iter__
        klass.__reversed__ = slotted_class_meta.instance__reversed__
        klass.__eq__ = slotted_class_meta.instance__eq__
        klass.__ne__ = slotted_class_meta.instance__ne__
        super(slotted_class_meta, klass).__init__(name, bases, {})

    def __repr__(klass):
        return klass.mesh_class_name

    @staticmethod
    def instance__init__(self, *args, **kwargs):
        debugprint('\n-------------------------')
        debugprint('New', str(type(self)), 'object created using:')
        self.__setslots(*args, **kwargs)
        debugprint('\n')
        super(type(self), self).__init__()

    @staticmethod
    def instance__call__(self, *args, **kwargs):
        debugprint('\n-------------------------')
        debugprint('Fields in', str(type(self)), 'object edited:')
        self.__setslots(*args, **kwargs)
        debugprint('\n')

    @staticmethod
    def __setslots(self, *args, **kwargs):
        for slot, arg in enumerate(args):
            setattr(self, str(self.__slots__[slot]), arg)
            debugprint({str(self.__slots__[slot]): arg})
        for k,v in kwargs.items():
            setattr(self, k, v)
            debugprint({str(k): v})

    @staticmethod
    def instance__repr__(self):
        return str(type(self)) + str({attr: getattr(self, attr) for attr in self.__slots__})

    @staticmethod
    def instance__iter__(self):
        for slot in self.__slots__:
            yield getattr(self, slot)

    @staticmethod
    def instance__reversed__(self):
        for slot in reversed(self.__slots__):
            yield getattr(self, slot)

    @staticmethod
    def instance__eq__(self, other):
        if set(self.__slots__) != set(other.__slots__): return False
        for slot in self.__slots__:
            if getattr(self, slot) != getattr(other,slot):
                return False
        return True

    @staticmethod
    def instance__ne__(self, other):
        return not self == other

##slotted_class_meta factory
def mesh_class(name, fields):
    new_class = slotted_class_meta(str(name), (object,), str(fields))
    return new_class

##mesh classes created
Point = mesh_class('Point', 'x y')
Elpoints = mesh_class('Elpoints','i j k l')
Boundpoint = mesh_class('Boundpoint','b')

##Testing procedures##

##Create some instances
Points = [Point(p,p) for p in range(0,4,1)]
e1 = Elpoints(*tuple(p for p in Points))
b1 = Boundpoint(Points[2])

##Do some assignment tests
assert Points[3].x == 3
Points[3](x = 4)
assert e1.l.x == 4
e1.j(y = 5)
assert Points[1].y == 5
e1.k(x=7)
assert b1.b.x == 7
assert Points[2].x == 7
e1(i = Points[3])
b1(b = Points[3])
Points[3](x=-1,y=-1)
assert e1.i.x, e1.i.y == (-1 , -1)
assert b1.b.x, b1.b.y == (-1 , -1)

##Test object comparisons
MC1 = mesh_class('MC1','a b c')
MC2 = mesh_class('MC2','a b')
MC3 = mesh_class('MC3','b a')
MC4 = mesh_class('MC4','x,y')
mc1 = MC1(1,2,3)
mc2 = MC2(1,2)
mc3 = MC3(2,1)
mc4 = MC4(1,2)

assert mc1 != mc2
assert mc2 != mc1
assert mc2 == mc3
assert mc3 == mc2
assert mc2 != mc4
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.