Python Language OOP Attributes and Properties: Difference between revisions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search
Line 85: Line 85:
</syntaxhighlight>
</syntaxhighlight>


<code>some_property</code> is interacted with as it was an attribute:
Use <code>some_property</code> as it was an attribute:
<syntaxhighlight lang='py'>
<syntaxhighlight lang='py'>
sc = SomeClass()
sc = SomeClass()

Revision as of 05:55, 23 August 2022

Internal

Overview

Attributes are variables associated with a class that carry state either for the class itself, or for the instances of that class. Properties are class constructs that behave like attributes, but are not variables. Technically, bona-fide attributes, properties and also the methods are all attributes on a class. Methods are just callable attributes.

Attributes

Attributes are variables associated with a class that hold state either for the instances of that class (instance attributes) or for the class itself (class attributes). An object instance carries its state as attributes. To differentiate attributes from properties, they are sometimes referred to as standard data attributes.

Instance Attributes

Declaring Instance Attributes

All instance attributes must be declared inside the __init__() method.

class A:
  def __init__(self):
    self.color = 'blue'

If an attribute is first used in a method other than __init__(), static analysis identifies this as a warning "instance attribute defined outside __init__()".

Accessing and Mutating Instance Attributes

Idiomatic Python favors direct attribute access. The instance attributes can be accessed and mutated inside the class definition using self.<attribute-name>. Outside the class definition, they can be accessed and mutated via the variable holding the reference to the class instance: my_instance.<attribute-name>. If an attribute was not explicitly declared inside the instance with self.some_attribute, even just to be assigned to None, an attempt to access the attribute will end up in:

AttributeError: 'MyClass' object has no attribute 'some_attribute'

Deleting Instance Attributes

An attribute can also be deleted, meaning that it will be removed from the instance it was deleted from.

class A:
    def __init__(self):
        self.color = 'blue'
        
    def delete_attr(self):
        del self.color    

a = A()
a2 = A()
a2.delete_attr()

print(a.color) # this will display "blue"
print(a2.color) # this will raise AttributeError: 'A' object has no attribute 'color'

Deleting instance attributes has limited usefulness.

Attribute Visibility

Unlike in other languages, all attributes are public in Python. There are naming conventions to designate attributes as protected, or even private, but these conventions depend on others' willingness to abide by them - the interpreter won't prevent access to an attribute conventionally declared "protected" or "private", they're still public.

"Protected" Attributes

Prepending a single underscore (_) to the attribute name provides some support for protecting module variables and functions, as well as class attributes and methods. Linters and IDE static analysis will flag protected member access. PyCharm explicitly shows them as "Protected Attributes":

PyCharm Protected Attributes.png

Also see:

Python Language | Leading Underscore Variable Names

"Private" Attributes

Prepending a double underscore (__) (also known as “dunder”) to an instance variable or method effectively makes the variable or method private to its class, using name mangling. Google Python Style Guide discourages this use as it impacts readability and testability, and isn’t really private. It advises to use a single underscore.

Class Attributes

TODO

Properties

A property is semantically equivalent with an attribute, in that is supposed to give read and write access to some state associated with the class instance. However, it does not do it by simply designating a variable to hold the state. It does it by defining the accessors and mutators (getters and setters) methods instead. Properties are customizable attributes.

There are two ways to declare the accessor, mutator and deleted method for state associated with the class instance: using the property() built-in and using decorators.

Defining Properties with the property() Built-in Function

The property() built-in function defines a class construct that acts like a virtual attribute, or a proxy to an attribute, by defining the accessor (getter) function, and optionally the mutator (setter) and the deleter function, as well as a docstring for the property.

class SomeClass:
  def __init__(self):
    self._internal_state = None

  def _some_getter(self):
    return self._internal_state

  def _some_setter(self, value):
    self._internal_state = value

  def _some_deleter(self):
     pass

  some_property = property(_some_getter, _some_setter, _some_deleter, "this is docstring")

Use some_property as it was an attribute:

sc = SomeClass()
sc.some_property = 'elephant'
assert sc.some_property == 'elephant'

⚠️ Do not use the property name as it would be a method name. sc.some_property('elephant') will raise an exception:

TypeError: 'NoneType' object is not callable

Defining Properties with Decorators








The state associated with a computed value can be prevented from being written by omitting the corresponding setter.

Another advantage of using properties over direct attribute access is that if the definition of the attribute changes, only the code within the class definition needs to be changed, not in all the callers.

Properties are class construct that associate getter and setter methods to an attribute. The attribute name can the be used to access and write state inside the class by invoking the associated "property" methods. In other words, the getter and setter methods are "properties" of the attribute with the given name.

class A:
  def __init__(self, c):
    self.internal_color = c

  def get_color(self):
    return self.internal_color

  def set_color(self, c):
    self.internal_color = c

  # 'color' is an attribute, though not explicitly declared on self, and get_color() and set_color() are properties of the attribute
  color = property(get_color, set_color)

The first argument to property() is the getter method, and the second argument is the setter method.

Internal class instance state can be accesses and written through the attribute, though there is no actual attribute with that name initialized on self:

a = A('red')
assert 'red' == a.color
a.color = 'blue'
assert 'blue' == a.color

TO DEPLETE Defining Properties with Decorators

Another way to define properties is with decorators. The same attribute color, which is not declared on self, can defined by two different property methods, one getter and one setter, preceded by corresponding decorators (annotations):

  • @property: it annotates the getter method. ⚠️ The name of the method must match the name of the attribute.
  • @<attribute-name>.setter: it annotates the setter method. ⚠️ The <attribute-name> that is part of the annotation must match the name of the attribute and the name of the setter method.
class A:
    def __init__(self, c):
        self.internal_color = c

    @property
    def color(self):
        return self.internal_color

    # not exposing a setter prevents the attribute from being written
    @color.setter
    def color(self, c):
        self.internal_color = c

The interaction with the internal state is done identically as in the case of the property() declaration:

a = A('red')
assert 'red' == a.color
a.color = 'blue'
assert 'blue' == a.color

For more details on decorators see:

Python Decorators