{ "cells": [ { "cell_type": "raw", "metadata": {}, "source": [ "---\n", "title: Object Oriented Programming\n", "file_title: OOP_Python\n", "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There are different programming paradigms. The kind you are likely familiar with is *procedural* programming. Python is capable, and even shines with procedural programming. However, Python has an *Object-oriented* programming paradigm that can help create maintainable and reusable code." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### What are objects?\n", "Objects are way for you to define an abstraction of a data type that can be reused. Objects can be thought of as physical objects that contain a set of properties. Those properties can be adjusted through various functions that are applied to that object type (those are called methods). A *class* is the template that each object is based on. This is really abstract and doesn't make sense without some solid examples. So let's jump into an example." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Structures as objects.\n", "Let's look at defining some basic classes to help with structural analysis.\n", "\n", "Below I have defined a basic `Node` class. A node is a place where a structural element (or memeber) begins and ends. In Python we use the keyword `class` to define a class name. The name of the class is `Node`, and it is based on the `object` class.\n", "\n", "The other thing we need is to define the \"constructor\" method. This is just a function that is called when we make an object based on the `Node` class. To define a node we need an `x` and a `y` coordinate. The keyword `self` is a reference to the object that the `Node` class creates. We then say that the object takes `x` and defines it as an attribute of the object it*self*. The same occurs with `y`." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "class Node(object):\n", " def __init__(self, x, y):\n", " self.x = x\n", " self.y = y" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So what does that get us? Well now we can define `Node` objects in Python and extract back the data we entered. Below I will define two nodes called `n1` and `n2`." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "n1 = Node(0, 0)\n", "n2 = Node(1, 0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's try and get both of the nodal coordinates from the objects." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0 0\n", "1 0\n" ] } ], "source": [ "print(n1.x, n1.y)\n", "print(n2.x, n2.y)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is great because the nodal coordinates are *encapsulated* within the namespace of the object. That is, we can easily reference the `x` and `y` coordinates of both nodes without having to define an array and remember indicies or define unique names for the attributes of the node object." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Well, now that we have two nodes, we might as well connect them! We will now define a `Member` class. This time instead of taking in a list of coordinate pairs, we will take a start node (SN) and end node (EN) to create the member." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "class Member(object):\n", " def __init__(self, SN, EN):\n", " self.SN = SN\n", " self.EN = EN" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we'll use the `Member` class to create a member that connects nodes 1 and 2." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "m1 = Member(n1, n2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's echo the coordinates of the members start node and end node:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(0,0) --> (1,0)\n" ] } ], "source": [ "print(f\"({m1.SN.x},{m1.SN.y}) --> ({m1.EN.x},{m1.EN.y})\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, the object-oriented approach here provides a very natural way of grouping data that very nicely builds on the things that we have already defined. There are no namespace issues, and we're not accidentally overriding variables. But what else can this get us?\n", "\n", "Let's take the `Member` class we defined and define a method, and wrap it in a `@property` decorator. A method is simply a function that operates on the objects own set of data. It can take in inputs, and must include the object itself. It then returns some output. By using the proprety decorator, we allow the length to be a dynamic property of the `Member` attributes. If we ever update a start node, then length automatically updates." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "class Member(object):\n", " def __init__(self, SN, EN):\n", " self.SN = SN\n", " self.EN = EN\n", " \n", " @property\n", " def length(self):\n", " dx = self.EN.x - self.SN.x\n", " dy = self.EN.y - self.SN.y\n", " L = np.sqrt(dx**2 + dy**2)\n", " return L" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's define member 1 based on the updated `Member` class and take a look at the new attribute `length` we have access to." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1.0" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m1 = Member(n1, n2)\n", "m1.length" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, whenever we need to use member 1 for any mathematical operations, we have access to its base attributes, but we also have access to its derived attributes, which can be really convenient for building up more complex programs." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Conclusion\n", "This has been a pretty quick look at what object-oriented programming can do for structural analysis. This particular use is what convinced me to learn it and help me understand why it is important. The classes I have defined here are relatively simple, but they can easily scale. For instance, each node could have an associated nodal cost. Each member could have its own cross section (and each cross section type have their own attributes and derived attributes), and element stiffness. These could all be then built into a `Structure` class." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.10" }, "varInspector": { "cols": { "lenName": 16, "lenType": 16, "lenVar": 40 }, "kernels_config": { "python": { "delete_cmd_postfix": "", "delete_cmd_prefix": "del ", "library": "var_list.py", "varRefreshCmd": "print(var_dic_list())" }, "r": { "delete_cmd_postfix": ") ", "delete_cmd_prefix": "rm(", "library": "var_list.r", "varRefreshCmd": "cat(var_dic_list()) " } }, "types_to_exclude": [ "module", "function", "builtin_function_or_method", "instance", "_Feature" ], "window_display": false } }, "nbformat": 4, "nbformat_minor": 2 }