Understanding __get__ and __set__ and Python descriptors
The descriptor is how Python's property
type is implemented. A descriptor simply implements __get__
, __set__
, etc. and is then added to another class in its definition (as you did above with the Temperature class). For example:
temp=Temperature()
temp.celsius #calls celsius.__get__
Accessing the property you assigned the descriptor to (celsius
in the above example) calls the appropriate descriptor method.
instance
in __get__
is the instance of the class (so above, __get__
would receive temp
, while owner
is the class with the descriptor (so it would be Temperature
).
You need to use a descriptor class to encapsulate the logic that powers it. That way, if the descriptor is used to cache some expensive operation (for example), it could store the value on itself and not its class.
An article about descriptors can be found here.
EDIT: As jchl pointed out in the comments, if you simply try Temperature.celsius
, instance
will be None
.
How to use __get__ and __set__ (python descriptors)
From the docs:
object.__set__(self, instance, value)
Called to set the attribute on an instance
instance
of the owner class to a new value,value
.
In your code, Foo.bar = 5
is setting the class attribute, not an instance attribute. If you do use an instance (without first setting Foo.bar = 5
, which overrides your descriptor), then you get an exception as expected:
>>> f = Foo()
>>> f.bar
10
>>> f.bar = 5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in __set__
Exception
If you want the __set__
behaviour to apply when the class attribute is set, then the class itself needs to be an instance of a metaclass which uses the descriptor:
class FooMeta(type):
class Bar:
def __get__(self,instance, owner):
return 10
def __set__(self,instance,value):
raise Exception
bar = Bar()
class Foo(metaclass=FooMeta):
pass
Testing:
>>> Foo.bar
10
>>> Foo.bar = 5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in __set__
Exception
How does a descriptor with __set__ but without __get__ work?
The descriptor you've given in the example is a data descriptor.
Upon setting the attribute, as any other data descriptor, it takes the highest priority and is called like so:
type(myinst).__dict__["attr"].__set__(myinst, 1234)
This in turn, adds attr
to the instance dictionary according to your __set__
method.
Upon attribute access, the descriptor is checked for having the __get__
method but fails, causing for the search to be redirected to the instance's dictionary like so:
myinst.__dict__["attr"]
If it is not found in the instance dictionary, the descriptor itself is returned.
This behavior is shortly documented in the data model like so:
If it does not define
__get__()
, then accessing the attribute will
return the descriptor object itself unless there is a value in the
object’s instance dictionary.
Common usecases include avoiding {instance: value}
dictionaries inside the descriptors, and caching values in an efficient way.
In Python 3.6, __set_name__
was added to the descriptor protocol thus eliminating the need for specifying the name inside the descriptor. This way, your descriptor can be written like so:
class Desc:
def __set_name__(self, owner, name):
self.name = name
def __set__(self, inst, value):
inst.__dict__[self.name] = value
print("set", self.name)
class Test:
attr = Desc()
Why does __get__ take an owner while __set__ and __delete__ do not?
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
.
Why __get__ method of a descriptor in Python is called inside of hassattr()?
If you look at the help
for hasattr
(or the documentation:
Help on built-in function hasattr in module builtins:
hasattr(obj, name, /)
Return whether the object has an attribute with the given name.
This is done by calling getattr(obj, name) and catching AttributeError.
So no, it doesn't just "check dictionaries", it couldn't do that because not all objects have dictionaries as namespaces to begin with, e.g. built-in objects, or user-defined objects with __slots__
.
getattr(obj, name)
will correctly invoke the equivalent machinery as:
obj.name
Which would call the descriptor's __get__
Python descriptors (__get__, __set__) on function parameters
Functions actually are first class objects in Python, but you are correct in saying that the syntax you describe would not work as you want. You could potentially do something like this with a decorator that inspects the passed attributes for characteristics that would enable this sort of functionality though. However, you'd probably be better off implementing a callable object, then attaching descriptors to that and creating instances of the callable rather than functions.
Python - __get__ is not called for my class
You can use your decriptor class as follows:
class B:
a = Argument("my_ret_value", argHelp="some help text")
b = B()
b.a
# __get__
# 'my_ret_value'
When set as class attribute on another class and accessed via an instance of said class, then the __get__
method of the descriptor is called. See the Descriptor How-To Guide.
Related Topics
Python Library for Linux Process Management
Python Error "Importerror: No Module Named"
Import Error: No Module Name Urllib2
How to Use String.Replace() in Python 3.X
Iterate an Iterator by Chunks (Of N) in Python
How to Parse Dates with -0400 Timezone String in Python
Does Python Make a Copy of Objects on Assignment
How to Direct Output to a File When There Are Utf-8 Characters
Docker.Errors.Dockerexception: Error While Fetching Server API Version
Passing Extra Arguments Through Connect
How to Find the Time Difference Between Two Datetime Objects in Python
Extracting Extension from Filename in Python
How to Get List of Values from Dict
How to Keep a Python Script Output Window Open
How Does Swapping of Members in Tuples (A,B)=(B,A) Work Internally