Prerequisite Knowledge#

Note

This is an introduction to a couple intermediate Python concepts that are useful for excelbird. Skip this page if: you already understand object-oriented-programming, comprehensions, and iterable unpacking.


Prerequisites (for this tutorial)#

  • You understand basic types: int, str, list, dict, and know how to use and create them

  • 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.

  • You understand loops and how to iterate through things

Key terms#

  • Argument and parameter

    • 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.

  • Positional arguments versus keyword arguments

    • 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

Naming things#

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

In excelbird, all layout element types are custom classes, not functions.

Objects and Classes#

If asked to define what a “car” means, you may write:

Car definition:

  • Has wheels

  • Has colored paint

  • Moves forward

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.

[35]:
class Car:
    def __init__(self, color, wheel_size):
        self.color = color
        self.wheel_size = wheel_size

    def move_forward(self):
        print("moving forward")


my_car = Car('white', 20)

my_car.move_forward()
print(my_car.wheel_size)
moving forward
[35]:
20

Magic Methods#

Excelbird objects can handle being used in arithmetic expressions, like my_column * my_row / 5. How is this possible?

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.

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.

[36]:
class Cell:
    def __add__(self, other):
        return other * 100

c = Cell()
c + 5
[36]:
500

Inline If-Else (Ternary)#

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.

[ ]:
age = 12

can_drink = True if age >= 21 else False

can_drink_message = (
    "Yes" if age >= 21
    else "No" if age >= 18
    else "Not a chance!" if age >= 12
    else "That's a seriously bad idea."
)

The rules are simple:

  • You must include an ‘else’ clause at the end of the statement

  • An ‘elif’ can be simulated by writing else <value> if <condition>, and you can include as many of these as you want.

Where can I use an inline conditional?

  • 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

Excelbird Example#

Note

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

[ ]:
from excelbird import Book, Sheet, Cell

show_element = False

Book(
    Sheet(
        Cell(1),
        Cell(2) if show_element is True else None,
        Cell(3),
    ),
).write("test.xlsx")

2c15beb7677945dd94ff4bd8241fcf2d

List Comprehensions#

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!

In other programming languages, you might write code that looks like:

[ ]:
items = []
for i in range(5):
    items.append(i)

In Python, we can write…

[ ]:
items = [i for i in range(5)]

Comprehension Inline If-Else#

In the example above, there are two places we can apply nested logic

  1. To the returned element

  2. To the iterable we’re looping through.

[5]:
[i if i % 2 == 0 else None for i in range(5)]  # option 1
[5]:
[0, None, 2, None, 4]
[6]:
[i for i in range(5) if i % 2 == 0]  # option 2
[6]:
[0, 2, 4]

Examine the second example. We broke rule #1 of the inline-if/else discussion earlier: we didn’t include an ‘else’ clause.

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.

Iterable Unpacking#

This is a simple feature, but it might take a few minutes to wrap your head around.

Try placing a * or ** before an inline reference to something: *my_list or **my_dict.

Examine the following examples.

[9]:
list_without_unpacking = [
    1,
    [2, 3],
    4,
]
for e in list_without_unpacking:
    print(e)
1
[2, 3]
4
[4]:
list_with_unpacking = [
    1,
    *[2, 3],
    4,
]
for e in list_with_unpacking:
    print(e)
1
2
3
4

The * seems to have simulated the effect of passing each element in a list separately, instead of passing the list itself.

The unpacking happens immediately, so the receiver of your arguments has no idea you’ve unpacked anything

Here’s a function that requires two arguments

[6]:
def takes_two(a, b):
    print(f"I received {a} and {b}")

# We can't just give it a list
inputs = [1, 2]

takes_two(inputs)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[6], line 7
      4 # We can't just give it a list
      5 inputs = [1, 2]
----> 7 takes_two(inputs)

TypeError: takes_two() missing 1 required positional argument: 'b'
[7]:
# But if we unpack our list, it works!
takes_two(*inputs)
I received 1, and 2

The function we called had no idea we ever unpacked anything.

takes_two(inputs) —> takes_two([1, 2])

takes_two(*inputs) —> takes_two(1, 2)

This can be done on the receiving end as well. Put a * in front of a function’s param

[25]:
def absorb_everything(*everything):
    print(f"'everything' is a tuple. Its value is", everything)


absorb_everything()  # It's optional!
absorb_everything(1)
absorb_everything(1, 2)
absorb_everything(1, 2, [3, 4])
'everything' is a tuple. Its value is ()
'everything' is a tuple. Its value is (1,)
'everything' is a tuple. Its value is (1, 2)
'everything' is a tuple. Its value is (1, 2, [3, 4])

It even works when we’re receiving things from a function

[26]:
def gives_four():
    return 1, 2, 3, 4

# This won't work. We're assigning 4 things to 2 variables
one, two = gives_four()
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[26], line 5
      2     return 1, 2, 3, 4
      4 # It's giving us four things, so this will not work
----> 5 one, two = gives_four()

ValueError: too many values to unpack (expected 2)

Instead, we can put the first value in one, and everything else in the_rest

[14]:
one, *the_rest = gives_four()

print(one)
print(the_rest)
1
[2, 3, 4]

What about keyword arguments?#

In the “absorb_everything” example earlier, we lied to you. The term, *everything did not absorb everything. It absorbed all positional arguments.

[18]:
def absorb_everything(*everything, another_thing=None):
    print("'everything':    ", everything)
    print("'another_thing': ", another_thing)

absorb_everything(1, 2, 3, 4)
'everything':     (1, 2, 3, 4)
'another_thing':  None
[19]:
absorb_everything(1, 2, 3, 4, another_thing='Fried Chicken')
'everything':     (1, 2, 3, 4)
'another_thing':  Fried Chicken

No problem! We can do the exact same thing with keyword arguments as we did with positional arguments.

Python has decided we must use ** instead of *, and it will return a dictionary of key-value pairs, instead of a tuple

[23]:
def actually_absorb_everything(*positional_args, **keyword_args):
    print("Positional args tuple:    ", positional_args)
    print("Keyword args dictionary:  ", keyword_args)
    print()

actually_absorb_everything(1)
actually_absorb_everything(stuff='abcd')
actually_absorb_everything(1, 2, one=10, two=20)
Positional args tuple:     (1,)
Keyword args dictionary:   {}

Positional args tuple:     ()
Keyword args dictionary:   {'stuff': 'abcd'}

Positional args tuple:     (1, 2)
Keyword args dictionary:   {'one': 10, 'two': 20}

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

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.

In other words, **{'name': 'Jeff', 'age': 85} will immediately be treated as name='Jeff', age=85 inplace. No more strings.

[31]:
def takes_kwargs(size=None, fill_color=None, auto_open=None):
    print("size:       ", size)
    print("fill_color: ", fill_color)
    print("auto_open:  ", auto_open)


options = {
    'fill_color': 'blue',
    'auto_open': True,
}

takes_kwargs(**options)
size:        None
fill_color:  blue
auto_open:   True