Python Forum

Full Version: iterate over class properties?
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
Pages: 1 2
is there a way to iterate over python property descriptors rather than __dict__ attributes?

import datetime
from datetime import date
from datetime import datetime as dt

class videofile:
    def __init__(self):
        self._date = date.today()
        self._dateadded = dt.now()
        self._title = 'New Title'
        self._plot = ''

    @property
    def title(self):
        return self._title

    @title.setter
    def title(self, value):
        self._title = value

    @property
    def plot(self):
        return self._plot

    @plot.setter
    def plot(self, value):
        self._plot = value



    @property
    def dateadded(self):
        return self._dateadded

    @dateadded.setter
    def dateadded(self, value):
        self._dateadded = value

class movie(videofile):

    def __init__(self):
        super(movie,self).__init__()
        self._outline = ''

    @property
    def outline(self):
        return self._outline

    @outline.setter
    def outline(self, value):
        self._outline = value


    @property
    def premiered(self):
        return self._date

    @premiered.setter
    def premiered(self, value):
        self._date = value


class tvepisode(videofile):

    def __init__(self):
        super(tvepisode,self).__init__()
        self._season = -1
        self._episode = -1
        self._absolute = -1

    @property
    def season(self):
        return self._season

    @season.setter
    def season(self, value):
        self._season = value

    @property
    def episode(self):
        return self._episode

    @episode.setter
    def episode(self, value):
        self._episode = value

    @property
    def absolute(self):
        return self._absolute

    @absolute.setter
    def absolute(self, value):
        self._absolute = value

    @property
    def aired(self):
        return self._date

    @aired.setter
    def aired(self, value):
        self._date = value
I want to iterate over the user values (to eventually shove them into an xml)
which i know i can do by iterating over the instance's dictionary

movieitem = movie()
movieitem.title = 'Avengers: Infinity War'
movieitem.premiered = date(2018,4,27)
movieitem.plot = 'The Avengers and their allies must be willing to sacrifice all in an attempt to defeat the powerful Thanos before his blitz of devastation and ruin puts an end to the universe.'

for attribName, attribValue in movieitem.__dict__.items():
    if isinstance(attribValue, str):
        print(attribName + ': ',attribValue )
    elif isinstance(attribValue,date):
        print(attribName + ': ',attribValue.strftime('%Y-%m-%d'))
    elif isinstance(attribValue,dt):
        print(attribName + ': ',attribValue.strftime('%Y-%m-%d %H:%M:%S'))
    else:
        print(attribName + ': ', str(attribValue))
the code above produces:

Output:
_title: Avengers: Infinity War _date: 2018-04-27 _dateadded: 2018-08-26 05:33:21 _outline: _plot: The Avengers and their allies must be willing to sacrifice all in an attempt to defeat the powerful Thanos before his blitz of devastation and ruin puts an end to the universe
.

the problem is that some of the user values are reused for different property names which is how i want them defined in the output... so is there a way to iterate over only properties instead of __dict__ attributes for instance, the self._date is used to for different properties in movies (premiered) and tvepisodes (aired)
(Aug-26-2018, 10:44 AM)NobahdiAtoll Wrote: [ -> ]is there a way to iterate over only properties instead of __dict__ attributes
There are ways of course, I give you a solution below. Before I do that, let me say that it is not a good way to handle your problem because properties are an implementation detail of your code and this should not be mixed with the problem's real world model. I mean that the list of items that you want to output should not be defined by the details of the implementation.

That said, the code below defines a function add_property_names() which adds a property_names() class method to a class and its subclasses. This method allows you to list the names of the class' properties.
In your example, you would add the code
add_property_names(videofile)
Then you could do
for name in movieitem.property_names():
    print(name, getattr(movieitem, name))
Here is the code
import inspect

def add_property_names(cls):
    def get_names(cls):
        for tp in inspect.getmro(cls):
            for name, value in vars(tp).items():
                if isinstance(value, property):
                    yield name
    def property_names(cls):
        register =  add_property_names.register
        if not cls in register:
            register[cls] = sorted(set(get_names(cls)))
        return register[cls]
    cls.property_names = classmethod(property_names)
    return cls
add_property_names.register = {}

if __name__ == '__main__':
    class A:
        pass

    class B(A):
        @property
        def foo(self):
            return 0
        
    class C(B):
        @property
        def bar(self):
            return self._bar
        
        @bar.setter
        def bar(self, value):
            self._bar = value

    add_property_names(A)

    a = A()
    print(a.property_names())
    c = C()
    print(c.property_names())
    print(C.property_names())
(Aug-26-2018, 03:21 PM)Gribouillis Wrote: [ -> ]That said, the code below defines a function add_property_names() which adds a property_names() class method to a class and its subclasses. This method allows you to list the names of the class' properties.
In your example, you would add the code
removed for length

I appreciate you pointing me to inspect that opens up a lot of opportunities.

