Python Language OOP: Difference between revisions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search
 
(161 intermediate revisions by the same user not shown)
Line 5: Line 5:
<font color=darkkhaki>
<font color=darkkhaki>
* How to call a method from inside the constructor. If I try, the compiler says "Unresolved reference"
* How to call a method from inside the constructor. If I try, the compiler says "Unresolved reference"
* [[PyOOP]] "Introducing object-oriented"
* [[PyOOP]] "Objects and classes"
* [[PyOOP]] "Specifying attributes and behaviors"
* [[PyOOP]] "Data describes objects"
* [[PyOOP]] "Behaviors are actions"
* [[PyOOP]] "Hiding details and creating the public interface"
* [[PyOOP]] "Composition"
* [[PyOOP]] "Inheritance"
* [[PyOOP]] "Inheritance provides abstraction"
* [[PyOOP]] "Multiple inheritance", "Case study"
* [[PyOOP]] "Objects in Python"
* [[PyOOP]] "Creating Python classes"
* [[PyOOP]] "Adding attributes", "Making it do something", "Talking to yourself", "More arguments", "Initializing the object"
* [[PyOOP]] "Explaining yourself"
* [[PyOOP]] "Who can access my data?"
* [[PyOOP]] "When Objects are Alike"
* [[PyOOP]] "Basic inheritance"
* [[PyOOP]] "Extending built-ins"
* [[PyOOP]] "Overriding and super"
* [[PyOOP]] "Multiple inheritance" + "The diamond problem" + "Different sets of arguments"
* [[PyOOP]] "Polymorphism"
* [[PyOOP]] "Abstract base classes" + Using an abstract base class" + "Creating an abstract base class" + "Demystifying the magic" + "Case study"
* [[PyOOP]] "When to Use Object-Oriented Programming" + "Treat objects as objects"
* [[PyOOP]] "Manager objects"
* [[PyOOP]] "Removing duplicate code" + "In practice" + "Case study"
* [[PyOOP]] "Empty objects"
* [[PyOOP]] "Using functions as attributes"
* [[PyOOP]] "Callable objects" + "Case study"
* [[PyOOP]] "Serializing objects" + "Customizing pickles" + "Serializing web objects" + "Case study"
</font>
</font>


