[Edu-sig] Descriptor concept clarifies Python behind the scenes

kirby urner kirby.urner at gmail.com
Tue Apr 16 16:51:04 EDT 2019


Perfect to bring in the Descriptor protocol, right when we're talking about
'self' and also (per Trey's article), the different species of callable,
i.e. functions, methods (functions inside classes) and the classes
themselves (also callables, passing through to __init__, no need for
keyword 'new' as in Java or Javascript).

The code for a Property class in:

https://docs.python.org/3/howto/descriptor.html#properties

is about my deepest dive in an introductory course (and a longish one at
that).   We looked at it last week.  Tonight I look a little more at
Descriptors so all this edu-sig work counts as warming up.

This isn't "cheat sheet" level Python, that's for sure.

The actual property class (a built-in) isn't actually implemented in pure
Python.  The code in the docs is like "if it were" (written in Python) and
to prove they're not kidding, when I introduce one of those Circle types
(ala Raymond Hettinger) with co-varying attributes (change the radius, area
and circumference change; change the circumference, radius and area
change... etc.) wherein at the top I go:

from modelproperty import Property as property  # <-- overwrite builtin
property with doc simulation

class Circle:
    def __init__(radius = 1):
        self.radius = 1  # trigger setter (this won't store directly to
self.__dict__['radius'] thanks to @property)

    @property
    def radius(self):  # one could do the recomputations upon getting but...
        return self._radius

    @radius.setter
    def radius(self, r):  # it makes the most sense to update other
dimensions when setting, no?
        self._radius = r
        self._area = pi * r * r
        self._circumference = 2 * pi * r

and so on.

Everything works the same.  Even though we're using doc code instead.

With Property in view, one can see how circle.radius = 3 is triggering
circle.radius.__set__(3), because radius is now the name of a Property
instance (thanks to decorator syntax), and the radius.__set__ method
invokes a stored method fset

The first @property is swallowing the immediately following radius method
whole, and storing it in fget (a property attribute).

The invocation of @radius.setter then calls a Property method:

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

which cleverly returns a whole new Property instance, by keeping self.fget
as it was, while taking in fset as the new kid on the block.

The newer Property is now armed with what it needs, to make radius play
well with the others (area and circumference).

Kirby


On Tue, Apr 16, 2019 at 12:37 PM Wes Turner <wes.turner at gmail.com> wrote:

> When you access an attribute of a class object (a dict), the class
> attribute is looked up by __get__().
> the class's instance of that function attribute is 'bound'; it receives
> 'self' (an object reference) as its first argument.
>
> If you write your own __get__ (e.g. with functools.partial or
> functools.wrap),
> or try and assign a function to a class [instance],
> or create a @staticmethod or a @classmethod,
> you can more fully understand how methods receive self as their first
> argument.
>
> - https://docs.python.org/3/howto/descriptor.html#functions-and-methods
> - https://python-reference.readthedocs.io/en/latest/docs/dunderdsc/ :
>
> """
> Descriptor Protocol
> In general, a descriptor is an object attribute with “binding behavior”,
> one whose attribute access has been overridden by methods in the descriptor
> protocol: __get__(), __set__(), and __delete__(). If any of those methods
> are defined for an object, it is said to be a descriptor.
>
> The default behavior for attribute access is to get, set, or delete the
> attribute from an object’s dictionary. For instance, a.x has a lookup chain
> starting with a.__dict__[‘x’], then type(a).__dict__[‘x’], and continuing
> through the base classes of type(a) excluding metaclasses.
>
> However, if the looked-up value is an object defining one of the
> descriptor methods, then Python may override the default behavior and
> invoke the descriptor method instead. Where this occurs in the precedence
> chain depends on which descriptor methods were defined and how they were
> called. Note that descriptors are only invoked for new style objects or
> classes (ones that subclass object() or type()).
>
> The starting point for descriptor invocation is a binding, a.x. How the
> arguments are assembled depends on a:
>
> Direct Call
> The simplest and least common call is when user code directly invokes a
> descriptor method: x.__get__(a).
>
> Instance Binding
> If binding to a new-style object instance, a.x is transformed into the
> call: type(a).__dict__[‘x’].__get__(a, type(a)).
>
> Class Binding
> If binding to a new-style class, A.x is transformed into the call:
> A.__dict__[‘x’].__get__(None, A).
>
> Super Binding
> If a is an instance of super, then the binding super(B, obj).m() searches
> obj.__class__.__mro__ for the base class A immediately preceding B and then
> invokes the descriptor with the call: A.__dict__[‘m’].__get__(obj,
> obj.__class__).
> """
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/edu-sig/attachments/20190416/6135f4f1/attachment.html>


More information about the Edu-sig mailing list