for the record i went with a slight variation of your method that yields a tuple of all properties and their values

I use this function globally

import inspect

def getproperties(t, obj):
    for tp in inspect.getmro(t):
        for name, value in vars(tp).items():
            if isinstance(value,property):
                yield name, getattr(obj, name)
so now i can just do this
for attribName, attribValue in getproperties(movie,movieitem):
    if isinstance(attribValue, str):
        print(attribName + ':',attribValue )
    elif isinstance(attribValue, dt):
        print(attribName + ':', attribValue.strftime('%Y-%m-%d %H:%M:%S'))
    elif isinstance(attribValue,date):
        print(attribName + ':',attribValue.strftime('%Y-%m-%d'))
    else:
        print(attribName + ':', str(attribValue))
which outputs it exactley what i want for any class i provide
Output:
premiered: 2018-04-27 outline: title: Avengers: Infinity War plot: The Avengers and their allies must be willing to sacrifice all in an attempt to defeat the powerful Thanos before his blitz of devastation and ruin puts an end to the universe. dateadded: 2018-08-26 13:03:37
for the record i'm not using this for "real world" output I'm eventually going to use it for building xml from objects and its easier to iterate over properties, for me right now, cause i don't know all the tricks of python yet.
(Aug-26-2018, 06:20 PM)NobahdiAtoll Wrote: [ -> ]I use this function globally
It works. The drawback is that you're going to recompute the list of properties for each instance for which you're calling this function. I only attempted to compute the list only once for each class.
(Aug-26-2018, 07:02 PM)Gribouillis Wrote: [ -> ]It works. The drawback is that you're going to recompute the list of properties for each instance for which you're calling this function. I only attempted to compute the list only once for each class.

So does your way import the function into every class? If so your way is better. Is there a way to extend classes globally so that you can add functions to any class you inport into your code? I only did it my way because I didn't understand what is going on completely with yours, if you can help me understand it better (exactley what's going on) i'd like to have a variation of my function available within every class, but I'm a bit squemish about not exactley knowing what's going on in my own code. I'd rather you teach me how to fish than give me a fish. I find the documentation for python isn't exactly targeted towards people like me who need a bit more explaining to know what's going on, it tends to use terminology that i don't understand and when trying to understand the terminology it references stuff i don't know much or anything about so i have to look that up and i get frustrated. Now there are sites that walk you hand in hand through python coding with easily understandable examples and dumbed-down terminology.. but only for the very simple parts of python. If you would have not pointed me to inspect, I'd have never found it.
It would perhaps be better to unpack my function a little. I don't claim it is the best way to do this, but I'm going to explain the following code
import inspect

def _get_names(cls):
    for tp in inspect.getmro(cls):
        for name, value in vars(tp).items():
            if isinstance(value, property):
                yield name

_register = {}

def property_names(cls):
    if cls not in _register:
        _register[cls] = sorted(set(_get_names(cls)))
    return _register[cls]


def add_property_names(cls):
    cls.property_names = classmethod(property_names)
    return cls

if __name__ == '__main__':
    class A:
        pass

    class B(A):
        @property
        def foo(self):
            return 0
        
    class C(B):
        @property
        def bar(self):
            return self._bar
        
        @bar.setter
        def bar(self, value):
            self._bar = value

    add_property_names(A)

    a = A()
    print(a.property_names())
    c = C()
    print(c.property_names())
    cc = C()
    print(cc.property_names())
well hell i just noticed your function functions inside a function... i didn't even know you dould do that
The most important call is add_property_names(A) if you look at what it does in line 18, it adds a new classmethod named property_names to class A. Everything happens as if we had written
class A:
    @classmethod
    def property_names(cls):
        ...
It means that we can now call A.property_names() or A().property_names() or even C.property_names() because C is a subclass of A.

Now look at this method at line 11. It looks in a global dictionary _register
if we have already stored a list of property names _register[A] for class A. If not it computes this list by calling _get_names(A). This way the list of properties is only computed once for each class.

Last subtlety: add_property_names(cls) returns cls. This allows us to use it as a class decorator and write
@add_property_names
class A:
    pass
so that we don't need to call explicitly add_property_names after the definition of class A.
that is so much easier to understand.

so this registers a class method... if i understand class methods they don't need an instance to work right?

if i wanted to add my method to return a tuple of property_name (using your function and property_value which would require an instance, would that be practical to write into the class or would i need to do the same with this function and register them together?
You could add this to my code
def property_summary(self):
    return {k: getattr(self, k) for k in self.property_names()}

def add_property_summary(cls):
    add_property_names(cls)
    cls.property_summary = property_summary
    return cls
Then instead of decorating videofile with add_property_names, decorate it with add_property_summary
if __name__ == '__main__':
    
    @add_property_summary
    class videofile:
        ...
you can then get a dict with
movieitem.property_summary()
Pages: 1 2