Python Language OOP: Difference between revisions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search
Line 57: Line 57:
==Class Names==
==Class Names==
{{External|[https://peps.python.org/pep-0008/#class-names PEP 8 – Style Guide for Python Code, Class Names]}}
{{External|[https://peps.python.org/pep-0008/#class-names PEP 8 – Style Guide for Python Code, Class Names]}}
==Attributes and Properties==
==<span id='Attribute'></span><span id='Attributes'></span>Attributes and Properties==
{{Internal|Python Language OOP Attributes and Properties|Attributes and Properties}}
{{Internal|Python Language OOP Attributes and Properties|Attributes and Properties}}
==<span id='Attribute'></span>Attributes==
An object contains data in form of variables called '''attributes'''.
The attributes can be accessed inside the class definition using <code>self.<attribute-name></code>. Outside the class definition, they can be accessed via the variable holding the reference to the class instance: <code>my_instance.<attribute-name></code>. Unlike in other languages, all attributes are public. If an attribute was not explicitly declared inside the instance with <code>self.some_attribute</code>, even just to be assigned to <code>None</code>, an attempt to access the attribute will end up in:
<font size=-1>
AttributeError: 'MyClass' object has no attribute 'some_attribute'
</font>
Python offers two syntactical patterns to expose internal class instance state, either represented by an actual attributes, or computed values: [[#Properties|properties]] and <code>[[#Decorators|@property/@<attribute-name>.setter]]</code> decorators.
===Naming Convention===
Prepending a single underscore (_) has 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":
:::[[File:PyCharm_Protected_Attributes.png]]
<font color=darkkhaki>
Cross-link to [[Python_Language#Leading_Underscore_Variable_Names|Python Language &#124; Leading Underscore Variable Names]]
</font>
===Private Attributes===
<font color=darkkhaki>
Prepending a double underscore (__ aka “dunder”) to an instance variable or method effectively makes the variable or method private to its class, using name mangling. [https://google.github.io/styleguide/pyguide.html#3162-naming-conventions 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.
</font>
===Properties===
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.
<font color=darkkhaki>
* TO PROCESS: [[PyOOP]] "Adding behavior to class data with properties", "Properties in detail"
* TO PROCESS: [[PyOOP]] "Deciding when to use properties"
</font>
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.
<syntaxhighlight lang='py'>
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)
</syntaxhighlight>
The first argument to <code>property()</code> 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 <code>self</code>:
<syntaxhighlight lang='py'>
a = A('red')
assert 'red' == a.color
a.color = 'blue'
assert 'blue' == a.color
</syntaxhighlight>
====<span id='Decorators'></span>Defining Properties with Decorators====
<font color=darkkhaki>TO PROCESS: [[PyOOP]] "Decorators - another way to create properties"</font>
Another way to define [[#Properties|properties]] is with '''decorators'''. The same attribute <code>color</code>, which is not declared on <code>self</code>, can defined by two different property methods, one getter and one setter, preceded by corresponding decorators (annotations):
* <code>@property</code>: it annotates the getter method. ⚠️ The name of the method must match the name of the attribute.
* <code>@<attribute-name>.setter</code>: it annotates the setter method. ⚠️ The  <code><attribute-name></code> that is part of the annotation must match the name of the attribute and the name of the setter method.
<syntaxhighlight lang='py'>
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
</syntaxhighlight>
The interaction with the internal state is done identically as in the case of the <code>property()</code> declaration:
<syntaxhighlight lang='py'>
a = A('red')
assert 'red' == a.color
a.color = 'blue'
assert 'blue' == a.color
</syntaxhighlight>
For more details on decorators see: {{Internal|Python_Decorators#Overview|Python Decorators}}
===Mangling Attribute Names for Privacy===
<font color=darkkhaki>TODO</font>


==<span id='Method'></span>Methods==
==<span id='Method'></span>Methods==

Revision as of 03:41, 23 August 2022

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 modules. 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():
  ...

Class Names

PEP 8 – Style Guide for Python Code, Class Names

Attributes and Properties

Attributes and Properties

Methods

An object contains behavior, in form of functions called methods. 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

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__".

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

Properties, Getters and Setters

In Python, all attributes are public, they can be accessed from outside the class using the variable name that holds a reference to a class' instance.

However, Python has syntax that allows declaring getter and setter methods to access instance state that is not available in form of an actual instance attribute. The instance state in question is known as a property, which is exposed via getter and setter methods.

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 | Class 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__.

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:

assert isinstance(o, MyClass)

Checking whether an Instance is a Class

class A:
  pass

assert type(A) is type

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.

The ID of any object can be obtained with the built-in function id()

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()

Polymorphism

Duck Typing

.