Alcott
Alcott

Reputation: 18585

Python `descriptor`

A descriptor class is as follows:

class Des(object):
    def __get__(self, instance, owner): ...
    def __set__(self, instance, value): ...
    def __delete__(self, instance): ...

class Sub(object):
    attr = Des()

X = sub()

Question

  1. I don't see the point of the existence of owner, how can I use it?

  2. To make an attr read-only, we shouldn't omit __set__ but define it to catch the assignments and raise an exception. So X.attr = 123 will fail, but __set__'s arguments doesn't contain owner, which means I can still do Sub.attr = 123, right?

Upvotes: 0

Views: 1617

Answers (6)

jruota
jruota

Reputation: 147

  1. The __set__ method is supposed to change attributes on an instance. But what if you would like to change an attribute that is shared by all instances and therefore lives in the class, e.g., is a class attribute? This can only be done if you have access to the class, hence the owner argument.

  2. Yes, you can overwrite the property / descriptor if you assign to an attribute through the class. This is by design, as Python is a dynamic language.

Hope that answers the question, although it was asked a long time ago.

Upvotes: 0

resigned
resigned

Reputation: 1084

As far as read only properties are concerned (see discussion above), the following example shows how its done:

############################################################
#
#    descriptors
#
############################################################

# define a class where methods are invoked through properties
class Point(object):
    def getX(self):
    print "getting x"
    return self._x

    def setX(self, value):
    print "setting x"
    self._x = value

    def delX(self):
    print "deleting x"
    del self._x

    x = property(getX, setX, delX)

p = Point()
p.x = 55    # calls setX
a = p.x     # calls getX
del p.x     # calls delX

# using property decorator (read only attributes)
class Foo(object):
    def __init__(self, x0, y0):
    self.__dict__["myX"] = x0
    self.__dict__["myY"] = y0

    @property
    def x(self):
    return self.myX

f = Foo(4,6)
print f.x
try:
    f.x = 77        # fails: f.x is read-only
except Exception,e:
    print e

Upvotes: 1

resigned
resigned

Reputation: 1084

You can experiment with this example:

# the descriptor protocol defines 3 methods:
#    __get__()
#    __set__()
#    __delete__()

# any class implementing any of the above methods is a descriptor
# as in this class
class Trace(object):
    def __init__(self, name):
        self.name = name

    def __get__(self, obj, objtype):
        print "GET:" + self.name + " = " + str(obj.__dict__[self.name])
        return obj.__dict__[self.name]

    def __set__(self, obj, value):
        obj.__dict__[self.name] = value
        print "SET:" + self.name + " = " + str(obj.__dict__[self.name])

# define the attributes of your class (must derive from object)
#  to be references to instances of a descriptor

class Point(object):
# NOTES:
# 1. descriptor invoked by dotted attribute access:  A.x or a.x
# 2. descripor reference must be stored in the class dict, not the instance dict
# 3. descriptor not invoked by dictionary access: Point.__dict__['x']

    x = Trace("x")
    y = Trace("y")

    def __init__(self, x0, y0):
        self.x = x0
        self.y = y0

    def moveBy(self, dx, dy):
        self.x = self.x + dx     # attribute access does trigger descriptor
        self.y = self.y + dy


# trace all getters and setters    
p1 = Point(15, 25)
p1.x = 20
p1.y = 35
result = p1.x
p2 = Point(16, 26)
p2.x = 30
p2.moveBy(1, 1)

Upvotes: 2

resigned
resigned

Reputation: 1084

The owner is just the class of the instance and is provided for convenience. You can always compute it from instance:

owner = instance.__class__

Upvotes: 0

HardlyKnowEm
HardlyKnowEm

Reputation: 3232

I came across this question with similar confusion, and after I answered it for myself it seemed prudent to report my findings here for prosperity.

As ThiefMaster already pointed out, the "owner" parameter makes possible constructions like a classproperty. Sometimes, you want classes to have methods masked as non-method attributes, and using the owner parameter allows you to do that with normal descriptors.

But that is only half the question. As for the "read-only" issue, here's what I found:

I first found the answer here: http://martyalchin.com/2007/nov/23/python-descriptors-part-1-of-2/. I did not understand it at first, and it took me about five minutes to wrap my head around it. What finally convinced me was coming up with an example.

Consider the most common descriptor: property. Let's use a trivial example class, with a property count, which is the number of times the variable count has been accessed.

class MyClass(object):
    def __init__(self):
        self._count = 0
    @property
    def count(self):
        tmp = self._count
        self._count += 1
        return tmp
    @count.setter
    def setcount(self):
        raise AttributeError('read-only attribute')
    @count.deleter
    def delcount(self):
        raise AttributeError('read-only attribute')

As we've already established, the owner parameter of the __get__ function means that when you access the attribute at the class level, the __get__ function intercepts the getattr call. As it happens, the code for property simply returns the property itself when accessed at the class level, but it could do anything (like return some static value).

Now, imagine what would happen if __set__ and __del__ worked the same way. The __set__ and __del__ methods would intercept all setattr and delattr calls at the class level, in addition to the instance level.

As a consequence, this means that the "count" attribute of MyClass is effectively unmodifiable. If you're used to programming in static, compiled languages like Java this doesn't seem very interesting, since you can't modify classes in application code. But in Python, you can. Classes are considered objects, and you can dynamically assign any of their attributes. For example, let's say MyClass is part of a third-party module, and MyClass is almost entirely perfect for our application (let's assume there's other code in there besides the code for count) except that we wished the count method worked a little differently. Instead, we want it to always return 10, for every single instance. We could do the following:

>>> MyClass.count = 10
>>> myinstance = MyClass()
>>> myinstance.count
10

If __set__ intercepted the call to setattr(MyClass, 'count'), then there would be no way to actually change MyClass. Instead, the code for setcount would intercept it and couldn't do anything with it. The only solution would be to edit the source code for MyClass. (I'm not even sure you could overwrite it in a subclass, because I think defining it in a subclass would still invoke the setattr code. But I'm not sure, and since we're already dealing with a counterfactual here, I don't really have a way of testing it.)

Now, you may be saying, "That's exactly what I want! I intentionally did not want my user to reassign attributes of my class!" To that, all I can say is that what you wanted is impossible using naive descriptors, and I would direct you to the reasoning above. Allowing class attributes to be reassigned is much more in line with current Python idioms.

If you really, REALLY want to make a read-only class attribute, I don't think could tell you how. But if there is a solution, it would probably involve using metaclasses and either creating a property of the metaclass or modifying the metaclass's code for setattr and delattr. But this is Deep Magic, and well beyond the scope of this answer (and my own abilities with Python).

Upvotes: 1

ThiefMaster
ThiefMaster

Reputation: 318498

See http://docs.python.org/reference/datamodel.html#implementing-descriptors:

owner is always the owner class, while instance is the instance that the attribute was accessed through, or None when the attribute is accessed through the owner

A case where you would use owner would be creating a classproperty:

class _ContentQueryProperty(object):
    def __get__(self, inst, cls):
        return Content.query.filter_by(type=cls.TYPE)

Upvotes: 4

Related Questions