Collection of Utilities
Project description
WorkToy v0.99.xx
WorkToy collects common utilities. It is available for installation via pip:
pip install worktoy
Version 0.99.xx is in final stages of development. It will see no new features, only bug fixes and documentation updates. Upon completion of tasks given below, version 1.0.0 will be released. Navigate with the table of contents below.
Table of Contents
Installation
pip install worktoy
Usage
worktoy.desc
Background - The Python Descriptor Protocol
The descriptor protocol in Python allows significant customisation of the attribute access mechanism. To understand this protocol, consider a class body assigning an object to a name. During the class creation process, when this line is reached, the object is assigned to the name. For the purposes of this discussion, the object is created when this line is reached, for example:
class PlanePoint:
"""This class represent an integer valued point in the plane. """
x = Integer(0)
y = Integer(0) # Integer is defined below. In practice, classes should
# be defined in dedicated files.
The above class ´PlanePoint´ owns a pair of attributes. These are instances of the ´Integer´ class defined below. The ´Integer´ class is a descriptor and is thus the focus of this discussion.
class Integer:
"""This descriptor class wraps an integer value. More details will be
added throughout this discussion."""
__fallback_value__ = 0
__default_value__ = None
__field_name__ = None
__field_owner__ = None
def __init__(self, *args) -> None:
for arg in args:
if isinstance(arg, int):
self.__default_value__ = arg
break
else:
self.__default_value__ = self.__fallback_value__
# The '__init__' method implemented above makes use of the unusual
# 'else' clause at the end of a loop. This clause is executed once after
# the loop has completed. Since it is part of the loop, the 'break'
# keyword applies to it as well as the loop itself. The for loop above
# iterates through the positional arguments and if it encounters an
# 'int' argument, it assigns it and issues the 'break'. So if the loop
# completes without hitting the 'break', no 'int' could be found in any
# of the positional arguments. Conveniently, the 'else' block then
# assigns the fallback value.
def __set_name__(self, owner: type, name: str) -> None:
"""Powerful method called automatically when the class owning the
descriptor instance is finally created. It informs the descriptor
instance of its owner and importantly, it informs the descriptor of
the name by which it appears in the class body. """
self.__field_name__ = name
self.__field_owner__ = owner
def __get__(self, instance: object, owner: type) -> int:
"""Getter-function."""
The __set_name__
method
Python 3.6 was released on December 23, 2016. This version introduced
the __set_name__
method, which marks a significant improvement to the
descriptor protocol. It informs instances of descriptor classes of the
class that owns them and the name by which they appear in the class
namespace. Much of the functionality found in the worktoy.desc
module
would not be possible without this method.
The __get__
method
Consider the code below:
class Descriptor:
"""Descriptor class."""
def __get__(self, instance: object, owner: type) -> object:
"""Getter-function."""
def __set_name__(self, owner: type, name: str) -> None:
"""Informs the descriptor instance that the owner is created"""
class OwningClass:
"""Class owning the descriptor."""
descriptor = Descriptor()
# At this point in the code, the OwningClass is created, which triggers
# the '__set_name__' method on the descriptor instance.
# Event 1
if __name__ == '__main__':
owningInstance = OwningClass() # Event 2
print(OwningClass.descriptor) # Event 3
print(owningInstance.descriptor) # Event 4
Let us examine each of the four events marked in the above example:
- Event 1 - This marks the completion of the class creation process,
which is described in excruciating detail in later sections. Of
interest here is the call to the
__set_name__
method on the descriptor:Descriptor.__set_name__(descriptor, OwningClass, 'descriptor')
- Event 2 - This marks the creation of an instance of the owning class. This event is not of interest to this discussion.
- Event 3 - Accessing the descriptor on the OwningClass triggers the
following function call:
Descriptor.__get__(descriptor, None, OwningClass)
- Event 4 - Accessing the descriptor on an instance of the owning
class triggers the following function call:
Descriptor.__get__(descriptor, owningInstance, OwningClass)
The __get__
determines what is returned when the descriptor is
accessed. Please note that this method is what is called every time the
descriptor instance is accessed regardless of how. In the above example,
the following would each result in a call to the __get__
method:
# Each result in: Descriptor.__get__(descriptor, None, OwningClass)
OwningClass.descriptor
getattr(OwningClass, 'descriptor')
object.__getattribute__(OwningClass, 'descriptor')
The interpreter always refers to the __get__
method. To still allow
access to the descriptor object itself, the common pattern is for the
__get__
method to return the descriptor instance when accessed
through the owning class:
class Descriptor:
"""Descriptor class."""
def __get__(self, instance: object, owner: type) -> object:
"""Getter-function."""
if instance is None:
return self
# YOLO
This author suggests never deviating from this pattern. Perhaps some more functionality or some hooks may be implemented, but the descriptor instance itself should always be returned when accessed through the owning class. When accessed through the instance, the descriptor is free to do whatever it wants!
The __set__
method
The prior section focused on the distinction between accessing on the
class or instance level. The __set__
method defined on the Descriptor
is invoked only when accessed through the instance:
class Descriptor:
"""Descriptor class."""
# Code as before
def __set__(self, instance: object, value: object) -> None:
"""Setter-function."""
if __name__ == '__main__':
owningInstance = OwningClass()
owningInstance.descriptor = 69 # Event 1
print(owningInstance.descriptor) # Event 2
OwningClass.descriptor = 420 # Event 3
print(owningInstance.descriptor) # Event 4
The above code triggers the following function call:
-Event 1: Descriptor.__set__(descriptor, owningInstance, 7)
-Event 2: Descriptor.__get__(descriptor, owningInstance, OwningClass)
-Event 3: This call does not involve the descriptor at all, instead
it simply overwrites the descriptor.
-Event 4: The previous event overwrote the descriptor instance so now
it simply returns the value set in Event 3.
Thus, when trying to call __set__
through the owning class, it is
applied to the descriptor object itself. This matches the suggested
implementation of the __get__
method which would return the
descriptor item itself, when accessed through the owning class.
The delete
method
The most important comment here is this warning: Do not mistake
__del__
and __delete__
! __del__
is a mysterious method
associated with the destruction of an object.
The __delete__
method on the other hand allows instance specific
control over what happens when the del
keyword is used on an
attribute through an instance. If the descriptor class does not
implement it, it will raise an AttributeError
when the interpreter
tries to invoke __delete__
on the descriptor instance. Deleting an
attribute through the owning class does not involve the descriptor at all,
but is managed on the metaclass level. Generally, the descriptor is just
yeeted in this case.
class Descriptor:
"""Descriptor class."""
# Code as before
def __delete__(self, instance: object, ) -> None:
"""Setter-function."""
if __name__ == '__main__':
owningInstance = OwningClass()
del owningInstance.descriptor # event 1
print(owningInstance.descriptor) # event 2
If a descriptor class does implement the __delete__
method the script
above is expected to trigger the following:
-Event 1: Descriptor.__delete__(descriptor, owningInstance)
-Event 2a: AttributeError: __delete__
if the descriptor does not
implement the method.
-Event 2b: AttributeError: 'OwningClass' object has no attribute 'descriptor'!
if the descriptor does implement the method and allows
deletion to proceed.
-Event 2c: A custom implementation of the __delete__
method which
does not allow deletion and thus raises an appropriate exception. In this
case, this author suggests raising a TypeError
to indicate that the
attribute is of a read-only type. This is different than the
AttributeError
which generally indicate the absense of the attribute.
If a custom implementation of the __delete__
method is provided, 2b
or 2c as described above should happen, as this is the generally expected
behaviour in Python. Implementing some alternative behaviour might be
cute or something, but when the code is then used elsewhere, the bugs
resulting from this unexpected behaviour are nearly impossible to find.
Descriptor Protocol Implementations
There are three questions to consider before discussing implementation details:
- Is the descriptor class simply a way for owning classes to enhance attribute access?
- Should classes implement the descriptor protocol to define their behaviour when owned by other classes?
- Should a central descriptor class define how one class can own an instance of another?
There is certainly a need for classes to specialize access to their
attributes. This is the most common use of the descriptor protocol.
Python itself implements the property
class to provide this control.
When creating a custom class, it seems reasonable to consider that other classes might own instance of it and to implement descriptor protocol methods as appropriate. Unfortunately, this is not commonly done meaning that you can expect to have to own instances of classes that do not provide for this ownership interaction themselves.
The AttriBox
class provided by the worktoy.desc
module does
implement the descriptor protocol in a way designed to allow one class to
own instances of another without either having to implement anything
related to the descriptor protocol. This class also makes the second
question redundant.
This discussion will now proceed with the following:
- Usage of the
property
class provided by Python. - The
worktoy.desc
module provides theAbstractDescriptor
class, which implements the parts of the descriptor protocol used by bothField
andAttriBox
. - Implementation of the vastly superior
Field
class provided by theworktoy.desc
module. 4Usage an examples of theAttriBox
class provided by theworktoy.desc
module.
The property
class
The property
class is a built-in class in Python. It allows the use of
a decorator to define getter, setter and deleter functions for a property.
Alternatively, the property
may be instantiated in the class body
with the getter, setter and deleter functions as arguments. The following
example will demonstrate a class owning a number and a name,
demonstrating the two approaches to defining properties.
from __future__ import annotations
class OwningClass:
"""This class uses 'property' to implement the 'name' attribute. """
__fallback_number__ = 0
__fallback_name__ = 'Unnamed'
__inner_number__ = None
__inner_name__ = None
def __init__(self, *args, **kwargs) -> None:
self.__inner_number__ = kwargs.get('number', None)
self.__inner_name__ = kwargs.get('name', None)
for arg in args:
if isinstance(arg, int) and self.__inner_number__ is None:
self.__inner_number__ = arg
elif isinstance(arg, str) and self.__inner_name__ is None:
self.__inner_name__ = arg
@property
def name(self) -> str:
"""Name property"""
if self.__inner_name__ is None:
return self.__fallback_name__
return self.__inner_name__
@name.setter
def name(self, value: str) -> None:
"""Name setter"""
self.__inner_name__ = value
@name.deleter
def name(self) -> None:
"""Name deleter"""
del self.__inner_name__
def _getNumber(self) -> int:
"""Number getter"""
if self.__inner_number__ is None:
return self.__fallback_number__
return self.__inner_number__
def _setNumber(self, value: int) -> None:
"""Number setter"""
self.__inner_number__ = value
def _delNumber(self) -> None:
"""Number deleter"""
del self.__inner_number__
number = property(_getNumber, _setNumber, _delNumber, doc='Number')
The above example demonstrates the use of the property
class to enhance
the attribute access mechanism.
The AbstractDescriptor
class
The AbstractDescriptor
class provides the __set_name__
method and
delegates accessor functions to the following methods:
__instance_get__
: Getter-function (Required!)__instance_set__
: Setter-function (Optional)__instance_del__
: Deleter-function (Optional)
When implementing __instance_get__
to handle a missing value, the
subclass must raise a MissingValueException
passing the instance and
itself to the constructor of it. This missing value situation is one
where no default value is provided and the value has not been set. The
AbstractDescriptor
calls __instance_get__
during the __set__
and delete__
methods to collect the old value which is used in the
notification. If it catches such an error during __get__
, it raises
an AttributeError
from it.
The AbstractDescriptor
provides descriptors to mark methods to be
notified when the attribute is accessed. The methods are:
ONGET
: Called when the attribute is accessed.ONSET
: Called when the attribute is set.ONDEL
: Called when the attribute is deleted.
Both Field
and AttriBox
subclass the AbstractDescriptor
class.
These are discussed below.
The Field
class
The Field
class provides descriptors in addition to those implemented
on the AbstractDescriptor
class. These are used by owning classes to
mark accessor methods, much like the property
class. Unlike the
Python property
class, instances of the Field
class must be
defined before the accessor methods in the class body. The decorators
require their field instance to be defined in the lexical scope before
they are available to decorate methods.
Below is an example of a plane point implementing the coordinate
attributes using the Field
class:
from __future__ import annotations
from worktoy.desc import Field
class Point:
"""This class uses the 'Field' descriptor to implement the coordinate
attributes. """
__x_value__ = None
__y_value__ = None
x = Field()
y = Field()
@x.GET
def _getX(self) -> float:
return self.__x_value__
@x.SET
def _setX(self, value: float) -> None:
self.__x_value__ = value
@y.GET
def _getY(self) -> float:
return self.__y_value__
@y.SET
def _setY(self, value: float) -> None:
self.__y_value__ = value
def __init__(self, *args, **kwargs) -> None:
self.__x_value__ = kwargs.get('x', None)
self.__y_value__ = kwargs.get('y', None)
for arg in args:
if isinstance(arg, int):
arg = float(arg)
if isinstance(arg, float):
if self.__x_value__ is None:
self.__x_value__ = arg
elif self.__y_value__ is None:
self.__y_value__ = arg
break
else:
if self.__x_value__ is None:
self.__x_value__, self.__y_value__ = 69., 420.
elif self.__y_value__ is None:
self.__y_value__ = 420.
The Field
class allows classes to implement how attributes are accessed.
The AttriBox
class
Where Field
relies on the owning class itself to specify the accessor
functions, the AttriBox
class provides an attribute of a specified
class. This class is not instantiated until an instance of the owning
class calls the __get__
method. Only then will the inner object of
the specified class be created. The inner object is then placed on a
private variable belonging to the owning instance. When the __get__
is next called the inner object at the private variable is returned. When
instantiating the AttriBxo
class, the following syntactic sugar
should be used: fieldName = AttriBox[FieldClass](*args, **kwargs)
.
The arguments placed in the parentheses after the brackets are those used
to instantiate the FieldClass
given in the brackets.
Below is an example of a class using the AttriBox
class to implement
a Circle
class. It uses the Point
class defined above to manage
the center of the circle. Notice how the Point
class itself is wrapped
in an AttriBox
instance. The area
attribute is defined using the
Field
class and illustrates the use of the Field
class to expose
a value as an attribute. Finally, it used the ONSET
decorator to mark
a method as the validator for the radius attribute. This causes the
method to be hooked into the __set__
method on the radius
.
from __future__ import annotations
class Circle:
"""This class uses the 'AttriBox' descriptor to manage the radius and
center, and it also illustrates a use case for the 'Field' class."""
radius = AttriBox[float](0)
center = AttriBox[Point](0, 0)
area = Field()
@area.GET
def _getArea(self) -> float:
return 3.1415926535897932 * self.radius ** 2
@radius.ONSET
def _validateRadius(self, _, value: float) -> None:
if value < 0:
e = """Received negative radius!"""
raise ValueError(e)
def __init__(self, *args, **kwargs) -> None:
"""Constructor omitted..."""
def __str__(self) -> str:
msg = """Circle centered at: (%.3f, %.3f), with radius: %.3f"""
return msg % (self.center.x, self.center.y, self.radius)
if __name__ == '__main__':
circle = Circle(69, 420, 1337)
print(circle)
circle.radius = 1
print(circle)
Running the code above will output the following:
Circle centered at: (69.000, 420.000), with radius: 4.000
Circle centered at: (69.000, 420.000), with radius: 1.000
worktoy.desc
- Summary
As have been demonstrated and explained, the worktoy.desc
module
provides helpful, powerful and flexible implementations of the descriptor
protocol. The Field
allows classes to customize attribute access
behaviour in significant detail. The AttriBox
class provides a way to
set as attribute any class on another class in a single line. As
mentioned briefly, the class contained by the AttriBox
instance is
not instantiated until an instance of the owning class calls the
__get__
method. In the following section, the importance of this lazy
instantiation feature will be illustrated with an example involving the
PySide6
library.
PySide6 - Qt for Python
The PySide6 library provides Python bindings for the Qt framework. Despite
involving bindings to a C++ library, the code itself remains Python and
not C++, thank the LORD. Nevertheless, certain errors do not have a
Pythonic representation. The AttriBox
class was envisioned for this
very reason. Its lazy instantiation system prevents something called
'Segmentation Fault'. You can lead a long and happy life not ever
encountering those words again, thanks to the AttriBox
class!
Central to Qt is the main event loop managed by an instance of
QCoreApplication
or of a subclass of it. While the application is
running, instances of QObject
provide the actual application
functionality. It is reasonable to regard the QObject
in Qt and the
object
object in Python as similar. Every window, every widget, the
running application, managed threads and just about everything else in Qt
is essentially an instance of QObject
. Having defined these terms, we
may now discuss the two central rules of Qt:
- Only one instances of
QCoreApplication
may be running at any time. - The first instantiated
QObject
must be an instance of theQCoreApplication
class.
Instantiating any QObject
before the QCoreApplication
is running
will result in immediate error. Enter the AttriBox
class. The
somewhat inflexible nature of the above rules regains flexibility by
making use of the lazy instantiation provided by the AttriBox
class.
In the following, we will see a simple application consisting of a window
showing a welcome message provided as an instance of the QLabel
class
along with an exit button provided by the QPushButton
class. These
widgets are stacked vertically and are managed by an instance of the
QVBoxLayout
class and finally these are managed by an instance of
QWidget
which is the parent of the widgets and which owns the layout.
This widget is set as the central widget in the main window. The main
window itself is a subclass of the QMainWindow
class.
import sys
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtWidgets import QWidget, QLabel, QPushButton, QVBoxLayout
from PySide6.QtCore import QSize
from worktoy.desc import AttriBox
class MainWindow(QMainWindow):
"""This subclass of QMainWindow provides the main application window. """
baseWidget = AttriBox[QWidget]()
layout = AttriBox[QVBoxLayout]()
welcomeLabel = AttriBox[QLabel]()
exitButton = AttriBox[QPushButton]()
def initUi(self) -> None:
"""This method sets up the user interface"""
self.setMinimumSize(QSize(480, 320))
self.setWindowTitle("Welcome to WorkToy!")
self.welcomeLabel.setText("""Welcome to WorkToy!""")
self.exitButton.setText("Exit")
self.layout.addWidget(self.welcomeLabel)
self.layout.addWidget(self.exitButton)
self.baseWidget.setLayout(self.layout)
self.setCentralWidget(self.baseWidget)
def initSignalSlot(self) -> None:
"""This method connects the signals and slots"""
self.exitButton.clicked.connect(self.close)
def show(self) -> None:
"""This reimplementation calls 'initUi' and 'initSignalSlot' before
calling the parent implementation"""
self.initUi()
self.initSignalSlot()
QMainWindow.show(self)
if __name__ == '__main__':
app = QApplication([])
window = MainWindow()
window.show()
sys.exit(app.exec())
The PySide6 library provides Python bindings for the Qt framework. What is Qt? For the purposes of this discussion, Qt is a framework for developing professional and high-quality graphical user interfaces. Entirely with Python. Below is a very simple script that opens an empty window and nothing more.
import sys
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtCore import QSize
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Hello, World!")
self.setMinimumSize(QSize(480, 320))
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
From here, the window class can be extended to include buttons, text boxes, and other widgets. Qt provides off-the-shelf widgets for much common use. These widgets may be subclassed further customizing their appearance or behaviour. Actually advanced users may even create entirely new widgets from the ground up. The possibilities are endless.
Before we get carried away, we need to keep one very important quirk in
mind. Qt provides a vast array of classes that all inherit from the
QObject
class. This class has an odd, but very unforgiving requirement.
No instances of QObject
may be instantiated without a running
QCoreApplication. This immediately presents a problem to our otherwise
elegant descriptor protocol: We are not permitted to instantiate
instances before the main script runs. Such as during class creation. For
this reason, the AttriBox
class was created to implement lazy
instantiation! Let us now see how we might create a more advanced
graphical user interface whilst adhering to the QObject
requirement.
The AttriBox
class - Lazy instantiation
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel
from PySide6.QtCore import QSize
from worktoy.desc import AttriBox, THIS
class MainWindow(QMainWindow):
"""Subclass of QMainWindow. This class provides the main window for the
application. """
baseWidget = AttriBox[QWidget](THIS)
verticalLayout = AttriBox[QVBoxLayout]()
welcomeLabel = AttriBox[QLabel]()
def show(self) -> None:
"""Before invoking the parent method, we will setup the window. """
self.setMinimumSize(QSize(480, 320))
self.setWindowTitle("WorkToy!")
self.welcomeLabel.setText("""Welcome to AttriBox!""")
self.verticalLayout.addWidget(self.welcomeLabel)
self.baseWidget.setLayout(self.verticalLayout)
self.setCentralWidget(self.baseWidget)
QMainWindow.show(self)
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
window.show()
app.exec()
The above script makes use of the lazy instantiation provided by the
AttriBox
class. While some readers may have recognized the
similarities between Field
and property
, many readers are presently
picking jaws up from the floor, pinching themselves or seeking spiritual
guidance. The AttriBox
not only implements an enhanced version of the
descriptor protocol, but it does so on a single line, where it even
provides syntactic sugar for defining the class intended for lazy
instantiation. Let us examine AttriBox
in more detail.
The AttriBox
class
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel
from PySide6.QtCore import QSize
from worktoy.desc import AttriBox, THIS
class MainWindow(QMainWindow):
"""Subclass of QMainWindow. This class provides the main window for the
application. """
baseWidget = AttriBox[QWidget](THIS)
# The above line creates a descriptor at name 'baseWidget' that will
# instantiate a QWidget instance. When the __get__ on the descriptor
# tries to retrieve the value it owns, only then will the value be
# instantiated. When instantiating the value, the arguments in the
# parentheses are passed to the constructor of the class. That brings
# us to the 'THIS' token. When instantiating the value, the 'THIS' token
# is replaced with the instance of the owning class. This is convenient
# for the 'baseWidget' attribute, as it allows the instance created to
# set its parent to the owning instance.
The use case pertaining to the PySide6 library makes great use of the
lazy instantiation. In fact, the motivation that led to the creation of
the AttriBox
class was this need for lazy instantiation.
worktoy.desc.AttriBox
- Advanced Instantiation
PENDING... WorkToy v1 will not release until this documentation is complete.
worktoy.meta - Understanding the Python metaclass
Readers may associate the word meta with crime on account of the hype created around the term metaverse. This author hopes readers will come to associate the word instead with the Python metaclass. The 'worktoy.meta' module provides functions and classes allowing a more streamlined approach to metaclass programming. This documentation explains the functionality of metaclasses in general and how this module provides helpful tools.
Everything is an object!
Python operates on one fundamental idea: Everything is an object.
Everything. All numbers, all strings, all functions, all modules and
everything that you can reference. Even object
itself is an object.
This means that everything supports a core set of attributes and methods
defined on the core object
type.
Extensions of object
With everything being an object, it is necessary to extend the
functionalities in the core object
type to create new types,
hereinafter classes. This allows objects to share the base object
,
while having additional functionalities depending on their class. Python
provides a number of special classes listed below:
object
- The base class for all classes. This class provides the most basic functionalities.int
- Extension for integers. The python interpreter uses heavily optimized C code to handle integers. This is the case for several classes on this list.float
- Extension for floating point numbers. This class provides a number of methods for manipulating floating point numbers.list
- Extension for lists of objects of dynamic size allowing members to be of any type. As the amount of data increases, the greater the performance penalty for the significant convenience.tuple
- Extension for tuples of objects of fixed size. This class is similar to the list class, but the size is fixed. This means that the tuple is immutable. While this is inflexible, it does allow instances to be used as keys in mappings.dict
- Extension for mappings. Objects of this class map keys to values. Keys be of a hashable type, meaning thatobject
itself is not sufficient. The hashables on this list are:int
,float
,str
andtuple
.set
- Extension for sets of objects. This class provides a number of methods for manipulating sets. The set class is optimized for membership testing.frozenset
- Provides an immutable version ofset
allowing it to be used as a key in mappings.str
- Extension for strings. This class provides a number of methods for manipulating strings. Theworktoy.text
module expands upon some of these.
To reiterate, everything is an object. Each object belongs to the
object
class but may additionally belong to a class that extends the
object
class. For example: 7
is an object. It is an instance of
object
by being an instance of int
which extends object
.
Classes are responsible for defining the instantiation of instances
belonging to them. Generally speaking, classes may be instantiated by
calling the class object treating it like a function. Classes may accept
or even require arguments when instantiated.
Before proceeding, we need to talk about functions. Python provides two
builtin extensions of object
that provide standalone objects that
implement functions: function
and lambda
. Both of these have
quite unique instantiation syntax and does not follow the conventions we
shall see later in this discussion.
Defining a function
Python allows the following syntax for creating a function. Please note
that all functions are still objects, and all functions created with the
syntax below belong to the same class function
. Unfortunately, this
class cannot be referred to directly. Which is super weird. Anyway, to
create a function, use the following syntax:
def multiplication(a: int, b: int) -> int:
"""This function returns the product of two integers."""
return a * b
RANT
The above function implements multiplication. It also provides the optional features: type hints and a docstring. The interpreter completely ignores these, but they are very helpful for humans. It is the opinion of this author that omitting type hints and docstrings is acceptable only when running a quick test. If anyone except you or God will ever read your code, it must have type hints and docstrings!
END OF RANT
Below is the syntax that invokes the function:
result = multiplication(7, 8) # result is 56
In the function definition, the positional arguments were named a
and
b
. In the above invocation, the positional arguments were given
directly. Alternatively, they might have been given as keyword arguments:
result = multiplication(a=7, b=8) # result is 56
tluser = multiplication(b=8, a=7) # result is 56
When keyword arguments are used instead of positional arguments, the order is irrelevant, but names are required.
The star *
and double star **
operators
Suppose the function were to be invoked with the numbers from a
list: numbers = [7, 8]
, then we might invoke the multiplication
function as follows:
result = multiplication(numbers[0], numbers[1]) # result is 56
Imagine the function took more than two arguments. The above syntax would
still work, but would be cumbersome. Enter the star *
operator:
result = multiplication(*numbers) # result is 56
Wherever multiple positional arguments are expected, and we have a list or a tuple, the star operator unpacks it. This syntax will seem confusing, but it is very powerful and is used extensively in Python. It is also orders of magnitude more readable than the equivalent in C++ or Java.
RANT
This rant is left as an exercise to the reader
END OF RANT
Besides function calls, the star operator conveniently concatenates lists
and tuples. Suppose we have two lists: a = [1, 2]
and b = [3, 4]
we may concatenate them in several ways:
a = [1, 2]
b = [3, 4]
ab = [a[0], a[1], b[0], b[1]] # Method 1: ab is [1, 2, 3, 4]
ab = a + b # Method 2: ab is [1, 2, 3, 4]
ab = [*a, *b] # Method 3: ab is [1, 2, 3, 4]
a.extend(b) # Method 4 modifies list 'a' in place.
a = [1, 2, 3, 4] # a is extended by b
Obviously, don't use the first method. The one relevant for the present discussion is the third, but the second and fourth have merit as well, but will not be used here. Finally, list comprehension is quite powerful as well but is the subject for a different discussion.
The double star **
operator
The single star is to lists and tuples as the double star is to
dictionaries. Suppose we have a dictionary: data = {'a': 1, 'b': 2}
then we may invoke the multiplication
function as follows:
data = {'a': 1, 'b': 2}
result = multiplication(**data) # result is 2
Like the star operator, the double star operator can be used to
concatenate two dictionaries. Suppose we have two dictionaries:
A = {'a': 1, 'b': 2}
and B = {'c': 3, 'd': 4}
. These may be
combined in several ways:
A = {'a': 1, 'b': 2}
B = {'c': 3, 'd': 4}
# Method 1
AB = {**A, **B} # AB is {'a': 1, 'b': 2, 'c': 3, 'd': 4}
# Method 2
AB = A | B
# Method 3 updates A in place
A |= B
A = {'a': 1, 'b': 2} # Resetting A
# Method 4 updates A in place
A.update(B)
As before, the first method is the most relevant for the present discussion. Unlike the example with lists, there is not really a method that is bad like the first method with lists.
In conclusion, the single and double star operators provide powerful unpacking of iterables and mappings respectively. Each have reasonable alternatives, but it is the opinion of this author that the star operators are preferred as they are unique to this use. The plus and pipe operators are used for addition and bitwise OR respectively. When the user first sees the plus or the pipe, they cannot immediately infer that the code is unpacking the operands. Not before having identified the types of the operands. In contrast, the star in front of an object without space immediately says unpacking.
RANT
If you have ever had the misfortune of working with C++ or Java, you would know that the syntax were disgusting, but you didn't know the words for it. The functionalities coded by C++ and Java cannot be inferred easily. It is necessary to see multiple parts of the code to infer what functionality is intended. For example, suppose we have a C++ class with a constructor.
class SomeClass {
private:
int _a;
int _b;
public:
int a;
SomeClass(int a, int b) {
// Constructor code
}
int b {
return _b;
}
};
Find the constructor above. It does not have a name that means "Hello there, I am a constructor". Instead, it is named the same as the class itself. So to find the constructor, you need to identify the class name first then go through the class to find name again. The decision for this naming makes sense in that it creates something with the name called. But it significantly reduces readability. The second attack on human dignity is the syntax for the function definition. Where the class defines the public variable 'a', the syntax used is not bad. But because the syntax is identical for the functions, it increases the amount of code required to infer that a function is being created.
The two examples of nauseating syntax above do not serve any performance related purpose. Software engineering and development requires the full cognitive capability of the human brain. Deliberately obscuring code, reduces the cognitive capacity left over for actual problem-solving. This syntax is kept in place for no other purpose than gate-keeping.
END OF RANT
The famous function signature: def someFunc(*args, **kwargs)
Anyone having browsed through Python documentation or code may have
marvelled at the function signature: def someFunc(*args, **kwargs)
.
The signature means that the function accepts any number of positional
arguments as well as any number of keyword arguments. This allows one
function to accept multiple different argument signatures. While this may
be convenient, the ubiquitous use of this pattern is likely motivated by
the absense of function overloading in native Python. (Foreshadowing...)
The lambda
function
Before getting back to class instantiation, we will round off this
discussion of functions with the lambda
function. The lambda
function is basically the anonymous function. The syntax of it is
lambda arguments: expression
. Whatever the expression on the right
hand side of the colon evaluates to is returned by the function. The
lambda
function allows inline function definition which is much more
condensed that the regular function definition as defined above. This
allows it to solve certain problems in one line, for example:
fb = lambda n: ('' if n % 3 else 'Fizz') + ('' if n % 5 else 'Buzz') or n
Besides flexing, the lambda
function is useful when working with
certain fields of mathematics, requiring implementation of many functions
that fit on one line. Below is an example of a series of functions
implementing Taylor series expansions. This takes advantage of the fact
that many such functions may be distinguished only by a factor mapped
from the term in the series.
factorial = lambda n: factorial(n - 1) * n if n else 1
recursiveSum = lambda F, n: F(n) + (recursiveSum(F, n - 1) if n else 0)
taylorTerm = lambda x, t: (lambda n: t(n) * x ** n / factorial(n))
expTerm = lambda n: 1
sinTerm = lambda n: (-1 if ((n - 1) % 4) else 1) if n % 2 else 0
cosTerm = lambda n: sinTerm(n + 1)
sinhTerm = lambda n: 1 if n % 2 else 0
coshTerm = lambda n: sinhTerm(n + 1)
exp = lambda x, n: recursiveSum(taylorTerm(x, expTerm), n)
sin = lambda x, n: recursiveSum(taylorTerm(x, sinTerm), n)
cos = lambda x, n: recursiveSum(taylorTerm(x, cosTerm), n)
sinh = lambda x, n: recursiveSum(taylorTerm(x, sinhTerm), n)
cosh = lambda x, n: recursiveSum(taylorTerm(x, coshTerm), n)
The above collection of functions implement recursive lambda functions to calculate function values of common mathematical functions including:
exp
: The exponential function.sin
: The sine function.cos
: The cosine function.sinh
: The hyperbolic sine function.cosh
: The hyperbolic cosine function.
The lambda functions implement Taylor-Maclaurin series expansions at a given number of terms and then begin by calculating the last term adding the previous term to it recursively, until the 0th term is reached. This implementation demonstrates the power of the recursive lambda function and is not at all flexing.
Instantiation of classes
Since this discussion includes class instantiations, the previous section discussing functions will be quite relevant. We left the discussion of builtin Python classes having listed common ones. Generally speaking, Python classes have a general syntax for instantiation except for those listed. Below is the instantiation of the builtin classes.
- object:
obj = object()
- This creates an object. Not particularly useful but does show the general syntax. - int:
number = 69
- This creates an integer. - float:
number = 420.0
- This creates a float. - str:
message = 'Hello World!'
- This creates a string. - list:
data = [1, 2, 3]
- This creates a list. - tuple:
data = (1, 2, 3)
- This creates a tuple. - ?:
what = (1337)
- What does this create? Well, you might imagine that this creates a tuple, but it does not. The interpreter first removes the redundant parentheses and then the evaluation makes it an integer. To create a single element tuple, you must add the trailing comma:what = (1337,)
. This applies to one element tuples, as the comma separating the elements of a multi-element tuple sufficiently informs the interpreter that this is a tuple. The empty tuple requires no commas:empty = ()
. - set:
data = {1, 2, 3}
- This creates a set. - dict:
data = {'key': 'value'}
- This creates a dictionary. If the keys are strings, the general syntax may be of greater convenience:data = dict(key='value')
. Not requiring quotes around the keys. Although this syntax does not support non-string keys. - ?:
data = {}
- What does this create? Does it create an empty set or an empty dictionary. This author is not actually aware, and recommends insteadset()
ordict()
respectively when creating empty sets or dictionaries.
Except for list
and tuple
, the general class instantiation syntax
may be applied as seen below:
- int:
number = int(69)
- float:
number = float(420.0)
- str:
message = str('Hello World!')
- dict:
data = dict(key='value')
- This syntax is quite reasonable, but is limited to keys of string type.
Now let's have a look at what happens if we try to instantiate tuple
,
list
, set
or frozenset
using the general syntax:
- list:
data = list(1, 2, 3)
- NOPE! This does not create the list predicted by common sense:data = [1, 2, 3]
. Instead, we are met by the following error message: "TypeError: list expected at most 1 argument, got 3". Instead, we must use the following syntax:data = list((1, 2, 3))
ordata = list([1, 2, 3])
. Now the attentive reader may begin to object, as one of the above require a list to already be defined and the other requires the tuple to be defined. Let's see how one might instantiate a tuple directly: - tuple:
data = tuple(1, 2, 3)
- NOPE! This does not work either! We receive the exact same error message as before. Instead, we must use one of the following:data = tuple((1, 2, 3))
ordata = tuple([1, 2, 3])
. The logically sensitive readers now see a significant inconsistency in the syntax: One cannot in fact instantiate a tuple nor a list directly without having a list or tuple already created. This author suggests that the following syntax should be accepted:data = smartTuple(1, 2, 3)
and even:data = smartList(1, 2, 3)
. Perhaps this author is just being pedantic. The existing syntax is not a problem, and it's not like the suggested instantiation syntax is used anywhere else in Python. - set:
data = set(1, 2, 3,)
This is correct syntax. So this works, but the suggestedsmartList
andsmartTuple
functions does not, OK sure, makes sense... - frozenset:
data = frozenset([69, 420])
- This is correct syntax.
Let us have another look at the instantiations of dict
and of set
,
but not list
and tuple
.
def newDict(**kwargs) -> dict:
"""This function creates a new dictionary having the key value pairs
given by the keyword arguments. """
return dict(**kwargs) # Unpacking the keyword arguments creates the dict.
def newSet(*args) -> set:
"""This function creates a new set having the elements given by the
positional arguments. """
return set(args) # Unpacking the positional arguments creates the set.
def newList(*args) -> list:
"""As long as we don't use the word 'list', we can actually instantiate
a list in a reasonable way."""
return [*args, ] # Unpacking the positional arguments creates the list.
def newTuple(*args) -> tuple:
"""Same as for list, but remember the hanging comma!"""
return (*args,) # Unpacking the positional arguments creates the tuple.
Custom classes
In the previous section, we examined functions and builtin classes. To
reiterate, in the context of this discussion a class is an extension of
object
allowing objects to belong to different classes implementing
different extensions of object
. This raises a question: What
extension of object
contains object
extensions? If 7
is an
instance of the int
extension of object
, of what extension is
int
and instance. The answer is the type
. This extension of
object
provides all extensions of object
. This implies the
surprising that type
is an instance of itself.
The introduction of the type
class allows us to make the following
insightful statement:
7
is to int
as int
is to type
. This means that type
is responsible for instantiating new classes. A few readers may now begin
to see where this is going, but before we get there, let us examine how
type
creates a new class. In the example below, we create a simple
class and we will examine the exact steps that the type
class takes
during class creation.
from worktoy.desc import AttriBox
class PlanePoint:
"""Class representing a point in the plane """
x = AttriBox[float]()
y = AttriBox[float]()
def __init__(self, *args) -> None:
"""Constructor for the PlanePoint class. """
floatArgs = [float(arg) for arg in args if isinstance(arg, (int, float))]
self.x, self.y = [*floatArgs, 0.0, 0.0][:2]
def __abs__(self, ) -> float:
"""Returns the distance from the origin. """
return (self.x ** 2 + self.y ** 2) ** 0.5
if __name__ == '__main__':
P = PlanePoint(69, 420)
When the Python interpreter encounters the line beginning with the
reserved keyword class
, it creates a new class object, it begins a
new lexical scope and the contents parentheses after the name determine
what happens next. If no such parentheses are present, the default
behaviour is to create a new instance of type
, meaning an extension
of object
. This starts the following process:
- namespace:
type
creates a namespace object that the interpreter will use to build the new class. This object is simply an empty instance ofdict
. - Class Body Execution: The interpreter goes through the class body line by line from top to bottom. When encountering an assignment, it updates the namespace accordingly.
- Class Object Creation: The interpreter passes the namespace object
back to
type
that creates the new class object. This happens when the__new__
method oftype
returns. - Descriptor Class Notification: All
__set_name__
methods on objects owned by the class are notified, receiving the class object as the first argument and the name by which the object is assigned to the owning class. In the above example, theAttriBox
objects are notified:PlanePoint.x.__set_name__(PlanePoint, 'x')
andPlanePoint.y.__set_name__(PlanePoint, 'y')
. type.__init__
: This is the final step before thetype
is complete and the class object is returned. Please note that the interpreter uses highly optimized C code during this whole procedure, and thetype.__init__
has no C code implementation making it a noop.- Class Instantiation: Once the class object is created, instances of
the class may now be created. This begins with a call the
__call__
method on the class object. This method is defined bytype
. If the class itself defines__call__
, that method is invoked only when an instance of the class is called. type.__call__(cls, *args, **kwargs)
: This call on thetype
object creates the new instance of the class.cls.__new__(cls, *args, **kwargs)
: This method is responsible for creating the new instance of the class. Please note that it makes use of the__new__
method defined on the class object. This means that new classes are able to customize how new instances are created, however implementing the__init__
method defined below is more common and quite sufficient for most purposes.cls.__init__(self, *args, **kwargs)
: When the new instance is created, it is passed to the__init__
method on the class. When coding custom classes implementing the__init__
method is the most convenient way to define how new instances are initialized.
What is a metaclass?
In the previous section, we examined how type
creates a new class.
What exactly is type
though? type
is an object
, but is also
an extension of object
whose instances themselves are extensions of
object
. But what if we extended type
? We can do that because
type
itself extends object
. This is what a metaclass is. An
extension of the type
extension of object
.
Each of the steps in the class creation process described above may be
customized by extending the type
class. Below is a list of the
methods defined on type
that a custom class may override:
__prepare__
: In the previous section when the namespace object was created, this is done by the__prepare__
method on the metaclass. Thetype
implementation of__prepare__
returns an empty dictionary. A metaclass can change this by prepopulating the items in this dictionary or even return a custom namespace object.__new__
: This method is responsible for creating the object that will be created at the name after the class definition. This is where a new class is conventionally created, but this is by no means a requirement for a custom metaclass. It is possible to implement a metaclass that creates some other object than a new class.__init__
: After the metaclass has created the new class object, or whatever object is created, it is passed to the__init__
method. Please note that this method is called after the__set_name__
has been applied to the objects implementing the descriptor protocol. When this method returns the new class object is created.
After the metaclass has created the new class and has returned the
__init__
, the metaclass is still called by the class object under
certain circumstances. Below is a list of methods that on the metaclass
that may be called during class lifetime:
__call__
: When an instance of the class is called, the__call__
method on the metaclass is invoked. By default, the__new__
on the created class object is called, and the object returned is passed to the__init__
method on the class object. The metaclass may override this behaviour.__instance_check__
: When theisinstance
function is called, the metaclass is called with create class and the instance. Thus, the metaclass may specify how classes derived from it determine if an instance is an instance of it. For example, a custom metaclass creating Numerical classes might recognize instances offloat
orint
as their own.__subclass_check__
: This is the method called whenissubclass
is called on a class object derived from the metaclass. It allows the metaclass to customize what classes it regards as subclasses. Similar to the__instance_check__
.- **
__str__
: When printing a class object, the resulting text is frequently more confusing than helpful. I defined a class namedTestClass
in the main script and printed it. The output was:<class '__main__.TestClass'>
. But suppose we used a custom metaclassMetaType
and derived from it a class calledTestClass
, then the default output would be:<class '[MODULE].TestClass'>
, but it will not make reference to the metaclass. Instead, let us have the metaclass improve this output:[MODULE].TestClass(MetaType)
. __iter__
and__next__
: Conventionally, iterating over an object happens on the instance level, and only by implementing the iteration protocol on the metaclass level can the class object itself become iterable.__getitem__
: This method allows a metaclass to define handling ofcls[key]
. Please note that as of Python version 3.9, Python classes may implement a method called__class_getitem__
, which is intended for the same use. In case both the metaclass and the class itself implement these classes respectively, the metaclass implementation is used and the class version is ignored.__setitem__
: This method allows a metaclass to define handling ofcls[key] = value
.
Above is a non-exhaustive list of type
methods that a custom metaclass
may override. Before proceeding, we must discuss the role of the
namespace object. A significant aspect of the custom metaclass is the
ability to provide a custom namespace object.
Custom Namespace
Going back to the class creation procedure, the interpreter requests a
namespace object from the metaclass. A custom metaclass may reimplement
the __prepare__
method responsible for creating the custom namespace
and have it return an instance of a custom namespace class. Doing so
places a few subtle requirements on this class.
Preservation of KeyError
When a dictionary is accessed with a key that does not exist, a
KeyError
is raised. The interpreter relies on this behaviour to
handle lines in the class body that are not directly assignments
correctly. This is a particularly important requirement because failing
to raise the expected KeyError
will affect only classes that happen
to include a non-assignment line. Below is a list of known situations
that causes the issue:
- Decorators: Unless the decorator is a function defined earlier in
the class body as an instance method able to receive a callable at the
self
argument, the decorator will cause the issue described. Please note that a static method would be able to receive a callable at the first position, but the static method decorator itself would cause the issue even sooner. - Function calls: If a function not defined previously in the class body is called during the class body without being assigned to a name, the error will occur.
The issue raises an error message that will not bring attention to the namespace object. Further, classes will frequently work fine, if they happen to not include any of the above non-assignments. In summary: failing to raise the expected error must be avoided at all costs, as it will cause undefined behaviour without any indication as to the to cause.
Subclass of dict
After the class body is executed the namespace object is passed to the
__new__
method on the metaclass. If the metaclass is intended to
create a new class object, the metaclass must eventually call the
__new__
method on the parent type
class. The type.__new__
method must receive a namespace object that is a subclass of dict
. It
is only at this stage the requirement is enforced. Thus, it is possible
to use a custom namespace object that is not a subclass of dict
, but
then it is necessary to implement functionality in the __new__
method
on the metaclass such that a dict
is passed to the type.__new__
call.
Required functionalities
Please note that this section pertains only to functionalities whose absense will cause the interpreter to raise an exception. The functionalities described here cannot be said to be sufficient for any degree of functionality. A custom namespace class must additionally implement whatever functionality is required for its intended purpose.
__getitem__
: This method must implement this method such that if a normal dictionary would raise an error on receiving a key, then that error must still be raised. Please note, that the interpreter handles this exception silently. Other than this situation, the__getitem__
method are otherwise free to do anything it wants.__setitem__
: The namespace object must return a callable object at name__setitem__
, that accepts three positional arguments: the namespace instance at theself
argument, as well as thekey
and thevalue
. As long as a callable is at the name, and it does not raise an error upon receiving three arguments, the namespace object can do whatever it wants. It does not even have to remember anything.
Potential functionalities
This section describes functionalities that is certain to preserve all
information received in the class body. This is an enhancement compared
to the default namespace object. It permits the __new__
in the
metaclass access to all information encountered in the class body. As
long as this is satisfied, there is little additional functionality the
namespace object may provide to the __new__
method. Nevertheless,
readers are encouraged to experiment with custom namespace classes beyond
this.
Custom Metaclass Requirements
This section illustrates the immense flexibility of the custom metaclass, by just how little is actually required for the interpreter to go through the class creation process without raising an exception. This author has found only two requirements for the custom metaclass:
- Callable: The object used as
metaclass
must be callable. - Accept three positional arguments: As well as being callable, three positional arguments are passed to it. As long as doing so does not raise an exception, the metaclass is free to do whatever it wants.
And that is all that is required. The metaclass is typically a subclass
of type
, but is not required. Conventionally, some kind of class
object is created, but is not required. The metaclass is not even
required to return anything in which case, the None
object will
appear at the given class name. Thus, the custom metaclass can be used to
create new classes, but in reality it can be used to create anything. It
could be used to replace functions defined with the def
keyword.
Readers are encouraged to dream up new uses for the custom metaclass.
The remainder of this documentation focus on the worktoy.meta
module
and the classes and functions defined therein. These focus on the more
conventional applications of the custom metaclass, that is, creation of
classes having functionalities beyond the default Python classes.
The worktoy.meta
module
Nomenclature
Before proceeding, let us define terms:
cls
- A newly created class objectself
- A newly created object that is an instance of the newly created class.mcls
- The metaclass creating the new class.namespace
- This is where the class body is stored during class creation.
AbstractMetaclass
and AbstractNamespace
These abstract baseclass illustrates an elegant metaclass pattern. The
namespace class records every assignment in the class body, even if a
name is assigned a value for the second time. The namespace class also
implements a method called compile
which returns a regular dictionary
with the items the metaclass should pass on to the type.__new__
method. Without further subclassing, instances of this namespace class,
will provide behaviour indistinguishable from the default behaviour.
The abstract metaclass implements the __prepare__
class method which
returns creates an instance of the abstract namespace class defined above.
In the __new__
method the metaclass retrieves the final namespace
dictionary by calling the compile
method on the namespace object.
With this it calls the type.__new__
method with the thus obtained
namespace object. The class object returned from the call to
type.__new__
is returned by the abstract metaclass.
This pair of abstract classes provides a solid pair of baseclasses. The
pattern is convenient, the metaclass instantiates the namespace object.
Without loss of information, the namespace object is returned to the
metaclass. Here the metaclass obtains the final namespace dictionary from
the compile
method on the namespace object.
Singleton
The singleton term is well understood as a class having only one
instance. Typically, such a class is callable, but instead of creating a
new instance, the same singleton instance is returned. However, when the
singleton class is called the singleton instance will have its
__init__
repeated with whatever arguments are passed. This allows the
singleton instance to update itself. If this is undesirable, the
singleton class should itself prevent its __init__
method from
updating values intended to immutable.
worktoy.meta
provides a metaclass called SingletonMeta
and a
derived class called Singleton
. Custom singleton classes may either
set SingletonMeta
as the metaclass or subclass Singleton
. The
metaclass subclasses the BaseMetaclass
discussed below.
Zeroton
The Zeroton
class is a novelty. What specified the singleton class is
the fact that it has only one instance. The Zeroton
class in contrast
has not even one instance. Such a class is essentially a token. The
purpose of it is to retain itself across multiple modules. Presently, it
finds use in the AttriBox
implementation of the worktoy.desc
module. For more information, readers are referred to the section on
"Advanced Instantiation" in the worktoy.desc
documentation.
Function overloading in Python
When creating a new class using the default type
, only the most
recent assigned value at each name is retained. As such, implementing
overloading of methods in the class body requires a custom metaclass
providing a custom namespace. The worktoy.meta
module provides the
BaseObject
class derived from the BaseMetaclass
, which implements
function overloading of the methods in the class body. Before
demonstrating the syntactic use of the BaseObject
class, an
explanation of the implementation is provided. To skip directly to the
usage, see section worktoy.meta.overload - Usage.
Background
The main issue with function overloading is that multiple callables are now present on the same name. Thus, a new step is required to determine which available implementation to invoke, given the arguments received. The procedure chosen here is to contain the pairs of type signatures and callables in a dictionary. When the overloaded function is called, the type signature of the arguments is determined and used to look up the appropriate callable in the dictionary, before invoking it with the argument values. This step does add some overhead, but in testing has not exceeded 20 %.
Type Decoration
When a class body is to define multiple callables at the same name, but
with different functions, the worktoy.meta.overload
decorator factory
is used. When calling it with types as positional arguments, it returns a
decorator. When this decorator is called it sets the type signature of
the decorated function at the attribute named
__overloaded_signature__
to the type signature given the factory.
Function Dispatcher
The BaseNamespace
uses a dedicated class called Dispatcher
to
encapsulates the type signature to callable mapping. The Dispatcher
class implements both the descriptor protocol and the __call__
method
allowing it to emulate the behaviour of bounded and unbounded methods as
appropriate. When called it determines the type signature of the
arguments received, resolve the matching callable, invokes it with the
arguments received and returns the return value.
Namespace Compilation
The BaseMetaclass
implements the pattern described previously, where
the namespace class provides functionality for creating a dictionary to
be used in the type.__new__
method. This compile
method retrieves
the callables encountered during class body execution that were decorated
with by the overload decorator and for each name creates a Dispatcher
instance as described above, which is placed in the final dictionary at
the appropriate name.
worktoy.meta.overload
- Usage
To make use of the 'overload' functionality in a class definition, import
the overload
decorator factory and the BaseObject
class from the
worktoy.meta.overload
module. The BaseObject
class already uses the
BaseMetaclass
as the metaclass, provides replacements for
__init__
and __init_subclass__
which do not raise exceptions
every time they see an argument, in contrast to object.__init__
and
object.__init_subclass__
:
from __future__ import annotations
from worktoy.meta import BaseMetaclass
class BaseObject(metaclass=BaseMetaclass):
"""BaseObject provides argument-tolerant implementations of __init__ and
__init_subclass__ preventing the errors explained in the documentation."""
def __init__(self, *args, **kwargs) -> None:
"""Why are we still here?"""
def __init_subclass__(cls, *args, **kwargs) -> None:
"""Just to suffer?"""
Implement the custom class as a subclass of BaseObject
. In the
function body, provide implementations of the overloaded functions by
reusing the name of the function. Decorate each such function with the
overload
decorator factory and provide the type signatures of the
as positional arguments. For example:
from worktoy.meta import overload, BaseObject
from worktoy.desc import AttriBox
from typing import Self
class ComplexNumber(BaseObject):
"""Class representing complex numbers. """
__fallback_value__ = 0j
realPart = AttriBox[float]()
imagPart = AttriBox[float]()
@overload(int, int)
def __init__(self, a: int, b: int) -> None:
self.__init__(float(a), float(b))
@overload(float, float)
def __init__(self, a: float, b: float) -> None:
self.realPart, self.imagPart = a, b
@overload(complex)
def __init__(self, z: complex) -> None:
self.realPart, self.imagPart = z.real, z.imag
@overload()
def __init__(self, ) -> None:
self.__init__(self.__fallback_value__)
def __abs__(self) -> float:
return (self.realPart ** 2 + self.imagPart ** 2) ** 0.5
def __sub__(self, other: Self) -> Self:
return ComplexNumber(self.realPart - other.realPart,
self.imagPart - other.imagPart)
def __add__(self, other: Self) -> Self:
return ComplexNumber(self.realPart + other.realPart,
self.imagPart + other.imagPart)
def __eq__(self, other: Self) -> bool:
return False if abs(self - other) > 1e-08 else True
if __name__ == '__main__':
z1 = ComplexNumber(69, 420)
z2 = ComplexNumber(69.0, 420.0)
z3 = ComplexNumber(69 + 420j)
print(z1 == z2 == z3)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for worktoy-0.99.32-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 10b444f7c74a830e72a935ed3b20687e67e54131bc93265224e9c10cccaf40e9 |
|
MD5 | 76e475e4496911a4fab02350583cdeb8 |
|
BLAKE2b-256 | eac529cb76a6b2c00dd5e2bdcad985d2cb640b316d3cd34bacef413a618ccd12 |