Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python property descriptor design: why copy rather than mutate?

I was looking at how Python implements the property descriptor internally. According to the docs property() is implemented in terms of the descriptor protocol, reproducing it here for convenience:

class Property(object):     "Emulate PyProperty_Type() in Objects/descrobject.c"      def __init__(self, fget=None, fset=None, fdel=None, doc=None):         self.fget = fget         self.fset = fset         self.fdel = fdel         if doc is None and fget is not None:             doc = fget.__doc__         self.__doc__ = doc      def __get__(self, obj, objtype=None):         if obj is None:             return self         if self.fget is None:             raise AttributeError("unreadable attribute")         return self.fget(obj)      def __set__(self, obj, value):         if self.fset is None:             raise AttributeError("can't set attribute")         self.fset(obj, value)      def __delete__(self, obj):         if self.fdel is None:             raise AttributeError("can't delete attribute")         self.fdel(obj)      def getter(self, fget):         return type(self)(fget, self.fset, self.fdel, self.__doc__)      def setter(self, fset):         return type(self)(self.fget, fset, self.fdel, self.__doc__)      def deleter(self, fdel):         return type(self)(self.fget, self.fset, fdel, self.__doc__) 

My question is: why aren't the last three methods implemented as follows:

    def getter(self, fget):         self.fget = fget         return self      def setter(self, fset):         self.fset = fset         return self      def deleter(self, fdel):         self.fdel= fdel         return self 

Is there a reason for returing new instances of property, internally pointing to basically the same get and set functions?

like image 273
nesdis Avatar asked Mar 03 '18 07:03

nesdis


People also ask

What is the difference between properties and descriptors in Python?

The most accessible technique is to use the property function to define get, set and delete methods associated with an attribute name. The property function builds descriptors for you. A slightly less accessible, but more extensible and reusable technique is to define descriptor classes yourself.

What is the use of @property in Python?

The @property is a built-in decorator for the property() function in Python. It is used to give "special" functionality to certain methods to make them act as getters, setters, or deleters when we define properties in a class.

Which special method must be defined to implement a data descriptor?

Invoking descriptor : Descriptors are invoked automatically whenever it receives the call for a set() method or get() method. For example, obj. gfg looks up gfg in the dictionary of obj . If gfg defines the method __get__() , then gfg.

What is the use of descriptors in Python?

Python descriptors are a way to create managed attributes. Among their many advantages, managed attributes are used to protect an attribute from changes or to automatically update the values of a dependant attribute. Descriptors increase an understanding of Python, and improve coding skills.


1 Answers

Let's start with a bit of history, because the original implementation had been equivalent to your alternative (equivalent because property is implemented in C in CPython so the getter, etc. are written in C not "plain Python").

However it was reported as issue (1620) on the Python bug tracker back in 2007:

As reported by Duncan Booth at http://permalink.gmane.org/gmane.comp.python.general/551183 the new @spam.getter syntax modifies the property in place but it should create a new one.

The patch is the first draft of a fix. I've to write unit tests to verify the patch. It copies the property and as a bonus grabs the __doc__ string from the getter if the doc string initially came from the getter as well.

Unfortunately the link doesn't go anywhere (I really don't know why it's called a "permalink" ...). It was classified as bug and changed to the current form (see this patch or the corresponding Github commit (but it's a combination of several patches)). In case you don't want to follow the link the change was:

 PyObject *  property_getter(PyObject *self, PyObject *getter)  { -   Py_XDECREF(((propertyobject *)self)->prop_get); -   if (getter == Py_None) -       getter = NULL; -   Py_XINCREF(getter); -   ((propertyobject *)self)->prop_get = getter; -   Py_INCREF(self); -   return self; +   return property_copy(self, getter, NULL, NULL, NULL);  } 

And similar for setter and deleter. If you don't know C the important lines are:

((propertyobject *)self)->prop_get = getter; 

and

return self; 

the rest is mostly "Python C API boilerplate". However these two lines are equivalent to your:

self.fget = fget return self 

And it was changed to:

return property_copy(self, getter, NULL, NULL, NULL); 

which essentially does:

