質問

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 ?

役に立ちましたか?

解決

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

他のヒント

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 lambdas 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}
ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top