Using a class instance as a class attribute, descriptors, and properties
Asked Answered
C

2

8

I have recently stated trying to use the newer style of classes in Python (those derived from object). As an excersise to familiarise myself with them I am trying to define a class which has a number of class instances as attributes, with each of these class instances describing a different type of data, e.g. 1d lists, 2d arrays, scalars etc. Essentially I wish to be able to write

some_class.data_type.some_variable

where data_type is a class instance describing a collection of variables. Below is my first attempt at implementing this, using just a profiles_1d instance and rather generic names:

class profiles_1d(object):
    def __init__(self, x, y1=None, y2=None, y3=None):
        self.x = x
        self.y1 = y1
        self.y2 = y2
        self.y3 = y3

class collection(object):
    def __init__(self):
        self._profiles_1d = None

    def get_profiles(self):
        return self._profiles_1d

    def set_profiles(self, x, *args, **kwargs):
        self._profiles_1d = profiles_1d(x, *args, **kwargs)

    def del_profiles(self):
        self._profiles_1d = None

    profiles1d = property(fget=get_profiles, fset=set_profiles, fdel=del_profiles,
        doc="One dimensional profiles")

Is the above code roughly an appropriate way of tackling this problem. The examples I have seen of using property just set the value of some variable. Here I require my set method to initialise an instance of some class. If not, any other suggestions of better ways to implement this would be greatly appreciated.

In addition, is the way I am defining my set method ok? Generally the set method, as far as I understand, defines what to do when the user types, in this example,

collection.profiles1d = ...

The only way I can correctly set the attributes of the profiles_1d instance with the above code is to type collection.set_profiles([...], y1=[...], ...), but I think that I shouldn't be directly calling this method. Ideally I would want to type collection.profiles = ([...], y1=[...], ...): is this correct/possible?

Finally, I have seen a decorators mentioned alot with repect to the new style of classes, but this is something I know very little about. Is the use of decorators appropriate here? Is this something I should know more about for this problem?

Cortese answered 11/8, 2011 at 11:18 Comment(4)
Why use getters and getters and a private attribute? Just make profiles_1d a public attribute and have the code setting it pass a profiles_1d object instead of parameters for the constructor.Abruption
As a user surely it would be easier to just deal with the main class (collection in this case). But you're right, this would be an easy solution.Cortese
your collection already returns objects of profiles_1d type to the code using it; it violates the principal of least surprise for a setter to pass something different from what a getter returns (not to mention getters and setters being thoroughly unpythonic).Abruption
@Abruption Thanks - I'll bear this in mind.Cortese
K
10

First, it's good you're learning new-style classes. They've got lots of advantages.

The modern way to make properties in Python is:

class Collection(object):
    def __init__(self):
        self._profiles_1d = None

    @property
    def profiles(self):
        """One dimensional profiles"""
        return self._profiles_1d

    @profiles.setter
    def profiles(self, argtuple):
        args, kwargs = argtuple
        self._profiles_1d = profiles_1d(*args, **kwargs)

    @profiles.deleter
    def profiles(self):
        self._profiles_1d = None

then set profiles by doing

collection = Collection()
collection.profiles = (arg1, arg2, arg3), {'kwarg1':val1, 'kwarg2':val2}

Notice all three methods having the same name.

This is not normally done; either have them pass the attributes to collections constructor or have them create the profiles_1d themselves and then do collections.profiles = myprofiles1d or pass it to the constructor.

When you want the attribute to manage access to itself instead of the class managing access to the attribute, make the attribute a class with a descriptor. Do this if, unlike in the property example above, you actually want the data stored inside the attribute (instead of another, faux-private instance variable). Also, it's good for if you're going to use the same property over and over again -- make it a descriptor and you don't need to write the code multiple times or use a base class.

I actually like the page by @S.Lott -- Building Skills in Python's Attributes, Properties and Descriptors.

Kasandrakasevich answered 11/8, 2011 at 12:1 Comment(1)
Thanks for this answer, it is very helpful. However, presumably the argument x should not be in the line self._profiles_1d = profiles_1d(x, *args, **kwargs)?Cortese
B
1

When creating propertys (or other descriptors) that need to call other instance methods the naming convention is to prepend an _ to those methods; so your names above would be _get_profiles, _set_profiles, and _del_profiles.

In Python 2.6+ each property is also a decorator, so you don't have to create the (otherwise useless) _name methods:

@property
def test(self):
    return self._test

@test.setter
def test(self, newvalue):
    # validate newvalue if necessary
    self._test = newvalue

@test.deleter
def test(self):
    del self._test

It looks like your code is trying to set profiles on the class instead of instances -- if this is so, properties on the class won't work as collections.profiles would be overridden with a profiles_1d object, clobbering the property... if this is really what you want, you'll have to make a metaclass and put the property there instead.

Hopefully you are talking about instances, so the class would look like:

class Collection(object):  # notice the capital C in Collection
    def __init__(self):
        self._profiles_1d = None

    @property
    def profiles1d(self):
        "One dimensional profiles"
        return self._profiles_1d

    @profiles1d.setter
    def profiles1d(self, value):
        self._profiles_1d = profiles_1d(*value)

    @profiles1d.deleter
    def profiles1d(self):
        del self._profiles_1d

and then you would do something like:

collection = Collection()
collection.profiles1d = x, y1, y2, y3

A couple things to note: the setter method gets called with only two items: self, and the new value (which is why you were having to call set_profiles1d manually); when doing an assignment, keyword naming is not an option (that only works in function calls, which an assignment is not). If it makes sense for you, you can get fancy and do something like:

collection.profiles1d = (x, dict(y1=y1, y2=y2, y3=y3))

and then change the setter to:

    @profiles1d.setter
    def profiles1d(self, value):
        x, y = value
        self._profiles_1d = profiles_1d(x, **y)

which is still fairly readable (although I prefer the x, y1, y2, y3 version myself).

Boren answered 6/1, 2012 at 19:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.