Search code examples
pythondynamicpropertieslambdasetattr

Adding properties dynamically using functions created dynamically


I would like to implement something that would work like this:

memo = Note("memo",5)
report = Note("report",20)

notebook = Notebook(memo,report)

print str(notebook.memo) # 5 expected
print str(notebook.report) # 20 expected

Inspired by: http://znasibov.info/blog/html/2010/03/10/python-classes-dynamic-properties.html and How to implement property() with dynamic name (in python) , I implemented the following code:

class Note:
    def __init__(self,name,size):
        self.name = name
        self.size = size

class Notebook(object):
    def __new__(cls,*notes):
        notebook = object.__new__(cls)
        setattr(notebook,'_notes',{note.name : note.size for note in notes})
        functions = [lambda notebook : notebook._notes[note.name] for note in notes]
        for note,function in zip(notes,functions) :
            #version 1
            setattr(notebook.__class__, note.name, property(function))
            #version 2 -- note : __class__ removed
            #setattr(notebook, note.name, property(function))
        return notebook

note: I know for this minimal code use of __new__ instead of __init__ is not justified, but this will be required later on when I use subclasses of Notebook

If I use version 1: 1. instead of having 5 and 20 being printed, it prints 20 and 20. I do not get why. Printing the functions shows an array of functions with different addresses. 2. I used __class__ inspired by the blog entry given above, but I am not sure what it does. It makes the property a class property ? (which would be real bad in my case)

If I use version 2: prints something like property object at 0x7fb86a9d9b50. This seems to make sense, but I am not sure I understand why it does not print the same thing for version 1.

Is there a way to fix this, using either version (or another completely different approach) ?


Edit

An interesting answer for solving the issue was proposed. Here the corresponding code:

class Note:
    def __init__(self,name,value):
        self.name = name
        self.size = value
    def _get_size(self,notebook_class=None): return self.size+1

class Notebook(object):
    def __new__(cls,*notes):
        notebook = object.__new__(cls)
        notebook._notes = {note.name : note.size for note in notes}
        for note in notes : setattr(notebook.__class__, note.name, property(note._get_size))
        return notebook

Issue is : now this test code is not giving the desired output:

memo1 = Note("memo",5)
report1 = Note("report",20)
notebook1 = Notebook(memo1,report1)
print str(notebook1.memo) # print 6 as expected (function of Note return size+1)
print str(notebook1.report) # print 21 as expected

memo2 = Note("memo",35)
report2 = Note("report",40)
notebook2 = Notebook(memo2,report2)
print str(notebook2.memo) # print 36 as expected
print str(notebook2.report) # print 41 expected

print str(notebook1.memo) # does not print 6 but 36 !
print str(notebook1.report) # does not print 21 but 41 !

I guess this was to be expected as the property was added to the class .... Anyway to overcome this issue ?


Solution

  • Some more food for though. To simply obtain what you want to do in your first set of code, you can do that without all the extra tricks.

    The simplest way to do it is set the attributes to the desired one directly. (code consolidated in improper manors simply to save space)

    class Note:
        def __init__(self, name, value): self.name, self._size = name, value
        size = property(lambda x: x._size+1)
    
    class Notebook(object):
        def __new__(cls, *notes):
            notebook = object.__new__(cls)
            notebook._notes = {note.name: note.size for note in notes}
            for note in notes: setattr(notebook, note.name, note.size)
            return notebook
    
    
    memo1, report1 = Note("memo", 5), Note("report", 20)
    notebook1 = Notebook(memo1, report1)
    
    print(notebook1.memo, notebook1.report) # 6 21
    
    memo2, report2 = Note("memo", 35), Note("report", 40)
    notebook2 = Notebook(memo2,report2)
    
    print(notebook2.memo, notebook2.report) # 36 41
    print(notebook1.memo, notebook1.report) # 6 21
    notebook1.memo += 5
    print(notebook1.memo) # 11
    print(memo1.size) # 6
    memo1.size += 5 # AttributeError: can't set attribute
    

    The second way would be to have the notebook literally be a container for all the notes you pass to it. This way it would simply update the original class objects, and is basically just a holder for them.

    class Note2(object):
        def __init__(self, name, value): self.name, self._size = name, value
        def _set_size(self, value): self._size = value
        size = property(lambda x: x._size+1, _set_size)
        def __repr__(self): return str(self.size) #simple trick to gain visual access to .size
    
    class Notebook2(object):
        def __new__(cls, *notes):
            notebook = object.__new__(cls)
            notebook._notes = {note.name: note.size for note in notes}
            for note in notes: setattr(notebook, note.name, note)
            return notebook
    
    memo1, report1 = Note2("memo", 5), Note2("report", 20)
    notebook1 = Notebook2(memo1, report1)
    print(notebook1.memo, notebook1.report) # 6 21
    memo2, report2 = Note2("memo", 35), Note2("report", 40)
    notebook2 = Notebook2(memo2, report2)
    print( notebook2.memo, notebook2.report) # 36 41
    print(notebook1.memo, notebook1.report) # 6 21
    notebook1.memo.size += 16
    print(notebook1.memo) # 23
    print(memo1) # 23, Notice this will also set the original objects value to the new value as well
    notebook1.memo += 15 # TypeError: unsupported operand type(s) for +=: 'Note2' and 'int' - It is true without making it as a property does make it less effective to work with
    

    It should also be possible to do as in your provided link suggests to make each Note class a member of Notebook with a leading underscore (i.e. notebook._memo) and then make a property for Notebook which would link Note name to size (i.e. notebook.memo would be a link to notebook._memo.size). Hope these examples help.


    Original answer.

    Interesting idea, to simply get it working here is a hack of your original version:

    class Note(object):
        def __init__(self,name, size):
            self.name = name
            self._size = size
    
        def _get_size(self, notebook_class=None):
            return self._size
    
        def _set_size(self, notebook_class=None, size=0):
            self._size = size
    
    class Notebook(object):
        def __new__(cls,*notes):
            notebook = object.__new__(cls)
            for note in notes:
                setattr(notebook.__class__, note.name, property(note._get_size, note._set_size))
            return notebook
    

    However you seem to be removing each Note class when you ingest them into Notebook anyways so you could do something much easier:

    class Note(object):
        def __init__(self, name, size):
            self.name = name
            self.size = size
    
    class Notebook(object):
        def __new__(cls, *notes):
            notebook = object.__new__(cls)
            for note in notes:
                setattr(notebook.__class__, note.name, note.size)
            return notebook
    

    To be any more helpful I would really need to know the goal or a general idea of where you want to take this. It seems confusing to set the properties in such an odd way, yet only do it once at the creation of the class as opposed to the examples of being able to dynamical add and remove them.

    Hope this helped