2

What I've succeeded to do so far:

I've made an elem class to represent html elements (div, html, span, body, etc.).

I'm able to derivate this class like this to make subclasses for each element:

class elem:
    def __init__(self, content="", tag="div", attr={}, tag_type="double"):
        """Builds the element."""
        self.tag = tag
        self.attr = attr
        self.content = content
        self.tag_type = tag_type

class head(elem):
    """A head html element."""

    def __init__(self, content=None, **kwargs):
        super().__init__(tag="head", content=content, **kwargs)

And it works pretty well.

But I have to write this for each subclass declaration, and that's pretty repetitive and redundant if I want to do every HTML tag type.

So I was trying to make a make_elem() function that would make my class by taking the corresponding tag name as a string parameter.

So instead of the previous class definition, I would simply have something like this:

head = make_elem_class("head")

Where I'm stuck

This function should create a class. And the __init__() method from this class should call the __init__() method from the class it inherits from.

I tried to make this make_elem_class() function and it looked like this :

def make_elem_class(name):
    """Dynamically creates the class with a type() call."""

    def init(self, content=None, **kwargs):
        super().__init__(tag=name, content=None, **kwargs)

    return type(name, (elem,), {"__init__" : init})

But when running html = make_elem_class('html'), then html("html element") I get the following error:

Traceback (most recent call last):
  File "elements.py", line 118, in <module>
    html("html element")
  File "elements.py", line 20, in init
    super().__init__(tag=name, content=None, **kwargs)
TypeError: object.__init__() takes no parameters

I guess that it has something to do with the empty super() call, so I tried with super(elem, self) instead. But it obviously doesn't work better.

How could I achieve this?

NB : If I remove the "__init__":init from the dictionnary in the type() call, it works fine but the tag isn't correctly set in my elem. I've also tried to directly pass {"tag":name} to type() but it didn't work either.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
vmonteco
  • 14,136
  • 15
  • 55
  • 86
  • Can you produce a simplified `elem` class too please? I also can't reproduce your exact exception, I get `RuntimeError: super(): __class__ cell not found` instead. Are you perhaps using `make_elem_class` as a static or classmethod somewhere? – Martijn Pieters Apr 27 '16 at 12:59
  • Or, alternatively, did you perhaps set `__class__ = elem` somewhere? – Martijn Pieters Apr 27 '16 at 13:03
  • @MartijnPieters Sure! I edited my post, the `__init__()` method is complete. And no `__class__ = elem` in my code. – vmonteco Apr 27 '16 at 13:05
  • Try this yourself; you can't get your exception with just the `elem` class as you posted (the `make_html()` method is not needed here, it is never reached). – Martijn Pieters Apr 27 '16 at 13:10

3 Answers3

4

You can't use the no-argument form of super() here, as there is no class statement here to provide the context that that function normally needs.

Or rather, you can't unless you provide that context yourself; you need to set the name __class__ as a closure here:

def make_elem_class(name):
    """Dynamically creates the class with a type() call."""

    def init(self, content=None, **kwargs):
        super().__init__(tag=name, content=content, **kwargs)

    __class__ = type(name, (elem,), {"__init__" : init})
    return __class__

super() automatically will take the __class__ value from the closure. Note that I pass on the value for content, not None, to the elem.__init__ method; you wouldn't want to lose that value.

If that is too magical for you, explicitly name the class and self when calling super(); again, the class is going to be taken from the closure:

def make_elem_class(name):
    """Dynamically creates the class with a type() call."""

    def init(self, content=None, **kwargs):
        super(elemcls, self).__init__(tag=name, content=content, **kwargs)

    elemcls = type(name, (elem,), {"__init__" : init})
    return elemcls
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Your solutions work both perfectly! It may be simplified (I'm not a Python expert and may not have the right vocabulary), but does `super()` search for the class name we pass to it in the local scope (`__class__` by default if no argument is passed, and `__class__` is created when using a `class myclass():` syntaxe?)? – vmonteco Apr 27 '16 at 13:16
  • @vmonteco: see the already linked [Why is Python 3.x's super() magic?](https://stackoverflow.com/q/19608134); `super()` looks for a *closure cell*, and this closure cell is automatically provided by the Python compiler when you a) create a method in a `class` block, and `b` the name `super` is used in that method. – Martijn Pieters Apr 27 '16 at 13:24
  • @vmonteco: normally closure cells are only created when you use a name from a parent function scope (like `name` in your `make_elem_class`, to be able to use that in the `init()` function such a cell must be created). – Martijn Pieters Apr 27 '16 at 13:24
1

What's about a more straight-forward solution like inferring the tag for the class __name__?

class elem:
    def __init__(self, content="", tag=None, attr={}, tag_type="double"):
        """Builds the element."""
        self.tag = tag or self.__class__.__name__
        ...

And then:

class div(elem): pass
class head(elem): "Optional docstring for <head>"
...

A bit less magic (controversial), and a bit more explicit. :-)

Eldar Abusalimov
  • 24,387
  • 4
  • 67
  • 71
  • Not sure that that is less magic, honestly. And we are only lucky that there are no HTML tags that happen to be [reserved Python keywords](https://docs.python.org/3/reference/lexical_analysis.html#keywords). Using a class attribute may be the better idea `self.tag` would then read from `elem.tag`, which you'd override in a subclass. Easily set with `'tag': name` in the dictionary passed to `type()`. – Martijn Pieters Apr 27 '16 at 13:27
  • Fair enough! Not really less magic, probably, rather a sort of embedded DSL for this kind of things. – Eldar Abusalimov Apr 27 '16 at 13:37
0

I think this is a little bit of an XY problem. In that you've asked to how to use super in a dynamically created class, but what you really want is a less verbose way to set various class variables and defaults for your subclasses.

Since you don't expect all instances of the same tag class to share the same tag name, you might as well set it as a class variable rather than an instance variable. eg.

from abc import ABC, abstractmethod

class Elem(ABC):
    tag_type = "double" # the default tag type

    def __init__(self, content="", attr=None, tag_type=None):
        """Builds the element."""
        self.attr = attr if attr is not None else {}
        self.content = content
        if tag_type is not None:
            self.tag_type = tag_type

    @property
    @abstractmethod
    def tag(self):
        """All base classes should identify the tag they represent"""
        raise TypeError("undefined tag for {}".format(type(self)))

class Head(Elem):
    tag = "head"
    tag_type = "text"

class Div(Elem):
    tag = "div"

h = Head()
d = Div()
h1 = Head(tag_type="int")

assert h.tag == "head"
assert d.tag == "div"
assert h1.tag == "head"
assert h.tag_type == "text"
assert d.tag_type == "double"
assert h1.tag_type == "int"

You can now write very short child classes, and still have your classes explicitly declared. You'll note that I changed a couple of the defaults to None. For attr, this is because having mutable default arguments won't work how you expect -- it'll behave more like it's a shared class variable. Instead, have the default as None, if attr has not been specified then create a new attr for each instance. The second (tag_type) is so that if tag_type is specified then the instance will have it's tag_type set, but all other instances will rely on the class for the default value.

Dunes
  • 37,291
  • 7
  • 81
  • 97