return type(self)(fget, self.fset, self.fdel, self.__doc__) 

Why was it changed?

Since the link is down I don't know the exact reason, however I can speculate based on the added test-cases in that commit:

import unittest  class PropertyBase(Exception):     pass  class PropertyGet(PropertyBase):     pass  class PropertySet(PropertyBase):     pass  class PropertyDel(PropertyBase):     pass  class BaseClass(object):     def __init__(self):         self._spam = 5      @property     def spam(self):         """BaseClass.getter"""         return self._spam      @spam.setter     def spam(self, value):         self._spam = value      @spam.deleter     def spam(self):         del self._spam  class SubClass(BaseClass):      @BaseClass.spam.getter     def spam(self):         """SubClass.getter"""         raise PropertyGet(self._spam)      @spam.setter     def spam(self, value):         raise PropertySet(self._spam)      @spam.deleter     def spam(self):         raise PropertyDel(self._spam)  class PropertyTests(unittest.TestCase):     def test_property_decorator_baseclass(self):         # see #1620         base = BaseClass()         self.assertEqual(base.spam, 5)         self.assertEqual(base._spam, 5)         base.spam = 10         self.assertEqual(base.spam, 10)         self.assertEqual(base._spam, 10)         delattr(base, "spam")         self.assert_(not hasattr(base, "spam"))         self.assert_(not hasattr(base, "_spam"))         base.spam = 20         self.assertEqual(base.spam, 20)         self.assertEqual(base._spam, 20)         self.assertEqual(base.__class__.spam.__doc__, "BaseClass.getter")      def test_property_decorator_subclass(self):         # see #1620         sub = SubClass()         self.assertRaises(PropertyGet, getattr, sub, "spam")         self.assertRaises(PropertySet, setattr, sub, "spam", None)         self.assertRaises(PropertyDel, delattr, sub, "spam")         self.assertEqual(sub.__class__.spam.__doc__, "SubClass.getter") 

That's similar to the examples the other answers already provided. The problem is that you want to be able to change the behavior in a subclass without affecting the parent class:

>>> b = BaseClass() >>> b.spam 5 

However with your property it would result in this:

>>> b = BaseClass() >>> b.spam --------------------------------------------------------------------------- PropertyGet                               Traceback (most recent call last) PropertyGet: 5 

That happens because BaseClass.spam.getter (which is used in SubClass) actually modifies and returns the BaseClass.spam property!

So yes, it had been changed (very likely) because it allows to modify the behavior of a property in a subclass without changing the behavior on the parent class.

Another reason (?)

Note that there is an additional reason, which is a bit silly but actually worth mentioning (in my opinion):

Let's recap shortly: A decorator is just syntactic sugar for an assignment, so:

@decorator def decoratee():     pass 

is equivalent to:

def func():     pass  decoratee = decorator(func) del func 

The important point here is that the result of the decorator is assigned to the name of the decorated function. So while you generally use the same "function name" for the getter/setter/deleter - you don't have to!

For example:

class Fun(object):     @property     def a(self):         return self._a      @a.setter     def b(self, value):         self._a = value  >>> o = Fun() >>> o.b = 100 >>> o.a 100 >>> o.b 100 >>> o.a = 100 AttributeError: can't set attribute 

In this example you use the descriptor for a to create another descriptor for b that behaves like a except that it got a setter.

It's a rather weird example and probably not used very often (or at all). But even if it's rather odd and (to me) not very good style - it should illustrate that just because you use property_name.setter (or getter/deleter) that it has to be bound to property_name. It could be bound to any name! And I wouldn't expect it to propagate back to the original property (although I'm not really sure what I would expect here).

Summary

  • CPython actually used the "modify and return self" approach in the getter, setter and deleter once.
  • It had been changed because of a bug report.
  • It behaved "buggy" when used with a subclass that overwrote a property of the parent class.
  • More generally: Decorators cannot influence to what name they will be bound so the assumption that it's always valid to return self in a decorator might be questionable (for a general-purpose decorator).
like image 87
MSeifert Avatar answered Sep 24 '22 17:09

MSeifert