diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 6b4822d..ce0c90c 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -12,7 +12,7 @@ permissions: id-token: write concurrency: - group: "pages" + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -58,12 +58,25 @@ jobs: name: jupyterlite path: _output/* + build-slides: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Make slides + run: npx @marp-team/marp-cli@latest --input-dir slides --output _output + + - uses: actions/upload-artifact@v3 + with: + name: slides + path: _output/* + deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest - needs: [build-book, build-pyodide] + needs: [build-book, build-pyodide, build-slides] if: github.event_name == 'push' steps: - name: Setup Pages @@ -79,6 +92,11 @@ jobs: name: jupyterlite path: public/live + - uses: actions/download-artifact@v3 + with: + name: slides + path: public/slides + - name: Upload artifact uses: actions/upload-pages-artifact@v1 with: diff --git a/.gitignore b/.gitignore index f3c67fc..1b67176 100644 --- a/.gitignore +++ b/.gitignore @@ -175,3 +175,7 @@ cython_debug/ /content/week12/0[123]*/*.hpp /content/week12/0[123]*/CMakeLists.txt /content/week12/0[123]*/pyproject.toml + +/slides/*.html + +.vscode diff --git a/.prettierrc.toml b/.prettierrc.toml index c5a11fc..7f00d77 100644 --- a/.prettierrc.toml +++ b/.prettierrc.toml @@ -1,2 +1,6 @@ proseWrap = "always" printWidth = 80 + +[[overrides]] +files = "slides/*.md" +options.proseWrap = "never" diff --git a/content/week06/introoo.md b/content/week06/introoo.md index f173241..c6e127b 100644 --- a/content/week06/introoo.md +++ b/content/week06/introoo.md @@ -310,7 +310,7 @@ another. Remember, inheritance indicates an "is a" relationship. Subclasses can specialize, but if you are overriding every method of a superclass with distinct implementations you aren't really inheriting anything. -### Multiple Inheritance +### Multiple inheritance If one parent is good, why not allow more? Some languages allow you to combine multiple classes into one child - this is called multiple inheritance. It is @@ -328,6 +328,87 @@ mechanisms in super that kick in if you have multiple parents. In short, always check the `__mro__`; that's always linear and super will always go up the `__mro__`. +### Abstract base classes and interfaces + +When designing with inheritance in mind, you might want to require a method be +implemented in all subclasses. For example, if you had a Vector class with +Vector2D and Vector3D subclasses, you might want to require all subclasses +define `mag2` (the squared magnitude). You can do this with the `abc` module: + +```python +import abc + + +class Vector(abc.ABC): + @abc.abstractmethod + def mag2(self): + pass + + def mag(self): + return self.mag2() ** 0.5 +``` + +The `abc.ABC`` class is a convenience class; you can also use `class +Vector(metaclass=abc.ABCMeta)`instead to avoid inheritance on this convenience class. We won't be discussing metaclasses, so just briefly they customize _class_ creation rather than _instance_ creation (everything is an object, even classes!). ABCs inject checks to all the child classes so that when you create an instance, they see if any abstract components are missing from the class. If you never make an instance, you can have abstract methods. Above,`Vector` +is called an abstract class, since you can't make instances of it. However, you +can make a concrete class from it: + +```python +@dataclasses.dataclass +class Vector2D(Vector): + x: float + y: float + + def mag2(self): + return self.x**2 + self.y**2 +``` + +Since we have provided concrete definitions for all abstract methods in +`Vector2D`, we can instantiated it at use it: + +```python +assert Vector2D(3, 4).mag() == 5 +``` + +Notice that we can provide concrete methods in an abstract class, and we can +even provide helper code in the abstract methods that can be accessed via +`super()`. The only rule is no abstract methods can be exposed directly in a +class that gets instantiated. + +Notice what this means for a user. If a user knows they have a `Vector`, they +can now use `.mag()` and `.mag2()` without worrying _which_ Vector they have. We +call this an _Interface_. When we get to static typing, we will discuss a way to +formalize this in Python without ABCs (hint: it will be called `Protocol`s). +Python actually has dozens of Interfaces, many of which are in +`collections.abc`. For example, the `Sized` Interface is basically this: + +```python +class Sized(abc.ABC): + @abc.abstractmethod + def __len__(self): + pass +``` + +However, the implementation doesn't really matter for an Interface; you don't +have to inherit from an Interface to implement it. In fact, Python will even +report any instance of a class that defined `__len__` as +`isinstance(..., collections.abc.Sized)`, regardless of whether it actually +inherits from this ABC! This is called structural subtyping, and it solves one +of the big drawbacks we've been seeing with subclassing, the loss of modularity. + +Users of a Interface simply use ducktyping and access the methods that they +support. In the case of `Sized`, `len(x)` works on `Sized`, which just simply +calls `x.__len__()`. There are +[lots of other](https://docs.python.org/3/library/collections.abc.html), more +complex Interfaces, such as `Iterable` (for loops and such). Most of the ones in +Python use dunder names. This is because Python reserves all dunder names for +it's own use, but some libraries (especially large, older libraries!) do define +new ones, almost always for Interfaces. + +At this point, an ABC is well defined (we have seen how to make one in code), +but an Interface is a concept, an agreement between implementer and caller. We +fill formalize this later when we get to static typing with `Protocol`s. + ### Special methods We've already seen a special method in Python: `__init__`. You can customize diff --git a/content/week06/oodesign.md b/content/week06/oodesign.md index 5f176c0..cfa7449 100644 --- a/content/week06/oodesign.md +++ b/content/week06/oodesign.md @@ -36,7 +36,7 @@ Let's define some terminology we've been seeing, along with a bit of new stuff: under one roof. - **Encapsulation** (meaning 2): Isolating the implementation and only providing access to a minimal "public" part. -- **Object** or **instance** -- A variable holding data with a type holding +- **Object**/**instance**: A variable holding data with a type holding functions. - **Attribute**: Something accessible on an object. - **Member**: A data attribute stored in each object. @@ -62,6 +62,8 @@ Let's define some terminology we've been seeing, along with a bit of new stuff: - **Inheritance** and **Composition** can be used to keep code DRY. - Clean way to establish interfaces (via Abstract Base Classes (ABCs) or Protocols (see static typing)) +- Ensure required code is run (constructors, destructors, context managers, etc) +- Associate functions to a specific type (varies by language) ```{admonition} Interfaces Python refers to an Interface (Java terminology, technically) as a Protocol. C++20 @@ -73,7 +75,12 @@ We also will refer to "interface" meaning the interaction with your API by consumers (possibly also you). ``` -### Why Inheritance? +In languages without type based dispatch, classes are an ideal way to ensure +functions are associated with the type they were intended to be called on. Even +in languages with multiple dispatch, they still help with discoverability, such +as tab completion in editors. + +### Why inheritance? If you want two classes both to have a set of data or methods specified by the same code, you can put the code in class A and have class B "inherit" that code. @@ -81,7 +88,7 @@ We then say that A is the **parent class** or **super class** of B, and that B is the child class or subclass of A. - Provides the concept of "is a" (`isinstance` / `issubclass` in Python). -- Provides a way to "Realize" or "Implement" a specified interface (ABC or +- Provides a way to "realize" or "implement" a specified interface (ABC or Protocol). For example, in `content/week06/geom_example/geometry/classic.py`, `Shape` is a @@ -97,7 +104,7 @@ learn how to formalize this requirement, for now, we have to trust duck typing and willpower to avoid using anything that's not in `Shape` when we accept it as an argument. -### Why Composition/Aggregation? +### Why composition/aggregation? - Provides the concept of "has a", or "inherent part of". `Kidney` is a part of `Human`, for example. @@ -476,11 +483,11 @@ global, and you can even set the default value when you make a new instance! Classes allow you to organize code so that each each class addresses a specific concern. -Some languages (Ruby) support partial classes, which can load portions based on -what you are interested in doing, but Python and C++ do not. Type dispatch (C++, -Julia) can be used as an alternative. Python has mixins, covered below, which -are not quite the same as partial classes, but prvode similar benefits Ruby has -both partial classes and mixins but not multiple inheritance. +Some languages (Ruby, Rust) support partial classes, which can load portions +based on what you are interested in doing, but Python and C++ do not. Type +dispatch (C++, Julia) can be used as an alternative. Python has mixins, covered +below, which are not quite the same as partial classes, but provide similar +benefits. Ruby has both partial classes and mixins but not multiple inheritance. ### eDSLs diff --git a/slides/week-06-1.md b/slides/week-06-1.md new file mode 100644 index 0000000..f60bafd --- /dev/null +++ b/slides/week-06-1.md @@ -0,0 +1,520 @@ +--- +marp: true +theme: gaia +_class: lead +paginate: false +backgroundColor: #fff +backgroundImage: url('https://marp.app/assets/hero-background.svg') +--- + +# APC 524 + +## Object Oriented Introduction + +--- + +## Prelude + +To keep the slides simple, I won't be showing the following imports: + +```python +import abc +import dataclasses +import json +import os +``` + +I will be using the fully qualified names, though. + +--- + +## A toolbox + +We'll be providing lots of _ideas_ for design. Remember core concepts: + +- Modular +- DRY (Don't Repeat Yourself) +- Single responsibility +- Limited public API +- Readability + +Don't sacrifice too many of these just for another if you can help it! + +--- + +## Structured data + +Most languages provide a way to group data together. For example: + +```python +from types import SimpleNamespace + +vector = SimpleNamespace(x=1, y=2) +print(f"{vector.x=}, {vector.y=}") +``` + +```output +vector.x=1, vector.y=2 +``` + +--- + +## Structured data + functions + +```python +from types import SimpleNamespace + + +def make_path(string_location): + self = SimpleNamespace() + self.string_location = string_location + self.exists = lambda: os.path.exists(string_location) + return self + + +my_dir = make_path("my/dir") +print(f"{my_dir.string_location = }") +print(f"{my_dir.exists() = }") +``` + +--- + +## Structured data + functions + +```python +from types import SimpleNamespace + + +def make_path(string_location): # Construction function + self = SimpleNamespace() + self.string_location = string_location # Member + self.exists = lambda: os.path.exists(string_location) # Method + return self # Instance + + +my_dir = make_path("my/dir") +print(f"{my_dir.string_location = }") +print(f"{my_dir.exists() = }") +``` + +--- + +## Classes + +Every instance gets "printed out" by the constructor with same structure. We need to depend on that structure to use the objects. + +We can formalize this in most languages with classes. + +```python +class Vector2D: + def __init__(self, x, y): + self.x = x + self.y = y +``` + +--- + +## More boilerplate + +```python +class Vector2D: + __match_args__ = ("x", "y") + __hash__ = None + + def __init__(self, x, y): + self.x = x + self.y = y + + def __repr__(self): + return f"{self.__class__.__name__}(x={self.x!r}, y={self.y!r})" + + def __eq__(self, other): + if not isinstance(other, Vector): + return NotImplemented + return self.x == other.x and self.y == other.y +``` + +--- + +## Dataclasses (more like other languages, too) + +```python +@dataclasses.dataclass +class Vector2D: + x: float + y: float +``` + +Automatically generates the helper functions. See . + +Don't worry about the type annotations above, they are only needed to make the syntax work (unless they use `typing.ClassVar`). + +--- + +## Dataclasses options + +Can add options: + +- `frozen=True`: Make read-only (see `dataclasses.replace`) +- `order=True`: Define ordering (treats fields like tuple) +- (3.10+) `kw_only`: require keywords when setting +- (3.10+) `slots=True`: Generate `__slots__` + +Helpful tools in `dataclasses`: + +- `asdict` / `astuple`: convert any dataclass to dict/tuple +- `isdataclass`: see if something is a dataclass + +--- + +## Python example + +```python +class Path: + def __init__(self, string_location): + self.string_location = string_location + + def exists(self): + return os.path.exists(self.string_location) +``` + +--- + +## Dataclasses (Python) example + +```python +@dataclasses.dataclass +class Path: + string_location: str + + def exists(self): + return os.path.exists(self.string_location) +``` + +--- + +## C++ example + +```cpp +#include +#include + +class Path { + public: + std::string string_location; + + Path(std::string string_location) : string_location(string_location) {} + + bool exists() const { + const std::filesystem::path path_location{string_location}; + return std::filesystem::exists(path_location); + } +}; +``` + +--- + +## Subclassing + +```python +class Animal: + def eat(self, food): + print(f"{self.__class__.__name__} eating {food}") + + +class Dog(Animal): + pass + + +bluey = Dog() +bluey.eat("fruit") +``` + +```output +Dog eating fruit +``` + +--- + +## Subclassing: using super + +```python +class Raccoon(Animal): + def eat(self, food): + print("Washing first") + super().eat(food) + + +rascal = Raccoon() +rascal.eat("berries") +``` + +```output +Washing first +Raccoon eating berries +``` + +--- + +## Subclassing: details + +Subclasses are instances of the parents, too. + +```python +print(f"{isinstance(rascal, Raccoon)}") # True +print(f"{isinstance(rascal, Animal)}") # True +``` + +You can print the subclass chain: + +```python +print(f"{Raccoon.__mro__ = }") +``` + +```output +Raccoon.__mro__ = (, , ) +``` + +--- + +## You can't delete via subclasses! + +```python +@dataclasses.dataclass +class Vector2D: + x: float + y: float + + def mag(self): + return (self.x**2 + self.y**2) ** 0.5 + + +class Vector3D(Vector2D): + z: float +``` + +**`mag2` will be incorrect unless overridden!** + +--- + +## Safer design + +```python +@dataclasses.dataclass +class Vector: + pass + + +@dataclasses.dataclass +class Vector2D(Vector): + x: float + y: float + + +class Vector3D(Vector): + x: float + y: float + z: float +``` + +--- + +## Abstract base classes + +What about `mag2`? We want to require that it be implemented in all subclasses of `Vector`. + +```python +class Vector(abc.ABC): + @abc.abstractmethod + def mag2(self): + pass + + def mag(self): + return self.mag2() ** 0.5 +``` + +`Vector` is called an abstract base class. Checked on instantiation. + +--- + +## Interfaces (Protocol preview) + +`Vector` can also be seen as a set of _allowed_ methods: + +```python +def do_something(x): + return x.mag() + x.mag2() +``` + +Any class that defined `mag` and `mag2` would work here (due to duck typing). + +- **Interface**: A collection of expected methods/properties +- **Structural Subtyping**: Calling something a subclass using structure +- **Protocol**: Python's formalization, will see in a couple of weeks + +--- + +## collections.abc + +Lots of Interfaces are in `collections.abc`: + +- `Sized`: Anything with `__len__` + +```pycon +>>> isinstance(list(), collections.abc.Sized) +True +``` + +But `list` does not inherit from `Sized`! This is structural subtyping. + +Most standard library interfaces use dunder names for methods. + +--- + +## Exceptions + +Exceptions build this structure almost entirely just for the structure itself! + +```python +print(f"{KeyError.__mro__ = }") +``` + +```output +KeyError.__mro__ = (, + , + , + , + ) +``` + +So you can catch `KeyError` by catching `LookupError`, for example. + +--- + +## Multiple inheritance + +You can also inherit from more than one at a time. + +- `__mro__` builds linear history from tree + +Just because you can, doesn't mean you should. ;) + +--- + +## Special methods (dunder methods) + +You can customize the syntax around objects, such as: + +- `__init__`: Runs when an object is created +- `__repr__`: Display of object on REPL +- `__str__`: Conversion of object to string +- `__add__`/`__sub__`/`__mul__`/`__truediv__`: Math operators +- `__iadd__`/`__isub__`/`__imul__`/`__itruediv__`: Inplace math +- `__radd__`/`__rsub__`/`__rmul__`/`__rtruediv__`: Right acting math +- `__eq__`/`__neq__`/`__lt__`/`__gt__`/`__ge__`/`__le__`: Comparisons + +--- + +## Design + +Choice: imperative vs. declarative + +- Imperative design: make function that do each step. + - Have to repeat this process every new data structure. + - Not modular: the structure is mangled up with the conversions. +- Declarative design: Declare the structure, then define conversions, etc. independent of the structure details. + - Can be reused with new data structures. + - Modular: can use libraries like `cattrs`. + +--- + +## Example: using dataclasses + +See code example in `content/week3/config_example`. + +Input: + +```json +{ + "size": 100, + "name": "Test", + "simulation": true, + "details": { + "info": "Something or other" + }, + "duration": 10.0 +} +``` + +--- + +## Example: using dataclasses (2) + +```python +@dataclasses.dataclass +class Configuration: + size: int + name: str + simulation: bool + path: str + duration: float +``` + +--- + +## Conversion from JSON + +```python +def new_configuration_from_json(filename): + """Read a JSON file and return the contents as a Configuration object.""" + + with open(filename, encoding="utf-8") as f: + json_dict = json.load(f) + + # Optional, but protects against extra keys in the JSON file + config_dict = {f.name: json_dict[f.name] for f in dataclasses.fields(Configuration)} + + return Configuration(**config_dict) +``` + +--- + +## No custom code needed to convert to JSON + +```python +json.dumps(dataclasses.asdict(data), indent=2) +``` + +```json +{ + "size": 100, + "name": "Test", + "simulation": true, + "details": { + "info": "Something or other" + }, + "duration": 10.0 +} +``` + +--- + +## Can put conversion functions in class: + +```python +@dataclasses.dataclass +class Configuration: + size: int + name: str + simulation: bool + path: str + duration: float + + @classmethod + def from_json(cls, filename): + ... + return cls(**config_dict) + + def to_dict(self): + return dataclasses.asdict(self) +``` diff --git a/slides/week-06-2.md b/slides/week-06-2.md new file mode 100644 index 0000000..b3d61ea --- /dev/null +++ b/slides/week-06-2.md @@ -0,0 +1,346 @@ +--- +marp: true +theme: gaia +_class: lead +paginate: false +backgroundColor: #fff +backgroundImage: url('https://marp.app/assets/hero-background.svg') +--- + +# APC 524 + +## Object Oriented Programming + +--- + +## Why design classes? + +We want an interface that is: + +- Easy to use correctly +- Hard to use incorrectly + +We don't want to give up on our principles, like _modularity_! + +You absolutely can design a spaghetti mess of code with OOP! + +--- + +- **Encapsulation** (1): Bundling data & operations together +- **Encapsulation** (2): Isolating implementation & minimal public part +- **Object**/**Instance**: A variable holding data with a type holding +- **Attribute**/**Property**: Something accessible on an object +- **Member**: A data attribute stored in each object +- **Method**/**Member function**: A callable attribute stored in a class +- **Constructor**/**Initializer**: A function that creates instances +- **Destructor**: A function that runs when an instance is destroyed +- **Inheritance**: When one class uses another as a starting point +- **Composition** (1): Adding instances of classes as members +- **Composition** (2): Combining classes via multiple inheritance + +--- + +## Why use classes? + +- Keep related values tother +- **Inheritance** and **Composition** can be used to keep code DRY +- Clean way to establish interfaces (ABCs/Protocols) +- Ensure required code is run +- Associate functions to a specific type (varies by language) + +--- + +## Why inheritance? + +A: **Parent class** / **super class** B: **Child class** + +```python +class A: + pass + + +class B(A): + pass +``` + +- Provides the concept of "is a" (`isinstance` / `issubclass` in Python) +- Provides a way to "realize" or "implement" a specified interface + +--- + +## Why composition/aggregation? + +```python +class A: + pass + + +class B: + def __init__(self): + self.a = A() +``` + +- Provides the concept of "has a", or "inherent part of". +- **Composition**: contained object's lifetime is tied to the parent +- **Aggregation** parent holds objects, but they exist separately too +- "Wrapped" class can re-expose (some) of contained methods (remember inheritance can't remove attributes!) + +--- + +## UML diagrams + +UML, or Unified Modeling Language, is a method of displaying class diagrams [(read more here)](https://www.lucidchart.com/pages/what-is-UML-unified-modeling-language), or read about it for [mermaid](https://mermaid-js.github.io/mermaid/#/classDiagram), which is supported quite a lot of places these days, including GitHub. + +--- + +## SOLID + +- **S**ingle responsibility: classes should do one thing (modular) +- **O**pen-closed principle: API stable (closed) but extensible (open) +- **L**iskov substitution: children work where parents expected +- **I**nterfaces should be specific and segregated +- **D**ependency inversion + - High level code _should not_ depend on low level code details + - Low level code _should_ depend on high level abstractions + +--- + +## Interfaces example + +- Xerox makes a mutlifunction machine + - `Stapler` and `Printer` are subclasses of `Job` + - `Job` holds _everything_ for interacting with the machine + - `Printer` can access stapling functions since they are in `Job` +- Now they want to make a coper that can't staple + - But `Job` still knows about stapling! +- Testing is also much harder + +--- + + + +# Design principles & patterns + +--- + +## Minimal public API + +```python +class Container: + def __init__(self, x): + self.x = x + + +c = Container(1) +c.x = 2 +``` + +Since we allow setting `x` after creation, we have to design the class assuming a user can manually change it at any time! + +--- + +## Minimal public API (2) + +```python +class Container: + def __init__(self, x): + self._x = x + + @property + def x(self): + return self._x +``` + +Now a user can't change `x`, only set it + +- `frozen=True` does this with dataclasses +- We can also make this hashable now (usable in dict keys and sets) +- The underscore means it's "hidden" (some languages have private) + +--- + +## Pattern: code reuse + +```python +# fmt: off +class SteppedCode: + def step_1(self): + print("Working on step 1") + def step_2(self): + print("Working on step 2") + def step_3(self): + print("Working on step 3") + def run(self): + self.step_1() + self.step_2() + self.step_3() +# fmt: on +SteppedCode().run() +``` + +--- + +## Pattern: code reuse (2) + +Now you can replace one step via inheritance: + +```python +class NewSteps(SteppedCode): + def step_2(self): + print("Replaced step 2") + + +NewSteps().run() +``` + +--- + +## Pattern: code reuse (3) + +Or inject code around an existing step: + +```python +class SurroundedSteps(SteppedCode): + def step_2(self): + print("Before step 2") + super().step_2() + print("After step 2") + + +SurroundedSteps().run() +``` + +--- + +## Required interface + +- Require an implementer to provide one or methods +- Often via `@abstractmethod` and ABCs + +Extended "Integrator" example in class notes. + +--- + +## Functors: the problem + +```python +_start = 0 + + +def incr(): + global _start + _start += 1 + return _start + + +print(f"{incr() = }") # incr() = 1 +print(f"{incr() = }") # incr() = 2 +print(f"{incr() = }") # incr() = 3 +``` + +This is _global_, there can only by one. + +--- + +## Functors: the problem (2) + +```python +def make_incr(): + _start = 0 + + def incr(): + nonlocal _start + _start += 1 + return _start + + return incr + + +incr1 = make_incr() +print(f"{incr1() = }") # incr1() = 1 +print(f"{incr1() = }") # incr1() = 2 +``` + +Not global anymore, but still ugly. `_start` is kept around via reference counting. + +--- + +## Functors: the solution + +```python +class Incr: + start: int = 0 + + def __call__(self): + self.start += 1 + return self.start + + +incr = Incr() +print(f"{incr() = }") # incr() = 1 +print(f"{incr() = }") # incr() = 2 +``` + +- **Functor**: A callable object that holds state + +--- + +## Separation of concerns + +Each class should address a specific concern. + +Some languages allow you to define classes in multiple places (Ruby, Rust), and just load the part you need or extend it without subclassing. Python does not (at least not officially/nicely), neither does C++. Multiple inheritance, type dispatch (C++, Julia), or Traits (Rust) can provide similar benefits, though. + +--- + +## eDSLs: embedded Domain Specific Languages + +```python +class Path(str): + def __truediv__(self, other): + return self.__class__(f"{self}/{other}") + + +print(Path("one") / Path("two")) +``` + +You can use operator overloading (and a few other things) to make something for a domain specific purpose instead of a new language. + +Don't do this exact thing, `pathlib.Path` already exists. :) + +--- + +## Mixins + +A very specific form of multiple inheritance. + +### Rules: + +- No `__init__` +- No members (those need `__init__`) +- No `super()` +- No overloading the class they are to mix into + +You _can_ bend these rules a bit, but then you are moving into multiple inheritance territory, so be very careful. + +--- + +## Mixins (2) + +```python +class PathMixin: + def __truediv__(self, other): + return self.__class__(f"{self}/{other}") + + +class Path(str, PathMixin): + pass + + +print(Path("one") / Path("two")) +``` + +- Example: a drag-and-drop mixin for a GUI framework +- FYI, Python specific simi-alternative: new class keywords