Reputation: 4439
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 ?
Upvotes: 1
Views: 87
Reputation: 6160
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
Upvotes: 1
Reputation: 101989
Creating functions in a loop is tricky:
>>> lambdas = [(lambda: i) for i in range(5)]
>>> for lamb in lambdas:
... print(lamb())
...
4
4
4
4
4
Note that all lambda
s refer to the value that i
assumed in the last iteration.
When you create a function python associates a closure to it, which tells the interpreter which non local variables the function should use:
>>> lambdas[0].__closure__[0]
<cell at 0x7f675ab2dc90: int object at 0x9451e0>
However it refers to the variable, not the actual object contained when the function was defined. This would require a more complicated handling of the function frames.
this means that following iterations change the value contained in this cell, and in the end only the last iteration is significant:
>>> lambdas[0].__closure__[0].cell_contents
4
If you want to refer to previous values you can use a default value for an argument:
>>> lambdas = [(lambda i=i: i) for i in range(5)]
>>> for lamb in lambdas:
... print(lamb())
...
0
1
2
3
4
Concerning the second version. property
is implemented as a descriptor (see also this answer) and hence it must be set in the class in order for it to work properly. The same is true for other decorators such as staticmethod
and classmethod
. Putting them in an instance will just return the property
object, as you observed.
The line:
setattr(notebook,'_notes',{note.name : note.size for note in notes})
Can be safely changed to the simpler and more readable:
notebook._notes = {note.name : note.size for note in notes}
Upvotes: 1