Tim
Tim

Reputation: 99418

Why does __get__ take an owner while __set__ and __delete__ do not?

From the Python data model documentation:

object.__get__(self, instance, owner=None)

Called to get the attribute of the owner class (class attribute access) or of an instance of that class (instance attribute access). The optional owner argument is the owner class, while instance is the instance that the attribute was accessed through, or None when the attribute is accessed through the owner.

This method should return the computed attribute value or raise an AttributeError exception.

PEP 252 specifies that __get__() is callable with one or two arguments. Python’s own built-in descriptors support this specification; however, it is likely that some third-party tools have descriptors that require both arguments. Python’s own __getattribute__() implementation always passes in both arguments whether they are required or not.

object.__set__(self, instance, value)

Called to set the attribute on an instance instance of the owner class to a new value, value.

Note, adding __set__() or __delete__() changes the kind of descriptor to a “data descriptor”. See Invoking Descriptors for more details.

object.__delete__(self, instance)

Called to delete the attribute on an instance instance of the owner class.

Why does __get__ take an owner while __set__ and __delete__ do not?

Does it mean that when a descriptor supplies both __get__ and __set__,

My question is actually part of this one.

Upvotes: 3

Views: 859

Answers (3)

Bazzan
Bazzan

Reputation: 13

I am going to borrow Marty Alchin's answer:

... the fact that only __get__ can receive the owner class, while the rest only receive the instance.

Descriptors are assigned to a class, not to an instance, and modifying the class would actually overwrite or delete the descriptor itself, rather than triggering its code. This is intentional. Otherwise, once a descriptor is set, it could never be removed or modified without modifying source code and restarting the program. That’s not preferable, so only the retrieval method (__get__) has access to the owner class.

An example to show what is explained above. The code also explains this portion of @Gery Ogam's answer:

import random

class Die:
    def __init__(self, sides=6):
        self.sides = sides

    def __get__(self, instance, owner=None):
        return int(random.random() * self.sides) + 1

    def __set__(self, instance, value):
        ...

class Game:
    d6 = Die()

# Assume:
# if `__set__` was designed to take an `owner` class,
# the code below will trigger the `__set__` method
# rather than call `__get__` to override
# the value of `d6` in the `Game.__dict__`.
Game.d6 = 3
print(Game.d6)  # 3

The owner will always be set to the appropriate class, though instance may be None if the attribute was accessed from the class. This is what Django uses to throw an error if you try to access a manager from an object instead of a class, or a related model (ForeignKey) on a class instead of an object.

Upvotes: 0

Géry Ogam
Géry Ogam

Reputation: 8027

Let us consider an attribute access on an originating object which ends up finding a descriptor on an owning class and calls its __get__, __set__ or __delete__ method.

  • For __get__, the information on whether the originating object is an instance or subclass of the owning class is necessary, because classmethod add a subclass of the owning class as first argument before forwarding a call, and because Python 2 unbound methods check that the first argument is an instance of the owning class before forwarding a call (the second reason is now only historical since Python 3 replaced unbound methods with plain functions). So __get__ needs two pieces of information: the originating object to retrieve the attribute value from, and whether the originating object is an instance or subclass of the owning class.
  • For __set__ and __delete__, the information on whether the originating object is an instance or subclass of the owning class is not necessary, because the methods are only called when the originating object is an instance of the owning class, since if they were also called when the originating object is a subclass of the owning class it would be impossible to change a faulty descriptor as class dictionaries are read-only types.MappingProxyType. So __set__ needs two piece of information: the originating object on which to set the attribute value, and the attribute value. And __delete__ needs one piece of information: the originating object from which to delete the attribute value.

A straightforward way to provide this information would have been to design the descriptor API with the following parameters:

  • an origin object parameter for providing the originating object (so for __get__, __set__, and __delete__);
  • an isinstance Boolean parameter for providing whether the originating object is an instance or subclass of the owning class (so only for __get__);
  • a value object parameter for providing the attribute value (so only for __set__).

However Guido van Rossum adopted a different but equivalent design for the descriptor API:

  • an instance object parameter for providing the originating object when it is an instance of the owning class (so for __get__, __set__, and __delete__), and whether the originating object is an instance or subclass of the owning class by using None for the latter (so only for __get__);
  • an owner type parameter for providing the originating object when it is a subclass of the owning class (so only for __get__);
  • a value object parameter for providing the attribute value (so only for __set__).

He specified the design of the descriptor API in PEP 252: Making Types Look More Like Classes, published on 19 April 2001:

Specification of the attribute descriptor API Attribute descriptors

Attribute descriptors may have the following attributes. In the examples, x is an object, C is x.__class__, x.meth() is a method, and x.ivar is a data attribute or instance variable. All attributes are optional -- a specific attribute may or may not be present on a given descriptor. An absent attribute means that the corresponding information is not available or the corresponding functionality is not implemented.

  • __name__: the attribute name. Because of aliasing and renaming, the attribute may (additionally or exclusively) be known under a different name, but this is the name under which it was born. Example: C.meth.__name__ == 'meth'.
  • __doc__: the attribute's documentation string. This may be None.
  • __objclass__: the class that declared this attribute. The descriptor only applies to objects that are instances of this class (this includes instances of its subclasses). Example: C.meth.__objclass__ is C.
  • __get__(): a function callable with one or two arguments that retrieves the attribute value from an object. This is also referred to as a "binding" operation, because it may return a "bound method" object in the case of method descriptors. The first argument, X, is the object from which the attribute must be retrieved or to which it must be bound. When X is None, the optional second argument, T, should be meta-object and the binding operation may return an unbound method restricted to instances of T. When both X and T are specified, X should be an instance of T. Exactly what is returned by the binding operation depends on the semantics of the descriptor; for example, static methods and class methods (see below) ignore the instance and bind to the type instead.
  • __set__(): a function of two arguments that sets the attribute value on the object. If the attribute is read-only, this method may raise a TypeError or AttributeError exception (both are allowed, because both are historically found for undefined or unsettable attributes). Example: C.ivar.set(x, y) ~~ x.ivar = y.

I am indebted to Martijn Pieters for the arguments used in this answer (cf. our discussion in comments of this post).

Upvotes: 0

user2357112
user2357112

Reputation: 280465

owner mostly exists for getting the attribute on the class itself, rather than an instance. When you're retrieving the attribute on an instance, the owner argument is redundant, since it's just type(instance).

__set__ doesn't apply to setting the attribute on the class itself, so it has no use for owner.

Upvotes: 7

Related Questions