{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "# Prerequisite Knowledge\n", "\n", ".. note::\n", " This is an introduction to a couple intermediate Python concepts that are useful for excelbird.\n", " **Skip this page if:** you already understand *object-oriented-programming*, *comprehensions*, and *iterable unpacking*.\n", "\n", "---\n", "\n", "## Prerequisites (for this tutorial)\n", "\n", "- You understand basic types: *int*, *str*, *list*, *dict*, and know how to use and create them\n", "- You can write a function that takes *required* and *optional* arguments (i.e. `def func(required, optional=None): ...`) and know how to call that function.\n", "- You understand loops and how to iterate through things\n", "\n", "## Key terms\n", "\n", "- **Argument** and **parameter**\n", " - You'll see these terms a lot. They refer to the same thing, but indicate the perspective from which we're referring. For instance, function `def func(name): ...` has one *parameter*, `name`. When you call `func('Jeff')` you've given \"Jeff\" as an *argument* for `name`. Arguments are the things given, and parameters are taken/expeceted.\n", "- **Positional arguments** versus **keyword arguments**\n", " - There are **only two** ways to pass an argument: By keyword, and by position. If we write, `func('Jeff')`, we've passed Jeff's name as a **positional argument** to *func*. If instead we write, `func(name='Jeff')`, we've given it a **keyword argument**\n", "\n", "## Naming things\n", "\n", "You may have noticed that some things are named in *snake_case* and others use *TitleCase*. This means something. *TitleCase* indicates the term is a **class** whose source code is written in Python. Pandas `DataFrame` is a Python class. Numpy's `ndarray` is written in C\n", "\n", "In excelbird, all layout element types are custom classes, **not** functions.\n", "\n", "## Objects and Classes\n", "\n", "If asked to define what a \"car\" means, you may write:\n", "\n", "Car definition:\n", "\n", "- Has wheels\n", "- Has colored paint\n", "- Moves forward\n", "\n", "You've just created a class. You defined a *type* of object. No car was created yet. You defined what attributes a car has, *and* how it should behave. Each time one is manufactured, you will have created an **instance** of Car, just like `1` is an instance of `int`, and `'hello'` is an instance of `str`." ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "moving forward\n" ] }, { "data": { "text/plain": [ "20" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "class Car:\n", " def __init__(self, color, wheel_size):\n", " self.color = color\n", " self.wheel_size = wheel_size\n", " \n", " def move_forward(self):\n", " print(\"moving forward\")\n", " \n", "\n", "my_car = Car('white', 20)\n", "\n", "my_car.move_forward()\n", "print(my_car.wheel_size)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "### Magic Methods\n", "\n", "Excelbird objects can handle being used in arithmetic expressions, like `my_column * my_row / 5`. How is this possible?\n", "\n", "An object's magic methods have pre-defined names, and get called automatically by Python when a certain event happens. Square brackets: `my_list[5]` is just shorthand for `my_list.__getitem__(5)`. Python doesn't care what 'my_list' is - it will just call `__getitem__` when it sees the brackets. You can then customize that method however you want.\n", "\n", "Math symbols work the same way: `\"py\" + \"thon\"` is just a shortcut for `\"py\".__add__(\"thon\")`. Define custom logic for `__add__`, and you can break math." ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "500" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "class Cell:\n", " def __add__(self, other):\n", " return other * 100\n", "\n", "c = Cell()\n", "c + 5" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Inline If-Else (Ternary)\n", "\n", "In Python, we can write if-else statements in a single line. In excelbird, this feature is necessary for being able to nest logic inside your layout, instead of writing it elsewhere." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "age = 12\n", "\n", "can_drink = True if age >= 21 else False\n", "\n", "can_drink_message = (\n", " \"Yes\" if age >= 21\n", " else \"No\" if age >= 18\n", " else \"Not a chance!\" if age >= 12\n", " else \"That's a seriously bad idea.\"\n", ")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "The rules are simple:\n", "\n", "- You **must** include an 'else' clause at the end of the statement\n", "- An 'elif' can be simulated by writing `else if `, and you can include as many of these as you want.\n", "\n", "**Where can I use an inline conditional?**\n", "\n", "- Literally anywhere you'd use a variable. Since the inline conditional will always return a value, it's safe to use it in the middle of a function call, or before accessing an instance method or attribute, as long as you surround the statement in parentheses\n", "\n", "### Excelbird Example\n", "\n", ".. note::\n", " All excelbird layout elements accept `None` as a child argument, and immediately filter it out. This lets you make an inline decision on whether to display something" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from excelbird import Book, Sheet, Cell\n", "\n", "show_element = False\n", "\n", "Book(\n", " Sheet(\n", " Cell(1),\n", " Cell(2) if show_element is True else None,\n", " Cell(3),\n", " ),\n", ").write(\"test.xlsx\")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## List Comprehensions\n", "\n", "Comprehensions are a necessary skill for nesting inline logic in an excelbird layout. They're easy to learn, but take some time to master. Just practice!\n", "\n", "In other programming languages, you might write code that looks like:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "items = []\n", "for i in range(5):\n", " items.append(i)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "In Python, we can write..." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "items = [i for i in range(5)]" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "### Comprehension Inline If-Else\n", "\n", "In the example above, there are **two** places we can apply nested logic\n", "\n", "1. To the returned element\n", "2. To the iterable we're looping through." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[0, None, 2, None, 4]" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "[i if i % 2 == 0 else None for i in range(5)] # option 1" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[0, 2, 4]" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "[i for i in range(5) if i % 2 == 0] # option 2" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Examine the second example. **We broke rule #1** of the inline-if/else discussion earlier: we didn't include an 'else' clause.\n", "\n", "When written in place of a value, inline-if statements *determine which value to return*, but when written after a sequence (like `range(5)`), they *filter out elements returned by the sequence*. Therefore, an 'else' clause serves no purpose when our only option is to filter out elements." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Iterable Unpacking\n", "\n", "This is a simple feature, but it might take a few minutes to wrap your head around.\n", "\n", "Try placing a `*` or `**` before an inline reference to something: `*my_list` or `**my_dict`.\n", "\n", "Examine the following examples." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1\n", "[2, 3]\n", "4\n" ] } ], "source": [ "list_without_unpacking = [\n", " 1,\n", " [2, 3],\n", " 4,\n", "]\n", "for e in list_without_unpacking:\n", " print(e)" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1\n", "2\n", "3\n", "4\n" ] } ], "source": [ "list_with_unpacking = [\n", " 1,\n", " *[2, 3],\n", " 4,\n", "]\n", "for e in list_with_unpacking:\n", " print(e)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "The `*` seems to have simulated the effect of passing each element in a list separately, instead of passing the list itself.\n", "\n", "The unpacking happens immediately, so the receiver of your arguments has no idea you've unpacked anything\n", "\n", "Here's a function that requires **two** arguments" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "takes_two() missing 1 required positional argument: 'b'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[6], line 7\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[39m# We can't just give it a list\u001b[39;00m\n\u001b[1;32m 5\u001b[0m inputs \u001b[39m=\u001b[39m [\u001b[39m1\u001b[39m, \u001b[39m2\u001b[39m]\n\u001b[0;32m----> 7\u001b[0m takes_two(inputs)\n", "\u001b[0;31mTypeError\u001b[0m: takes_two() missing 1 required positional argument: 'b'" ] } ], "source": [ "def takes_two(a, b):\n", " print(f\"I received {a} and {b}\")\n", "\n", "# We can't just give it a list\n", "inputs = [1, 2]\n", "\n", "takes_two(inputs)" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "I received 1, and 2\n" ] } ], "source": [ "# But if we unpack our list, it works!\n", "takes_two(*inputs)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "The function we called had **no idea** we ever unpacked anything.\n", "\n", "`takes_two(inputs)` ---> `takes_two([1, 2])`\n", "\n", "`takes_two(*inputs)` ---> `takes_two(1, 2)`\n", "\n", "This can be done on the **receiving** end as well. Put a `*` in front of a function's param" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "'everything' is a tuple. Its value is ()\n", "'everything' is a tuple. Its value is (1,)\n", "'everything' is a tuple. Its value is (1, 2)\n", "'everything' is a tuple. Its value is (1, 2, [3, 4])\n" ] } ], "source": [ "def absorb_everything(*everything):\n", " print(f\"'everything' is a tuple. Its value is\", everything)\n", "\n", "\n", "absorb_everything() # It's optional!\n", "absorb_everything(1)\n", "absorb_everything(1, 2)\n", "absorb_everything(1, 2, [3, 4])" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "It even works when we're receiving things *from* a function" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "ename": "ValueError", "evalue": "too many values to unpack (expected 2)", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[26], line 5\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39m1\u001b[39m, \u001b[39m2\u001b[39m, \u001b[39m3\u001b[39m, \u001b[39m4\u001b[39m\n\u001b[1;32m 4\u001b[0m \u001b[39m# It's giving us four things, so this will not work\u001b[39;00m\n\u001b[0;32m----> 5\u001b[0m one, two \u001b[39m=\u001b[39m gives_four()\n", "\u001b[0;31mValueError\u001b[0m: too many values to unpack (expected 2)" ] } ], "source": [ "def gives_four():\n", " return 1, 2, 3, 4\n", " \n", "# This won't work. We're assigning 4 things to 2 variables\n", "one, two = gives_four()" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Instead, we can put the first value in `one`, and everything else in `the_rest`" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1\n", "[2, 3, 4]\n" ] } ], "source": [ "one, *the_rest = gives_four()\n", "\n", "print(one)\n", "print(the_rest)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "### What about keyword arguments?\n", "\n", "In the \"absorb_everything\" example earlier, **we lied to you**. The term, `*everything` did not absorb everything. It absorbed all *positional* arguments." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "'everything': (1, 2, 3, 4)\n", "'another_thing': None\n" ] } ], "source": [ "def absorb_everything(*everything, another_thing=None):\n", " print(\"'everything': \", everything)\n", " print(\"'another_thing': \", another_thing)\n", "\n", "absorb_everything(1, 2, 3, 4)" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "'everything': (1, 2, 3, 4)\n", "'another_thing': Fried Chicken\n" ] } ], "source": [ "absorb_everything(1, 2, 3, 4, another_thing='Fried Chicken')" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "No problem! We can do the exact same thing with keyword arguments as we did with positional arguments.\n", "\n", "Python has decided we must use `**` instead of `*`, and it will return a **dictionary** of key-value pairs, instead of a tuple" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Positional args tuple: (1,)\n", "Keyword args dictionary: {}\n", "\n", "Positional args tuple: ()\n", "Keyword args dictionary: {'stuff': 'abcd'}\n", "\n", "Positional args tuple: (1, 2)\n", "Keyword args dictionary: {'one': 10, 'two': 20}\n", "\n" ] } ], "source": [ "def actually_absorb_everything(*positional_args, **keyword_args):\n", " print(\"Positional args tuple: \", positional_args)\n", " print(\"Keyword args dictionary: \", keyword_args)\n", " print()\n", "\n", "actually_absorb_everything(1)\n", "actually_absorb_everything(stuff='abcd')\n", "actually_absorb_everything(1, 2, one=10, two=20)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Keyword argument unpacking in particular is very powerful. You can unpack a dictionary **into** keyword arguments, the same way we unpacked a list into integers at the beginning of this tutorial\n", "\n", "This is slightly less intuitive, because the **string keys** in your dictionary will be executed as **real python keywords** as soon as they're unpacked.\n", "\n", "In other words, `**{'name': 'Jeff', 'age': 85}` will immediately be treated as `name='Jeff', age=85` inplace. No more strings." ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "size: None\n", "fill_color: blue\n", "auto_open: True\n" ] } ], "source": [ "def takes_kwargs(size=None, fill_color=None, auto_open=None):\n", " print(\"size: \", size)\n", " print(\"fill_color: \", fill_color)\n", " print(\"auto_open: \", auto_open)\n", "\n", "\n", "options = {\n", " 'fill_color': 'blue',\n", " 'auto_open': True,\n", "}\n", "\n", "takes_kwargs(**options)" ] } ], "metadata": { "kernelspec": { "display_name": "env", "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.11.1" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "6ee8c39a5e4f5a5ffe92ac0abdca1f10885ded7e28c74450614be57680931831" } } }, "nbformat": 4, "nbformat_minor": 2 }