2

I am reading Item 31 in the book "Effective Python". I don't understand why in the example on page 97, why math_grade, writing_grade and science_grade are class (static) variables of the Exam class, rather than regular, instance variables. If they were instance variables, then the Grade class wouldn't need to use instance as key in its global book-keeping dictionary. Seems to me like the author made one obvious design mistake just to illustrate how to use descriptors, i.e. global book keeping in the Grade class, which seems like a bad idea anyways.

My other question is more high-level: isn't this a confusing, unclear way to do things? Keeping global state of multiple objects in one single registry, like Grade does. Doesn't seem like a reusable, clean design to me.

Here is reference to the code, for people that don't have the book:

https://github.com/SigmaQuan/Better-Python-59-Ways/blob/master/item_31_use_descriptors.py

Specifically

 class Grade(object):
     def __get__(*args, **kwargs):
          super().__getattribute__(*args, **kwargs)

     def __set__(*args, **kwargs):
          super().__setattr__(args, kwargs)


 class Exam(object):
     math_grade = Grade()
     writing_grade = Grade()
     science_grade = Grade()
Baron Yugovich
  • 3,843
  • 12
  • 48
  • 76
  • In isolation, yes. But this is just one step in explaining how custom descriptors replace some of the boilerplate introduced by writing the same code repeatedly for a group of properties. – chepner Mar 27 '18 at 15:49
  • But it's a contrived, artificial example, right? Because the necessity for descriptors in this case is created by a bad design decision. – Baron Yugovich Mar 27 '18 at 15:53
  • 1
    I don't think so, what the example intends to highlight is that descriptors can be used with advantage when you want to apply the same validation code to several different attributes in class. Otherwise you'll have to repeat the @property validation code over and over. – progmatico Mar 27 '18 at 15:56
  • Sure, the validation code can still be in the Grade class, but you can eliminate the need for a global state dictionary in it if math_grade, writing_grade are instance variables instead. Am I missing something? – Baron Yugovich Mar 27 '18 at 15:58
  • The linked file goes through several revisions of `Grade` addressing various concerns. Your question would be clearer if you compared your approach to the *final* approach, indicating specifically why you think it is worse than yours. – chepner Mar 27 '18 at 16:00
  • You need them in the class not in the instance, otherwise the descriptors behaviour will not work. Being in the class is what makes them to intercept accesses to the attributes. – progmatico Mar 27 '18 at 16:01
  • That's the part I was missing! Can you please elaborate? – Baron Yugovich Mar 27 '18 at 16:04
  • Consider `__set__`: If a class attribute implements `__set__` it will intercept an access to an attribute with the same name in the instance of the managed class (managed by the descriptor) – progmatico Mar 27 '18 at 16:06
  • 1
    `foo.math_grade = 67` becomes `Exam.math_grade.__set__(foo, Exam)`, so there's exactly one place (`Grade.__set__`) you need to define the `0 <= x <= 100` behavior, rather than applying it to each type of grade specifically (the setter for `math_grade`, the setter for `writing_grade`, etc). – chepner Mar 27 '18 at 16:07
  • If you define it in instances they will get thrown away by assignment, instead of intercepting the access. – progmatico Mar 27 '18 at 16:10
  • So are \__set__ and \__get__ class(static) methods or instance methods? And what is instance argument in them? Grade or Exam? – Baron Yugovich Mar 27 '18 at 16:10
  • they are instance methods of the Grade class, but you will instantiate Grade objects in the Exam class as class variables – progmatico Mar 27 '18 at 16:14
  • So what is the argument instance inside them? Is it a Grade object or an Exam object? Can somebody knowledgeable please compile all these comments into a complete, consistent answer with all details explained? – Baron Yugovich Mar 27 '18 at 16:16
  • 1
    I think the answer to that is explicit in `foo.math_grade = 67 becomes Exam.math_grade.__set__(foo, Exam)`. `foo` and `Exam` is what you are asking for. – progmatico Mar 27 '18 at 16:20
  • I guess one of my big open questions is still why descriptor instances have to be defined at class level? (like math_grade above) – Baron Yugovich Mar 27 '18 at 16:34
  • Because special methods (dunder methods, __set__, __get__, ...) are searched first in the class and only then in the instance. That's how the interception of an attribute of the managed class access, is made. – progmatico Mar 27 '18 at 16:41
  • Sorry, still not following. What does that imply? What would happen if they were class instances? Can you please elaborate everything in a proper response instead of comments? – Baron Yugovich Mar 27 '18 at 16:45
  • Sorry, don't have time right now and descriptors are not simple for a quick and correct answer. Please try https://stackoverflow.com/questions/3798835/understanding-get-and-set-and-python-descriptors which looks a bit confuse to me and much clear here (but old, can't check if it's still current, I think so) http://martyalchin.com/2007/nov/23/python-descriptors-part-1-of-2/ . Please give it some time to settle, descriptors are not that simple to understand or explain. – progmatico Mar 27 '18 at 16:54
  • @BaronYugovich this should help you with descriptors https://docs.python.org/3/howto/descriptor.html – progmatico Mar 28 '18 at 22:30