=Overview=
=Overview=
Everything in Python is an object, from numbers to functions to modules. Each object has an associated [[Python_Language#Type|type]]. The language offers possibility of declaring custom types, and creating object instances for those types. Unlike [[Python_Language_Modularization#Module|modules]], there can be multiple instances of the same type. Creation of new types is achieved by using the <code>[[#Class|class]]</code> syntax. The custom types such declared are no different, intrinsically, from the types already existing in the runtime. The existing types can be extended.


Attributes and methods.
An object contains data, in form of variables called [[#Attribute|attributes]] and behavior, in form of functions called [[#Method|methods]].


=<span id='Class'><span>Class=
=<span id='Class'><span>Class=
A class is the definition of an [[#Object|object]]. A new [[Python_Language#Type|type]] (class) is defined with the <code>[[Python_Language#class|class]]</code> keyword. "Class" and "type" mean pretty much the same thing.


<syntaxhighlight lang='py'>
<syntaxhighlight lang='py'>
Line 24: Line 55:
   ...
   ...
</syntaxhighlight>
</syntaxhighlight>
Classes are typically defined at the [[Python_Language_Modularization#Modules|module]] level, but they can also be defined inside a function or a method. A class created inside the scope of a function cannot be accesses from anywhere outside that function.
==Class Names==
{{External|[https://peps.python.org/pep-0008/#class-names PEP 8 – Style Guide for Python Code, Class Names]}}
==<span id='Attribute'></span><span id='Attributes'></span>Attributes and Properties==
{{Internal|Python Language OOP Attributes and Properties|Attributes and Properties}}
===Class (Static) Attributes===
{{Internal|Python_Language_OOP_Attributes_and_Properties#Class_.28Static.29_Attributes|Class (Static) Attributes}}
==<span id='Method'></span>Methods==
==<span id='Method'></span>Methods==
==Special Methods==
An object contains behavior, in form of functions called '''methods'''.  Methods are callable [[Python_Language_OOP_Attributes_and_Properties#Overview|attributes]].
===<tt>__str__()</tt>===
 
Unlike in other languages, all methods are public.
 
Calling class methods from inside class methods:
<syntaxhighlight lang='py'>
class MyClass:
  def __init__(self):
    self.internal()
 
  def internal(self):
    pass
</syntaxhighlight>
 
===The <code>self</code> Parameter===
 
===Special Methods===
====<tt>__init__()</tt>====
The object instance initialization method, which is used to do anything that's needed to distinguish an object instance from other object instances of the same class. The <code>self</code> argument specifies that it refers to the individual object itself. <code>self</code> is not a reserved word in Python, but it is used conventionally, and it's a good idea to stick with the convention.
<syntaxhighlight lang='py'>
class MyClass:
  def __init__(self, name, color):
    self.name = name
    self.color = color
</syntaxhighlight>
An <code>__init__()</code> method is not required.
 
It is good practice to define all attributes in a contiguous block in <code>__init__()</code>, even if we have to assign them to <code>None</code>, and possibly document them. If we call other "private" methods from <code>__init__()</code> that initialize attributes, an attempt to initialize an attribute not declared in <code>__init__()</code> will be signaled as a warning: "Instance attribute key defined outside __init__".
 
The <code>__init__()</code> cannot return anything.
 
<font color=darkkhaki>Can there be more than one __init__() methods, each with a different set of arguments, like overloaded constructors?</font>
 
====<tt>__str__()</tt>====
<code>__str__()</code> is used by <code>print()</code>, <code>str()</code> and the string formatters to produce a string representation of an instance.
<code>__str__()</code> is used by <code>print()</code>, <code>str()</code> and the string formatters to produce a string representation of an instance.
<syntaxhighlight lang='py'>
<syntaxhighlight lang='py'>
Line 34: Line 106:
       return self.name + ', ' + self.color
       return self.name + ', ' + self.color
</syntaxhighlight>
</syntaxhighlight>
====<tt>__eq__()</tt>====
{{Internal|__eq__() and __hash__() in Python#Override|<tt>__eq__()</tt> and <tt>__hash__()</tt> in Python}}
====<tt>__hash__()</tt>====
{{Internal|__eq__() and __hash__() in Python#Override|<tt>__eq__()</tt> and <tt>__hash__()</tt> in Python}}
===Magic Methods===
The "magic methods" are Python protocol methods. <font color=darkkhaki>What is Python protocol method? Is it a [[#Special_Methods|special method]]?</font> <code>[[Python_Mocking_with_unitest.mock_2#Mocking_a_Class_Instance_with_a_MagicMock|MagicMock]]</code> has default implementations of most of the  magic methods.
===<span id='Properties.2C_Getters_and_Setters'></span>Getters and Setters===
{{Internal|Python_Language_OOP_Attributes_and_Properties#Getters_and_Setters|Attributes and Properties &#124; Getters and Setters}}


==<span id='Static_Method'></span>Static Methods==
===<span id='Static_Method'></span>Static Methods===
<syntaxhighlight lang='py'>
class SomeClass:
  @staticmethod
  def some_static_method():
    ...
</syntaxhighlight>
 
===<span id='Class_Method'></span>Class Methods===
<syntaxhighlight lang='py'>
class SomeClass:
  @classmethod
  def some_class_method(cls):
    ...
</syntaxhighlight>
 
==Static Initialization at Class Level==
To perform an initialization once, upon creation of the type in the class' namespace, place the code directly under the code definition: All methods placed into the class are also "executed" - like when defining a function in the global namespace - but they are not calling.
<syntaxhighlight lang='py'>
class MyClass:
  # executed only once upon creation of the type
  pattern = re.compile(r'^(\w+):(\w+)-(\w+)$')
 
  def __init__(self):
    ...
  def my_method(self):
    ...
</syntaxhighlight>
 
The static variable can be accessed inside the class (and outside) by prefixing it with the class name, or, within an instance method, by prefixing it with <code>self</code>:
<syntaxhighlight lang='py'>
class MyClass:
  some_var = 'something'
 
  def some_method(self):
    print('some_var value', MyClass.some_var)
    print('some_var value', self.some_var)
</syntaxhighlight>
==Class Introspection==
{{Internal|Python Introspection#Overview|Python Introspection}}
===Access to Class Instance from the Class Code===
Use the variable <code>self.__class__</code>.
 
<code>__class__</code> also resolves, but in case of a class hierarchy, it won't behave polymorphically, it will resolve to the superclass, not the actual class.
 
===Access to Class Name from the Class Code===
Use the variable <code>self.__class__.__name__</code>.
===Access to Module the Class Belongs To===
 
<syntaxhighlight lang='py'>
sample = Sample()
 
module_name = sample.__class__.__module__ # 'my_package.Sample'
</syntaxhighlight>
 
Note <code>__module__</code> contains the '''name''' of the module, as string. The module can be an [[Python_Language_Modularization#Modules|ordinary module]] or a [[Python_Language_Modularization#Package|package]].
 
==Access Control==
All methods and attributes in a class are publicly available. If you want to suggest that a method should not be used publicly, add a note in the docstring indicating that this method is meant for internal use only. By convention, you can also prefix an internal attribute with the underscore character '_'. Python programmers will interpret this as an indication that this is an internal variable which should not be accessed or modified from the outside of the class.
===Name Mangling===
<font color=darkkhaki>
If the name of a variable or method is prefixed with a double underscore '__', the interpreter will perform name mangling, which mean the property is prefixed with <code>_<class-name></code>. When class methods access the variable internally, the name of the variable is automatically untangled. When external classes wish to access it, they will have to do the name mangling themselves. TODO.</font>
 
=Type Checking=
The type of a class instance can be displayed with <code>type()</code>.
 
<syntaxhighlight lang='py'>
class MyClass:
  ...
 
i = MyClass()
print(type(c))
</syntaxhighlight>
will display:
<font size=-1>
<class '__main__.MyClass'>
</font>
 
Whether the instance if of specific type can be verified with:
<syntaxhighlight lang='py'>
assert type(i) is MyClass
</syntaxhighlight>
 
<span id='isinstance'></span>Alternatively, <code>[[Python Language Functions#isinstance|isinstance()]]</code> built-in is available:
<syntaxhighlight lang='py'>
assert isinstance(o, MyClass)
</syntaxhighlight>
 
{{External|https://pynative.com/python-isinstance-explained-with-examples}}
==Checking whether an Instance is a Class==
<syntaxhighlight lang='py'>
class A:
  pass
 
assert type(A) is type
</syntaxhighlight>
==Checking whether a Class is Subclass of Another Class==
 
Checked with the <code>[[Python Language Functions#issubclass|issubclass()]]</code> built-in:
<syntaxhighlight lang='py'>
class A:
    pass
 
class B(A):
    pass
 
assert issubclass(B, A)
</syntaxhighlight>
==Checking whether an Instance is a Function==
{{Internal|Python_Language_Functions#Check_whether_an_Object_Instance_is_a_Function|Functions &#124; Check whether an Object Instance is a Function}}
 
==Checking whether an Instance is a Module==
{{Internal|Python_Module_Internal_Representation_and_Introspection#Checking_whether_an_Object_Instance_is_a_Module|Python Module Internal Representation and Introspection &#124; Checking whether an Object Instance is a Module}}
 
=Storing Classes in Files=
==A Class per File (Module)==
This pattern mimics Java's approach of storing one public class per file.
<syntaxhighlight lang='text'>
.
└─ src
    └─ some_package
        └─ some_subpackage_1
            └─ some_subpackage_2
                └─ SomeClass.py
</syntaxhighlight>
Class declaration:
<syntaxhighlight lang='py'>
class SomeClass():
  pass
</syntaxhighlight>
Class consumption:
<syntaxhighlight lang='py'>
from some_package.some_subpackage_1.some_subpackage_2.SomeClass import SomeClass
 
sc = SomeClass()
</syntaxhighlight>
 
==Multiple Classes per File(Module)==
 
=<span id='Object'></span>Objects=
In Python, everything is an object. To create an instance of a class, use the class name followed by left parenthesis, followed by constructor arguments, followed by the right parenthesis.
<syntaxhighlight lang='py'>
o2 = SomeOtherClass('something')
</syntaxhighlight>
If only the class name without parentheses is used, the code will work, but the variable will contain a reference to the class, not the instance of the class.
==Identity of an Object Instance==
{{External|https://docs.python.org/3/library/functions.html#id}}
The identity of an object instance is an integer which is guaranteed to be unique and constant for the duration of the object's lifetime. The identity of any object can be obtained with the [[Python_Language_Functions#id.28.29|built-in function <code>id()</code>]]. Two objects with non-overlapping lifetimes may have the same <code>id()</code> value. As an implementation detail, the identity is the address of the object in memory.
 
Also see: {{Internal|Eq_()_and_hash_()_in_Python#Overview|<tt>__eq__()</tt> and <tt>__hash__()</tt> in Python}}
 
==Object Instantiation==
===Via Class Name===
<syntaxhighlight lang='py'>
class SomeClass:
  def __init__(self, arg):
    ...
 
sc = SomeClass('blue')
</syntaxhighlight>
===Via Class Instance===
Given access to the instance of the class defined above, an object of that class can be instantiated by using the same constructor syntax on the class instance instead of the class name:
<syntaxhighlight lang='py'>
# this is the SomeClass class
cls = ...
 
sc = cls('blue')
</syntaxhighlight>


=Initialization=
=Initialization=
Line 42: Line 291:


==Overriding==
==Overriding==
If a method is defined in the base class but not defined in the subclass, the base class method is available to the subclass instances.
<syntaxhighlight lang='py'>
class SomeClass:
    def some_method(self):
        print('this is SomeClass.some_method()')
class SomeSubClass(SomeClass):
    pass
SomeSubClass().some_method()
# will print:
# this is SomeClass.some_method()
</syntaxhighlight>
===Property Overriding===
If a base class has a getter and a setter, and the subclass overrides '''only the getter''', that makes the superclass' setter inaccessible:
<syntaxhighlight lang='py'>
class Super:
    def __init__(self):
        self._color = 'white'
    @property
    def color(self):
        return self._color
    @color.setter
    def color(self, value):
        self._color = value
class Sub(Super):
    def __init__(self):
        super().__init__()
    @property
    def color(self):
        return self._color.upper()
sub = Sub()
sub.color = 'gray'
</syntaxhighlight>
results in:
<syntaxhighlight lang='text'>
Traceback (most recent call last):
  File "/Users/ovidiu/src/github.com/playground/pyhton/experiments2/main.py", line 26, in <module>
    sub.color = 'gray'
AttributeError: can't set attribute 'color'
</syntaxhighlight>
To enable access to setter, it must also be overridden in the subclass, even if the implementation just delegates to the superclass:
<syntaxhighlight lang='py'>
class Sub(Super):
    def __init__(self):
        super().__init__()
    @property
    def color(self):
        return self._color.upper()
    @color.setter
    def color(self, value):
        self._color = value
sub = Sub()
sub.color = 'gray'
assert sub.color == 'GRAY'
</syntaxhighlight>
===Overriding and Static Methods===


=Polymorphism=
=Polymorphism=


.
=Duck Typing=
 
Often you may not care about the type of an object but rather only whether it has certain methods or behaviors. This is sometimes called '''duck typing''', after the saying: "if it walks like a duck and quacks like a duck, then it's a duck". For example, you can verify that an object is iterable if it implements the iterator protocol. For many objects, this means it has an <code>__iter__</code> "magic method", though an alternative and better way to check is to try to use the <code>iter()</code> function on it. If the object is not iterable, <code>iter()</code> will raise a <code>TypeError</code> exception.

Latest revision as of 23:02, 15 May 2024

External

Internal

TODO

  • How to call a method from inside the constructor. If I try, the compiler says "Unresolved reference"
  • PyOOP "Introducing object-oriented"
  • PyOOP "Objects and classes"
  • PyOOP "Specifying attributes and behaviors"
  • PyOOP "Data describes objects"
  • PyOOP "Behaviors are actions"
  • PyOOP "Hiding details and creating the public interface"
  • PyOOP "Composition"
  • PyOOP "Inheritance"
  • PyOOP "Inheritance provides abstraction"
  • PyOOP "Multiple inheritance", "Case study"
  • PyOOP "Objects in Python"
  • PyOOP "Creating Python classes"
  • PyOOP "Adding attributes", "Making it do something", "Talking to yourself", "More arguments", "Initializing the object"
  • PyOOP "Explaining yourself"
  • PyOOP "Who can access my data?"
  • PyOOP "When Objects are Alike"
  • PyOOP "Basic inheritance"
  • PyOOP "Extending built-ins"
  • PyOOP "Overriding and super"
  • PyOOP "Multiple inheritance" + "The diamond problem" + "Different sets of arguments"
  • PyOOP "Polymorphism"
  • PyOOP "Abstract base classes" + Using an abstract base class" + "Creating an abstract base class" + "Demystifying the magic" + "Case study"
  • PyOOP "When to Use Object-Oriented Programming" + "Treat objects as objects"
  • PyOOP "Manager objects"
  • PyOOP "Removing duplicate code" + "In practice" + "Case study"
  • PyOOP "Empty objects"
  • PyOOP "Using functions as attributes"
  • PyOOP "Callable objects" + "Case study"
  • PyOOP "Serializing objects" + "Customizing pickles" + "Serializing web objects" + "Case study"

Overview

Everything in Python is an object, from numbers to functions to modules. Each object has an associated type. The language offers possibility of declaring custom types, and creating object instances for those types. Unlike modules, there can be multiple instances of the same type. Creation of new types is achieved by using the class syntax. The custom types such declared are no different, intrinsically, from the types already existing in the runtime. The existing types can be extended.

An object contains data, in form of variables called attributes and behavior, in form of functions called methods.

Class

A class is the definition of an object. A new type (class) is defined with the class keyword. "Class" and "type" mean pretty much the same thing.

class MyClass:
  def __init__(self):
    pass

The class may be declared with parentheses, but the IDE static checks find those as "redundant":

class MyClass():
  ...

Classes are typically defined at the module level, but they can also be defined inside a function or a method. A class created inside the scope of a function cannot be accesses from anywhere outside that function.

Class Names

PEP 8 – Style Guide for Python Code, Class Names

Attributes and Properties

Attributes and Properties

Class (Static) Attributes

Class (Static) Attributes

Methods

An object contains behavior, in form of functions called methods. Methods are callable attributes.

Unlike in other languages, all methods are public.

Calling class methods from inside class methods:

class MyClass:
  def __init__(self):
    self.internal()

  def internal(self):
     pass

The self Parameter

Special Methods

__init__()

The object instance initialization method, which is used to do anything that's needed to distinguish an object instance from other object instances of the same class. The self argument specifies that it refers to the individual object itself. self is not a reserved word in Python, but it is used conventionally, and it's a good idea to stick with the convention.

class MyClass:
  def __init__(self, name, color):
    self.name = name
    self.color = color

An __init__() method is not required.

It is good practice to define all attributes in a contiguous block in __init__(), even if we have to assign them to None, and possibly document them. If we call other "private" methods from __init__() that initialize attributes, an attempt to initialize an attribute not declared in __init__() will be signaled as a warning: "Instance attribute key defined outside __init__".

The __init__() cannot return anything.

Can there be more than one __init__() methods, each with a different set of arguments, like overloaded constructors?

__str__()

__str__() is used by print(), str() and the string formatters to produce a string representation of an instance.

class MyClass:
  ...
  def __str__(self):
      return self.name + ', ' + self.color

__eq__()

__eq__() and __hash__() in Python

__hash__()

__eq__() and __hash__() in Python

Magic Methods

The "magic methods" are Python protocol methods. What is Python protocol method? Is it a special method? MagicMock has default implementations of most of the magic methods.

Getters and Setters

Attributes and Properties | Getters and Setters

Static Methods

class SomeClass:
  @staticmethod
  def some_static_method():
     ...

Class Methods

class SomeClass:
  @classmethod
  def some_class_method(cls):
     ...

Static Initialization at Class Level

To perform an initialization once, upon creation of the type in the class' namespace, place the code directly under the code definition: All methods placed into the class are also "executed" - like when defining a function in the global namespace - but they are not calling.

class MyClass:
  # executed only once upon creation of the type
  pattern = re.compile(r'^(\w+):(\w+)-(\w+)$')

  def __init__(self):
    ...
  def my_method(self):
    ...

The static variable can be accessed inside the class (and outside) by prefixing it with the class name, or, within an instance method, by prefixing it with self:

class MyClass:
  some_var = 'something'

  def some_method(self):
    print('some_var value', MyClass.some_var)
    print('some_var value', self.some_var)

Class Introspection

Python Introspection

Access to Class Instance from the Class Code

Use the variable self.__class__.

__class__ also resolves, but in case of a class hierarchy, it won't behave polymorphically, it will resolve to the superclass, not the actual class.

Access to Class Name from the Class Code

Use the variable self.__class__.__name__.

Access to Module the Class Belongs To

sample = Sample()

module_name = sample.__class__.__module__ # 'my_package.Sample'

Note __module__ contains the name of the module, as string. The module can be an ordinary module or a package.

Access Control

All methods and attributes in a class are publicly available. If you want to suggest that a method should not be used publicly, add a note in the docstring indicating that this method is meant for internal use only. By convention, you can also prefix an internal attribute with the underscore character '_'. Python programmers will interpret this as an indication that this is an internal variable which should not be accessed or modified from the outside of the class.

Name Mangling

If the name of a variable or method is prefixed with a double underscore '__', the interpreter will perform name mangling, which mean the property is prefixed with _<class-name>. When class methods access the variable internally, the name of the variable is automatically untangled. When external classes wish to access it, they will have to do the name mangling themselves. TODO.

Type Checking

The type of a class instance can be displayed with type().

class MyClass:
  ...

i = MyClass()
print(type(c))

will display:

<class '__main__.MyClass'>

Whether the instance if of specific type can be verified with:

assert type(i) is MyClass

Alternatively, isinstance() built-in is available:

assert isinstance(o, MyClass)
https://pynative.com/python-isinstance-explained-with-examples

Checking whether an Instance is a Class

class A:
  pass

assert type(A) is type

Checking whether a Class is Subclass of Another Class

Checked with the issubclass() built-in:

class A:
    pass

class B(A):
    pass

assert issubclass(B, A)

Checking whether an Instance is a Function

Functions | Check whether an Object Instance is a Function

Checking whether an Instance is a Module

Python Module Internal Representation and Introspection | Checking whether an Object Instance is a Module

Storing Classes in Files

A Class per File (Module)

This pattern mimics Java's approach of storing one public class per file.

.
└─ src
    └─ some_package
        └─ some_subpackage_1
            └─ some_subpackage_2
                └─ SomeClass.py

Class declaration:

class SomeClass():
  pass

Class consumption:

from some_package.some_subpackage_1.some_subpackage_2.SomeClass import SomeClass

sc = SomeClass()

Multiple Classes per File(Module)

Objects

In Python, everything is an object. To create an instance of a class, use the class name followed by left parenthesis, followed by constructor arguments, followed by the right parenthesis.

o2 = SomeOtherClass('something')

If only the class name without parentheses is used, the code will work, but the variable will contain a reference to the class, not the instance of the class.

Identity of an Object Instance

https://docs.python.org/3/library/functions.html#id

The identity of an object instance is an integer which is guaranteed to be unique and constant for the duration of the object's lifetime. The identity of any object can be obtained with the built-in function id(). Two objects with non-overlapping lifetimes may have the same id() value. As an implementation detail, the identity is the address of the object in memory.

Also see:

__eq__() and __hash__() in Python

Object Instantiation

Via Class Name

class SomeClass:
  def __init__(self, arg):
     ...

sc = SomeClass('blue')

Via Class Instance

Given access to the instance of the class defined above, an object of that class can be instantiated by using the same constructor syntax on the class instance instead of the class name:

# this is the SomeClass class
cls = ...

sc = cls('blue')

Initialization

Inheritance

Overriding

If a method is defined in the base class but not defined in the subclass, the base class method is available to the subclass instances.

class SomeClass:
    def some_method(self):
        print('this is SomeClass.some_method()')

class SomeSubClass(SomeClass):
    pass

SomeSubClass().some_method()

# will print:
# this is SomeClass.some_method()

Property Overriding

If a base class has a getter and a setter, and the subclass overrides only the getter, that makes the superclass' setter inaccessible:

class Super:
    def __init__(self):
        self._color = 'white'

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

    @color.setter
    def color(self, value):
        self._color = value


class Sub(Super):
    def __init__(self):
        super().__init__()

    @property
    def color(self):
        return self._color.upper()


sub = Sub()
sub.color = 'gray'

results in:

Traceback (most recent call last):
  File "/Users/ovidiu/src/github.com/playground/pyhton/experiments2/main.py", line 26, in <module>
    sub.color = 'gray'
AttributeError: can't set attribute 'color'

To enable access to setter, it must also be overridden in the subclass, even if the implementation just delegates to the superclass:

class Sub(Super):
    def __init__(self):
        super().__init__()

    @property
    def color(self):
        return self._color.upper()

    @color.setter
    def color(self, value):
        self._color = value


sub = Sub()
sub.color = 'gray'
assert sub.color == 'GRAY'

Overriding and Static Methods

Polymorphism

Duck Typing

Often you may not care about the type of an object but rather only whether it has certain methods or behaviors. This is sometimes called duck typing, after the saying: "if it walks like a duck and quacks like a duck, then it's a duck". For example, you can verify that an object is iterable if it implements the iterator protocol. For many objects, this means it has an __iter__ "magic method", though an alternative and better way to check is to try to use the iter() function on it. If the object is not iterable, iter() will raise a TypeError exception.