
Have you ever thought about what happens when you put a dot in python? What does
str (“\ u002E”) hide behind? What secrets does he keep? If without mysticism, do you know how to find and set custom attribute values in python? Would you like to know? Then ... welcome!
To make the time spent reading easy, pleasant, and useful, it would be nice to know a few basic concepts of the language. In particular, the understanding of
type and
object will be extremely useful, as well as knowledge of several examples of both entities. You can read about them, including
here .
A little bit about the terminology I use before we get down to what we have gathered for:
- An object is any entity in python (function, number, string ... word, everything).
- A class is an object whose type is type (the type can be seen in the __class__ attribute).
- An instance of some class A is an object that has a reference to class A in the __class__ attribute.
Oh yes, all the examples in the article are written in
python3 ! This should definitely be considered.
If none of the above could temper your desire to find out what happens next, let's get started!
__dict__
Attributes of an object can be divided into two groups: certain python-ohms (such as
__class__ ,
__bases__ ) and user-defined, I am going to tell about them.
__dict__ according to this classification, refers to the “system” (defined by python) attributes. Its task is to store user attributes. It is a dictionary, in which the key is the
name of the attribute , the value, respectively, of the
value of the attribute .
To find an attribute of an object
o , python scans:
- The object itself ( o .__ dict__ and its system attributes).
- Object Class ( o .__ class __.__ dict__ ). Only __dict__ class, not system attributes.
- Classes from which the class of the object is set ( o .__ class __.__ bases __.__ dict__ ).
Thus, using
__dict__, an attribute can be defined both for a specific instance and for a class (that is, for all objects that are instances of a given class).
class StuffHolder: stuff = "class stuff" a = StuffHolder() b = StuffHolder() a.stuff
The example describes the class
StuffHolder with one
stuff attribute, which is inherited by both of its instances. Adding
b attribute
b_stuff to object
b does not affect
a .
Let's look at
__dict__ all the actors:
')
StuffHolder.__dict__
(The class StuffHolder in __dict__ stores an object of class dict_proxy with a bunch of different junk that you don’t need to pay attention to yet).Neither
a nor
b in
__dict__ has the
stuff attribute, having not found it there, the search engine looks for it in the
__dict__ class (
StuffHolder ), successfully finds and returns the value assigned to it in the class. The class reference is stored in the
__class__ attribute of the object.
An attribute search occurs at run time, so even after creating instances, all changes to the
__dict__ class will be reflected in them:
a.new_stuff
In the case of assigning a value to an instance attribute, only the
__dict__ instance is
changed , that is, the value in the
__dict__ class remains unchanged (if the value of the class attribute is not a data descriptor):
StuffHolder.__dict__
If the attribute names in the class and the instance are the same, the interpreter will look up the instance when searching for the value (in case the value of the class attribute is not a data descriptor):
StuffHolder.__dict__
By and large this is all that can be said about
__dict__ . This is a user-defined attribute store. Search in it is made at run time and the search takes into account the
__dict__ object class and base classes. It is also important to know that there are several ways to override this behavior. One of them is a great and mighty Handle!
Descriptors
With simple types as attribute values, everything is clear. Let's see how the function behaves in the same conditions:
class FuncHolder: def func(self): pass fh = FuncHolder() FuncHolder.func
WTF !? You ask ... maybe. I would ask. How does the function in this case differ from what we have already seen? The answer is simple: using the
__get__ method.
FuncHolder.func.__class__.__get__
This method overrides the mechanism for obtaining the value of the
func attribute of the
fh instance, and the object that implements this method is untranslatablely called a
non-data descriptor .
From
howto :
A descriptor is an object that is accessed by an attribute redefined by methods in a protocol descriptor :
descr .__ get __ (self, obj, type = None) -> value (overrides the way to get the attribute value)
descr .__ set __ (self, obj, value) -> None (overrides the method of assigning a value to an attribute)
descr .__ delete __ (self, obj) -> None (overrides the way the attribute is deleted)
Descriptors are of two types:
- Data Descriptor (data descriptor) - an object that implements the __get __ () and __set __ () method
- Non-data Descriptor (no data descriptor?) - an object that implements the __get __ () method
They differ in their behavior in relation to the entries in the
__ict__ instance. If
__dict__ has an entry with the same name as the data descriptor, the descriptor has an advantage. If the record name is the same as the “no data descriptor” name, the record priority in
__dict__ is higher.
Data descriptors
Consider the data descriptor more closely:
class DataDesc: def __get__(self, obj, cls): print("Trying to access from {0} class {1}".format(obj, cls)) def __set__(self, obj, val): print("Trying to set {0} for {1}".format(val, obj)) def __delete__(self, obj): print("Trying to delete from {0}".format(obj)) class DataHolder: data = DataDesc() d = DataHolder() DataHolder.data
It should be noted that the call to
DataHolder.data passes the
__get__ None method instead of an instance of the class.
Let us check the statement that the date of the descriptors has an advantage over the entries in the
__dict__ instance:
d.__dict__["data"] = "override!" d.__dict__
Indeed , an entry in
__dict__ of an instance is ignored if there is an
entry in the
__dict__ class of the instance (or its base class) with the same name and value - a data descriptor.
Another important point. If you change the value of an attribute with a descriptor through a class, no descriptor methods will be called, the value will change in the
__dict__ class as if it were a regular attribute:
DataHolder.__dict__
No data descriptors
Example of a data descriptor:
class NonDataDesc: def __get__(self, obj, cls): print("Trying to access from {0} class {1}".format(obj, cls)) class NonDataHolder: non_data = NonDataDesc() n = NonDataHolder() NonDataHolder.non_data
Its behavior is slightly different from what the date handle got up to. When trying to assign a value to the
non_data attribute, it was recorded in the
__dict__ instance, thus hiding the descriptor that is stored in the
__dict__ class.
Examples of using
Descriptors are a powerful tool that allows you to control access to the attributes of a class instance. One example of their use is functions, when called via an instance, they become methods (see the example above). Also a common way to use descriptors is to create a
property . By property, I mean a certain value characterizing the state of an object, access to which is controlled using special methods (getters, setters). Creating a property is simple using a handle:
class Descriptor: def __get__(self, obj, type): print("getter used") def __set__(self, obj, val): print("setter used") def __delete__(self, obj): print("deleter used") class MyClass: prop = Descriptor()
Or you can use the built-in
property class, it is a data descriptor. The code presented above can be rewritten as follows:
class MyClass: def _getter(self): print("getter used") def _setter(self, val): print("setter used") def _deleter(self): print("deleter used") prop = property(_getter, _setter, _deleter, "doc string")
In both cases, we get the same behavior:
m = MyClass() m.prop
It is important to know that a
property is always a data descriptor. If one of the functions (getter, setter or deliter) is not transferred to its constructor, AttributeError will be thrown out if an attempt is made to perform an appropriate action on the attribute.
class MySecondClass: prop = property() m2 = MySecondClass() m2.prop
The built-in descriptors also include:
- staticmethod is the same as a function outside the class; it does not pass an instance as the first argument.
- The classmethod is the same as the class method, only the instance class is passed as the first argument.
class StaticAndClassMethodHolder: def _method(*args): print("_method called with ", args) static = staticmethod(_method) cls = classmethod(_method) s = StaticAndClassMethodHolder() s._method()
__getattr __ (), __setattr __ (), __delattr __ () and __getatttribute __ ()
If you need to define the behavior of an object
as an attribute , you should use descriptors (for example,
property ). The same is true for a family of objects (for example,
functions ). Another way to influence access to attributes is:
__getattr __ () ,
__setattr __ () ,
__delattr __ () and
__getatttribute __ () methods . Unlike descriptors, they should be defined for the object
containing the attributes and they are called when accessing
any attribute of this object.
__getattr __ (self, name) will be called if the requested attribute is not found by the usual mechanism (in
__dict__ of an instance, class, etc.):
class SmartyPants: def __getattr__(self, attr): print("Yep, I know", attr) tellme = "It's a secret" smarty = SmartyPants() smarty.name = "Smartinius Smart" smarty.quicksort
__getattribute __ (self, name) will be called when trying to get the value of an attribute. If this method is redefined, the standard attribute value search mechanism will not be used. It should be borne in mind that calling special methods (for example,
__len __ () ,
__str __ () ) through built-in functions or an implicit call using language syntax bypasses
__getattribute __ () .
class Optimist: attr = "class attribute" def __getattribute__(self, name): print("{0} is great!".format(name)) def __len__(self): print("__len__ is special") return 0 o = Optimist() o.instance_attr = "instance" o.attr
__setattr __ (self, name, value) will be called when trying to set the value of an instance attribute. Similar to
__getattribute __ () , if this method is redefined, the standard value setting mechanism will not be used:
class NoSetters: attr = "class attribute" def __setattr__(self, name, val): print("not setting {0}={1}".format(name,val)) no_setters = NoSetters() no_setters.a = 1
__delattr __ (self, name) is similar to
__setattr __ () , but is used when deleting an attribute.
When overriding
__getattribute __ () ,
__setattr __ () and
__delattr __ () it should be borne in mind that the standard way of accessing attributes can be called via
object :
class GentleGuy: def __getattribute__(self, name): if name.endswith("_please"): return object.__getattribute__(self, name.replace("_please", "")) raise AttributeError("And the magic word!?") gentle = GentleGuy() gentle.coffee = "some coffee" gentle.coffee
Salt
So, to get the value of the attribute
attrname of instance
a in python:
- If a .__ class __.__ getattribute __ () method is defined, then it is called and the resulting value is returned.
- If attrname is a special (python-defined) attribute, such as __class__ or __doc__ , its value is returned.
- It is checked a .__ class __.__ dict__ for the presence of an entry with attrname . If it exists and the value is a data descriptor, the result of calling the __get __ () method of the descriptor is returned. Also all base classes are checked.
- If a record with the name attrname exists in a .__ dict__ , the value of that record is returned. If a is a class, then the attribute is also searched among its base classes and, if there is a data descriptor there or in __dict__ - the result of the descriptor __get __ () is returned.
- It is checked a .__ class __.__ dict__ , if there is an entry with attrname in it and this is “no data descriptor”, the result is __get __ () descriptor, if the entry exists and there is no descriptor, the value of the entry is returned. Base classes are also searched.
- If there is a .__ class __.__ getattr __ () method, it is called and its result is returned. If there is no such method, AttributeError is thrown away .
To set the
value of the
attrname attribute of instance
a :
- If there is a .__ class __.__ setattr __ () method, it is invoked.
- It is checked a .__ class __.__ dict__ , if it has an entry with attrname and it is a data descriptor, the __set __ () method of the descriptor is called. Base classes are also checked.
- In a .__ dict__ , a value entry is added with the attrname key.
__slots__
As
Guido writes in his
python history about how the new-style classes were invented:
... I was afraid that changes in the class system would have a bad effect on performance. In particular, in order for the data descriptors to work correctly, all manipulations of the object's attributes began with checking the __dict__ class that this attribute is a data descriptor ...
In case users get disappointed with the performance degradation, caring python developers come up with
__slots__ .
The presence of
__slots__ limits the possible names of attributes of an object to those specified there. Also, since all attribute names are now known in advance, removes the need to create
__dict__ instances.
class Slotter: __slots__ = ["a", "b"] s = Slotter() s.__dict__
It turned out that Guido's fears were not justified, but by the time it became clear, it was already too late. In addition, using
__slots__ can actually increase performance, especially by reducing the amount of memory used when creating many small objects.
Conclusion
Attribute access in python can be controlled in a huge number of ways. Each of them solves his problem, and together they fit almost every conceivable scenario of using an object. These mechanisms are the basis of language flexibility, along with multiple inheritance, metaclasses, and other goodies. It took me some time to figure out, understand and, most importantly, accept these many options for the work of attributes. At first glance, it seemed slightly redundant and not particularly logical, but given that it is rarely useful in daily programming, it is nice to have such powerful tools in your arsenal.
I hope that this article also clarified a couple of moments to you that hands did not reach to understand. And now, with fire in the eyes and confidence in the Point, you will write a huge amount of the cleanest, readable and resistant to changes in the requirements of the code! Well, or a comment.
Thank you for your time.
Links
- Shalabh Chaturvedi. Python Attributes and Methods
- Guido Van Rossum. The Inside Story on New-Style Classes
- Python documentation
UPD: Useful link from user
leron :
Python Data Model