1 Answers1

0

I think a good reference in the subject available to everyone, is actually the official docs in this Descriptors How To

I setup an example, but note that there is a lot more about descriptors, and you shouldn't be using this unless writing a framework or some library (like an ORM) that requires the dynamic instantiation and validation of fields of different types for example.

For the usual validation needs, limit yourself to property decorator.

class PositionX: # from 0 to 1000
    def __init__(self, x):
        self.x = x

print('***Start***')
print()
print('Original PositionX class')
pos1 = PositionX(50)
print(pos1.x)
pos1.x = 100
print(pos1.x)
pos1.x = -10
print(pos1.x)
print()


# let's validate x with a property descriptor, using @property

class PositionX: # from 0 to 1000
    def __init__(self, position):
        self._x = position

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

    @x.setter
    def x(self, value):
        if 0 <= value <= 1000:
            self._x = value
        else:
            raise ValueError

print('PositionX attribute x validated with @property')
pos2 = PositionX(50)
print(pos2.x)
pos2.x = 100
print(pos2.x)
try:
    pos2.x = -10
except ValueError:
    print("Can't set x to -10")
print()

# Let's try instead to use __set__ and __get__ in the original class
# This is wrong and won't work. This makes the class PositionX a descriptor,
# while we wanted to protect x attribute of PositionX with the descriptor.

class PositionX: # from 0 to 1000
    def __init__(self, x):
        self.x = x

    def __get__(self, instance):
        print('Running __get__')
        return self._x

    def __set__(self, instance, value):
        print('Running __set__')
        if 0 <= value <= 1000:
            self._x = value
        else:
            raise ValueError

print("Using __set__ and __get__ in the original class. Doesn't work.")
print("__get__ and __set__ don't even run because x is found in the pos3 instance and there is no descriptor object by the same name in the class.")
 
pos3 = PositionX(50)
print(pos3.x)
pos3.x = 100
print(pos3.x)
try:
    pos3.x = -10
except ValueError:
    print("Can't set x to -10")
print(pos3.x)
print()

# Let's define __set__ and __get__ to validate properties like x
# (with the range 0 to 1000). This actually makes the class Range0to1000
# a data descriptor. The instance dictionary of the managed class PositionX
# is always overrided by the descriptor.

# This works because now on x attribute reads and writes of a PositionX
# instance the __get__ or __set__ descriptor methods are always run.

# When run they get or set the PositionX instance __dict__ to bypass the
# trigger of descriptor __get__ or __set__ (again)

class Range0to1000:
    def __init__(self, name): # the property name, 'x', 'y', whatever
        self.name = name
        self.value = None

    def __get__(self, instance, managed_class):
        print('Running __get__')
        return instance.__dict__[self.name]
        # same as getattr(instance, self.name) but doesn't trigger
        # another call to __get__ leading to recursion error

    def __set__(self, instance, value):
        print('Running __set__')
        if 0 <= value <= 1000:
            instance.__dict__[self.name] = value
            # same as setattr(instance, self.name, self.value) but doesn't
            # trigger another call to __set__ leading to recursion error
        else:
            raise ValueError

class PositionX: # holds a x attribute from 0 to 1000

    x = Range0to1000('x') # no easy way to avoid passing the name string 'x'
                          # but now you can add many other properties
                          # sharing the same validation code
#   y = Range0to1000('y')
#   ...

    def __init__(self, x):
        self.x = x

print("Using a descriptor class to validate x.")
 
pos4 = PositionX(50)
print(pos4.x)
pos4.x = 100
print(pos4.x)
try:
    pos4.x = -10
except ValueError:
    print("Can't set x to -10")
print(pos4.x)
print()
print('***End***')
progmatico
  • 4,714
  • 1
  • 16
  • 27