Method parameter changes the outcome. How?

Viewed 42

I'm working on a Python module that can output HTML code. When I attempt to make child elements and try to add an attribute (such as an id or style), it would add the attributes to the parent and all other children; however I changed a few things in the class and it fixed it and it has me puzzled on how it works. The original class __init__ looked like so:

ezhtml.py (class Element)

    def __init__(self, tag_: str, attr=dict(), text=str()):
        '''
            tag_ : the HTML tag for the element
            attr : a dictionary containing any attributes required for the element (can be added later)
            text : the innerText for the HTML element
        '''
        self.__tag = tag_
        self.__Attributes = attr
        self.__children = {}
        self.__text_order = True
        self.__text = text

        for key in self.__Attributes.keys():
            setattr(self, key, self.__Attributes[key])

And the code to make a simple site looks like this:

sample.py

from ezhtml import *

html = Element("html")

head = Element('head')
html.appendChild(head)
html.head.appendChild(Element('title', text='Title To My Webpage'))

html.appendChild(Element('body'))

html.body.appendChild(Element('h1'))
html.body.h1.add_attr('style', 'background-color: black; color: white; text-align: center;')
html.body.h1.text = "Hello world"
print(html)

And the desired output is the following:

<html>
        <head>
                <title>Title To My Webpage</title>
        </head>
        <body>
                <h1 style="background-color: black; color: white; text-align: center;">Hello world</h1>
        </body>
</html>

Yet the output I get is this:

<html style="background-color: black; color: white; text-align: center;">
        <head style="background-color: black; color: white; text-align: center;">
                <title style="background-color: black; color: white; text-align: center;">Title To My Webpage</title>
        </head>
        <body style="background-color: black; color: white; text-align: center;">
                <h1 style="background-color: black; color: white; text-align: center;">Hello world</h1>
        </body>
</html>

Then I messed around and changed the __init__ method to the following:

def __init__(self, tag_: str, attr=None, text=str()):
        '''
            tag_ : the HTML tag for the element
            attr : a dictionary containing any attributes required for the element (can be added later)
            text : the innerText for the HTML element
        '''
        self.__tag = tag_
        if attr is None:
            self.__Attributes = dict()
        else:
            self.__Attributes = attr
        self.__children = {}
        self.__text_order = True
        self.__text = text

        for key in self.__Attributes.keys():
            setattr(self, key, self.__Attributes[key])

And that gave me the desired output. I don't see how that should change anything, but it worked and gave me the result I wanted. I'm just curious how or why this works.

3 Answers

This behavior arises because you have "attr=dict()" in the definition of your Elements class. This means that a single dictionary object is created, when Elements is defined (not a new one each time new Elements objects are created). Consequently, each new Elements object works with the same dictionary object. The "add_attr" method will probably update the "attr" dictionary - but it will do so for all Elements class members. Your second definition of Elements is exactly the way how to avoid this problem.

See also here: https://docs.python-guide.org/writing/gotchas/

The first time python ( the program ) sees your function/method (__init__) it evaluates and runs the assignment attr=dict() only once, meaning that the dict() function is evaluated and run only once and its reference is assigned to the arg attr of your function, and this happen the first time the function __init__ is evaluated.

then each time you create an instance without specifying the attr, the same reference is used again and again, creating a shared dictionary reference between all instances.

It s a common "gottcha", you can recreate the same behavior with a function and an empty list :

>>> def f(x=[]) :
...     x.append(1)
...     print(x)
...
>>> f()
[1]
>>> f()
[1, 1]
>>> f()
[1, 1, 1]
>>> f()
[1, 1, 1, 1]
>>> f([2])
[2, 1]
>>> f([2])
[2, 1]
>>> f()
[1, 1, 1, 1, 1]

I couldn't find any documentation for ezhtml on PyPi.org, but if assigning an empty dict fixes the issue then there's a default string of CSS styling being plugged in.

For the record that is some really nasty HTML it spits out without your fix. I'd like to see the package documentation to learn more, but it seems to be something private.

Related