2

I'm expereriencing an odd behaviour within the __new__ method of a Python metaclass. I know the following code works fine:

def create_property(name, _type):

    def getter(self):
        return self.__dict__.get(name)

    def setter(self, val):
        if isinstance(val, _type):
            self.__dict__[name] = val
        else:
            raise ValueError("Type not correct.")

    return property(getter, setter)


class Meta(type):

    def __new__(cls, clsname, bases, clsdict):
        for key, val in clsdict.items():

            if isinstance(val, type):
                clsdict[key] = create_property(key, val)

        return super().__new__(cls, clsname, bases, clsdict)

But when avoiding defining the define_property function and putting the code inside the for within the __new__ weird stuff happens. The following is the modified code:

class Meta(type):

    def __new__(meta, name, bases, clsdict):

        for attr, data_type in clsdict.items():
            if not attr.startswith("_"):

                def getter(self):
                    return self.__dict__[attr]

                def setter(self, val):
                    if isinstance(val, data_type):
                        self.__dict__[attr] = val
                    else:
                        raise ValueError(
                            "Attribute '" + attr + "' must be " + str(data_type) + ".")

                clsdict[attr] = property(getter, setter)

        return super().__new__(meta, name, bases, clsdict)

The idea is being able to create classes that behave like forms, i.e:

class Company(metaclass=Meta):
    name = str
    stock_value = float
    employees = list

if __name__ == '__main__':

    c = Company()
    c.name = 'Apple'
    c.stock_value = 125.78
    c.employees = ['Tim Cook', 'Kevin Lynch']

    print(c.name, c.stock_value, c.employees, sep=', ')

When executed, different errors start to happen, such as:

Traceback (most recent call last):
  File "main.py", line 37, in <module>
    c.name = 'Apple'
  File "main.py", line 13, in setter
    if isinstance(val, data_type):
TypeError: isinstance() arg 2 must be a type or tuple of types

Traceback (most recent call last):
  File "main.py", line 38, in <module>
    c.stock_value = 125.78
  File "main.py", line 17, in setter
    "Attribute '" + attr + "' must be " + str(data_type) + ".")
ValueError: Attribute 'name' must be <class 'str'>.

Traceback (most recent call last):
  File "main.py", line 37, in <module>
    c.name = 'Apple'
  File "main.py", line 17, in setter
    "Attribute '" + attr + "' must be " + str(data_type) + ".")
ValueError: Attribute 'stock_value' must be <class 'float'>.

Traceback (most recent call last):
  File "main.py", line 37, in <module>
    c.name = 'Apple'
  File "main.py", line 17, in setter
    "Attribute '" + attr + "' must be " + str(data_type) + ".")
ValueError: Attribute 'employees' must be <class 'list'>.

So, what is going on here? What is the difference between having the create_property defined separately than within the __new__ method?

Rodolfo Palma
  • 2,831
  • 3
  • 16
  • 11
  • See [Local variables in Python nested functions](http://stackoverflow.com/questions/12423614/local-variables-in-python-nested-functions). Curiously, this same topic came up yesterday in [Python Dispatcher Definitions in a Function](http://stackoverflow.com/questions/29541833/python-dispatcher-definitions-in-a-function) – PM 2Ring Apr 10 '15 at 12:25

1 Answers1

1

That's due to how the scoping and variable binding works in python. You define a function in a loop which accesses a local variable; but this local variable is looked up during execution of the function, not bound during its definition:

fcts = []
for x in range(10):
    def f(): print x
    fcts.append(f)
for f in fcts: f() #prints '9' 10 times, as x is 9 after the loop

As you've discovered, you can simply create a closure over the current loop value by using an utility function:

fcts = []
def make_f(x):
    def f(): print x
    return f

for x in range(10):
    fcts.append(make_f(x))
for f in fcts: f() #prints '0' to '9'

Another possibility is to (ab)use a default argument, as those are assigned during function creation:

fcts = []
for x in range(10):
    def f(n=x): print n
    fcts.append(f)
for f in fcts: f() #prints '0' to '9'
l4mpi
  • 5,103
  • 3
  • 34
  • 54