From f7193462bb06dac58c00cdc5790a0bfe75da65a2 Mon Sep 17 00:00:00 2001 From: John Stachurski Date: Fri, 1 Mar 2024 11:33:43 +1100 Subject: [PATCH 01/10] misc --- lectures/_toc.yml | 1 + lectures/names.md | 686 ++++++++++++++++++++++++++++++++++++++++++ lectures/oop_intro.md | 40 ++- 3 files changed, 714 insertions(+), 13 deletions(-) create mode 100644 lectures/names.md diff --git a/lectures/_toc.yml b/lectures/_toc.yml index 046662a9..82d2af3b 100644 --- a/lectures/_toc.yml +++ b/lectures/_toc.yml @@ -10,6 +10,7 @@ parts: - file: functions - file: python_essentials - file: oop_intro + - file: names - file: python_oop - file: workspace - caption: The Scientific Libraries diff --git a/lectures/names.md b/lectures/names.md new file mode 100644 index 00000000..8e460ab5 --- /dev/null +++ b/lectures/names.md @@ -0,0 +1,686 @@ +--- +jupytext: + text_representation: + extension: .md + format_name: myst +kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +(oop_intro)= +```{raw} jupyter +
+ + QuantEcon + +
+``` + + + +# OOP I: Names and Namespaces + +```{contents} Contents +:depth: 2 +``` + +## Overview + +This lecture is all about variable names, how they can be used and how they are +understood by the Python interpreter. + +This might sound a little dull but the model that Python has adopted for +handling names is elegant and interesting. + +In addition, you will save yourself many hours of debugging if you have a good +understanding of how names work in Python. + +(name_res)= +## Variable Names in Python + +```{index} single: Python; Variable Names +``` + +Consider the Python statement + +```{code-cell} python3 +x = 42 +``` + +We now know that when this statement is executed, Python creates an object of +type `int` in your computer's memory, containing + +* the value `42` +* some associated attributes + +But what is `x` itself? + +In Python, `x` is called a **name**, and the statement `x = 42` **binds** the name `x` to the integer object we have just discussed. + +Under the hood, this process of binding names to objects is implemented as a dictionary---more about this in a moment. + +There is no problem binding two or more names to the one object, regardless of what that object is + +```{code-cell} python3 +def f(string): # Create a function called f + print(string) # that prints any string it's passed + +g = f +id(g) == id(f) +``` + +```{code-cell} python3 +g('test') +``` + +In the first step, a function object is created, and the name `f` is bound to it. + +After binding the name `g` to the same object, we can use it anywhere we would use `f`. + +What happens when the number of names bound to an object goes to zero? + +Here's an example of this situation, where the name `x` is first bound to one object and then rebound to another + +```{code-cell} python3 +x = 'foo' +id(x) +``` + +```{code-cell} python3 +x = 'bar' # No names bound to the first object +``` + +What happens here is that the first object is garbage collected. + +In other words, the memory slot that stores that object is deallocated, and returned to the operating system. + +Garbage collection is actually an active research area in computer science. + +You can [read more on garbage collection](https://rushter.com/blog/python-garbage-collector/) if you are interested. + +## Namespaces + +```{index} single: Python; Namespaces +``` + +Recall from the preceding discussion that the statement + +```{code-cell} python3 +x = 42 +``` + +binds the name `x` to the integer object on the right-hand side. + +We also mentioned that this process of binding `x` to the correct object is implemented as a dictionary. + +This dictionary is called a namespace. + +```{admonition} Definition +A **namespace** is a symbol table that maps names to objects in memory. +``` + + +Python uses multiple namespaces, creating them on the fly as necessary. + +For example, every time we import a module, Python creates a namespace for that module. + +To see this in action, suppose we write a script `mathfoo.py` with a single line + +```{code-cell} python3 +%%file mathfoo.py +pi = 'foobar' +``` + +Now we start the Python interpreter and import it + +```{code-cell} python3 +import mathfoo +``` + +Next let's import the `math` module from the standard library + +```{code-cell} python3 +import math +``` + +Both of these modules have an attribute called `pi` + +```{code-cell} python3 +math.pi +``` + +```{code-cell} python3 +mathfoo.pi +``` + +These two different bindings of `pi` exist in different namespaces, each one implemented as a dictionary. + +We can look at the dictionary directly, using `module_name.__dict__` + +```{code-cell} python3 +import math + +math.__dict__.items() +``` + +```{code-cell} python3 +import mathfoo + +mathfoo.__dict__.items() +``` + +As you know, we access elements of the namespace using the dotted attribute notation + +```{code-cell} python3 +math.pi +``` + +In fact this is entirely equivalent to `math.__dict__['pi']` + +```{code-cell} python3 +math.__dict__['pi'] == math.pi +``` + +## Viewing Namespaces + +As we saw above, the `math` namespace can be printed by typing `math.__dict__`. + +Another way to see its contents is to type `vars(math)` + +```{code-cell} python3 +vars(math).items() +``` + +If you just want to see the names, you can type + +```{code-cell} python3 +# Show the first 10 names +dir(math)[0:10] +``` + +Notice the special names `__doc__` and `__name__`. + +These are initialized in the namespace when any module is imported + +* `__doc__` is the doc string of the module +* `__name__` is the name of the module + +```{code-cell} python3 +print(math.__doc__) +``` + +```{code-cell} python3 +math.__name__ +``` + +## Interactive Sessions + +```{index} single: Python; Interpreter +``` + +In Python, **all** code executed by the interpreter runs in some module. + +What about commands typed at the prompt? + +These are also regarded as being executed within a module --- in this case, a module called `__main__`. + +To check this, we can look at the current module name via the value of `__name__` given at the prompt + +```{code-cell} python3 +print(__name__) +``` + +When we run a script using IPython's `run` command, the contents of the file are executed as part of `__main__` too. + +To see this, let's create a file `mod.py` that prints its own `__name__` attribute + +```{code-cell} ipython +%%file mod.py +print(__name__) +``` + +Now let's look at two different ways of running it in IPython + +```{code-cell} python3 +import mod # Standard import +``` + +```{code-cell} ipython +%run mod.py # Run interactively +``` + +In the second case, the code is executed as part of `__main__`, so `__name__` is equal to `__main__`. + +To see the contents of the namespace of `__main__` we use `vars()` rather than `vars(__main__)`. + +If you do this in IPython, you will see a whole lot of variables that IPython +needs, and has initialized when you started up your session. + +If you prefer to see only the variables you have initialized, use `%whos` + +```{code-cell} ipython +x = 2 +y = 3 + +import numpy as np + +%whos +``` + +## The Global Namespace + +```{index} single: Python; Namespace (Global) +``` + +Python documentation often makes reference to the "global namespace". + +The global namespace is *the namespace of the module currently being executed*. + +For example, suppose that we start the interpreter and begin making assignments. + +We are now working in the module `__main__`, and hence the namespace for `__main__` is the global namespace. + +Next, we import a module called `amodule` + +```{code-block} python3 +:class: no-execute + +import amodule +``` + +At this point, the interpreter creates a namespace for the module `amodule` and starts executing commands in the module. + +While this occurs, the namespace `amodule.__dict__` is the global namespace. + +Once execution of the module finishes, the interpreter returns to the module from where the import statement was made. + +In this case it's `__main__`, so the namespace of `__main__` again becomes the global namespace. + +## Local Namespaces + +```{index} single: Python; Namespace (Local) +``` + +Important fact: When we call a function, the interpreter creates a *local namespace* for that function, and registers the variables in that namespace. + +The reason for this will be explained in just a moment. + +Variables in the local namespace are called *local variables*. + +After the function returns, the namespace is deallocated and lost. + +While the function is executing, we can view the contents of the local namespace with `locals()`. + +For example, consider + +```{code-cell} python3 +def f(x): + a = 2 + print(locals()) + return a * x +``` + +Now let's call the function + +```{code-cell} python3 +f(1) +``` + +You can see the local namespace of `f` before it is destroyed. + +## The `__builtins__` Namespace + +```{index} single: Python; Namespace (__builtins__) +``` + +We have been using various built-in functions, such as `max(), dir(), str(), list(), len(), range(), type()`, etc. + +How does access to these names work? + +* These definitions are stored in a module called `__builtin__`. +* They have their own namespace called `__builtins__`. + +```{code-cell} python3 +# Show the first 10 names in `__main__` +dir()[0:10] +``` + +```{code-cell} python3 +# Show the first 10 names in `__builtins__` +dir(__builtins__)[0:10] +``` + +We can access elements of the namespace as follows + +```{code-cell} python3 +__builtins__.max +``` + +But `__builtins__` is special, because we can always access them directly as well + +```{code-cell} python3 +max +``` + +```{code-cell} python3 +__builtins__.max == max +``` + +The next section explains how this works ... + +## Name Resolution + +```{index} single: Python; Namespace (Resolution) +``` + +Namespaces are great because they help us organize variable names. + +(Type `import this` at the prompt and look at the last item that's printed) + +However, we do need to understand how the Python interpreter works with multiple namespaces. + +Understanding the flow of execution will help us to check which variables are in scope and how to operate on them when writing and debugging programs. + + +At any point of execution, there are in fact at least two namespaces that can be accessed directly. + +("Accessed directly" means without using a dot, as in `pi` rather than `math.pi`) + +These namespaces are + +* The global namespace (of the module being executed) +* The builtin namespace + +If the interpreter is executing a function, then the directly accessible namespaces are + +* The local namespace of the function +* The global namespace (of the module being executed) +* The builtin namespace + +Sometimes functions are defined within other functions, like so + +```{code-cell} python3 +def f(): + a = 2 + def g(): + b = 4 + print(a * b) + g() +``` + +Here `f` is the *enclosing function* for `g`, and each function gets its +own namespaces. + +Now we can give the rule for how namespace resolution works: + +The order in which the interpreter searches for names is + +1. the local namespace (if it exists) +1. the hierarchy of enclosing namespaces (if they exist) +1. the global namespace +1. the builtin namespace + +If the name is not in any of these namespaces, the interpreter raises a `NameError`. + +This is called the **LEGB rule** (local, enclosing, global, builtin). + +Here's an example that helps to illustrate. + +Visualizations here are created by [nbtutor](https://github.com/lgpage/nbtutor) in a Jupyter notebook. + +They can help you better understand your program when you are learning a new language. + +Consider a script `test.py` that looks as follows + +```{code-cell} python3 +%%file test.py +def g(x): + a = 1 + x = x + a + return x + +a = 0 +y = g(10) +print("a = ", a, "y = ", y) +``` + +What happens when we run this script? + +```{code-cell} ipython +%run test.py +``` + +First, + +* The global namespace `{}` is created. + +```{figure} /_static/lecture_specific/oop_intro/global.png +:figclass: auto +``` + +* The function object is created, and `g` is bound to it within the global namespace. +* The name `a` is bound to `0`, again in the global namespace. + +```{figure} /_static/lecture_specific/oop_intro/global2.png +:figclass: auto +``` + +Next `g` is called via `y = g(10)`, leading to the following sequence of actions + +* The local namespace for the function is created. +* Local names `x` and `a` are bound, so that the local namespace becomes `{'x': 10, 'a': 1}`. + + * Note that the global `a` was not affected by the local `a`. +```{figure} /_static/lecture_specific/oop_intro/local1.png +:figclass: auto +``` + + + +* Statement `x = x + a` uses the local `a` and local `x` to compute `x + a`, and binds local name `x` to the result. + + +* This value is returned, and `y` is bound to it in the global namespace. +* Local `x` and `a` are discarded (and the local namespace is deallocated). + +```{figure} /_static/lecture_specific/oop_intro/local_return.png +:figclass: auto +``` + + +(mutable_vs_immutable)= +### {index}`Mutable ` Versus {index}`Immutable ` Parameters + +This is a good time to say a little more about mutable vs immutable objects. + +Consider the code segment + +```{code-cell} python3 +def f(x): + x = x + 1 + return x + +x = 1 +print(f(x), x) +``` + +We now understand what will happen here: The code prints `2` as the value of `f(x)` and `1` as the value of `x`. + +First `f` and `x` are registered in the global namespace. + +The call `f(x)` creates a local namespace and adds `x` to it, bound to `1`. + +Next, this local `x` is rebound to the new integer object `2`, and this value is returned. + +None of this affects the global `x`. + +However, it's a different story when we use a **mutable** data type such as a list + +```{code-cell} python3 +def f(x): + x[0] = x[0] + 1 + return x + +x = [1] +print(f(x), x) +``` + +This prints `[2]` as the value of `f(x)` and *same* for `x`. + +Here's what happens + +* `f` is registered as a function in the global namespace + +```{figure} /_static/lecture_specific/oop_intro/mutable1.png +:figclass: auto +``` + +* `x` bound to `[1]` in the global namespace + +```{figure} /_static/lecture_specific/oop_intro/mutable2.png +:figclass: auto +``` + +* The call `f(x)` + * Creates a local namespace + * Adds `x` to the local namespace, bound to `[1]` + +```{figure} /_static/lecture_specific/oop_intro/mutable3.png +:figclass: auto +``` + +```{note} +The global `x` and the local `x` refer to the same `[1]` +``` + +We can see the identity of local `x` and the identity of global `x` are the same + +```{code-cell} python3 +def f(x): + x[0] = x[0] + 1 + print(f'the identity of local x is {id(x)}') + return x + +x = [1] +print(f'the identity of global x is {id(x)}') +print(f(x), x) +``` + +* Within `f(x)` + * The list `[1]` is modified to `[2]` + * Returns the list `[2]` + +```{figure} /_static/lecture_specific/oop_intro/mutable4.png +:figclass: auto +``` +* The local namespace is deallocated, and the local `x` is lost + +```{figure} /_static/lecture_specific/oop_intro/mutable5.png +:figclass: auto +``` + +If you want to modify the local `x` and the global `x` separately, you can create a [*copy*](https://docs.python.org/3/library/copy.html) of the list and assign the copy to the local `x`. + +We will leave this for you to explore. + + +## Summary + +Messages in this lecture are clear: + + * In Python, *everything in memory is treated as an object*. + * Zero, one or many names can be bound to a given object. + * Every name resides within a scope defined by its namespace. + +This includes not just lists, strings, etc., but also less obvious things, such as + +* functions (once they have been read into memory) +* modules (ditto) +* files opened for reading or writing +* integers, etc. + +Consider, for example, functions. + +When Python reads a function definition, it creates a **function object** and stores it in memory. + +The following code illustrates further this idea + +```{code-cell} python3 +#reset the current namespace +%reset +``` + +```{code-cell} python3 +def f(x): return x**2 +f +``` + +```{code-cell} python3 +type(f) +``` + +```{code-cell} python3 +id(f) +``` + +```{code-cell} python3 +f.__name__ +``` + +We can see that `f` has type, identity, attributes and so on---just like any other object. + +It also has methods. + +One example is the `__call__` method, which just evaluates the function + +```{code-cell} python3 +f.__call__(3) +``` + +Another is the `__dir__` method, which returns a list of attributes. + +We can also find `f` our current namespace + +```{code-cell} python3 +'f' in dir() +``` + +Modules loaded into memory are also treated as objects + +```{code-cell} python3 +import math + +id(math) +``` + +We can find `math` in our global namespace after the import + +```{code-cell} python3 +print(dir()[-1::-1]) +``` + +We can also find all objects associated with the `math` module in the private namespace of `math` + +```{code-cell} python3 +print(dir(math)) +``` + +We can also directly import objects to our current namespace using `from ... import ...` + +```{code-cell} python3 +from math import log, pi, sqrt + +print(dir()[-1::-1]) +``` + +We can find these names appear in the current namespace now. + +*This uniform treatment of data in Python (everything is an object) helps keep the language simple and consistent.* + diff --git a/lectures/oop_intro.md b/lectures/oop_intro.md index d6043eca..ce056754 100644 --- a/lectures/oop_intro.md +++ b/lectures/oop_intro.md @@ -25,7 +25,7 @@ kernelspec: } -# OOP I: Objects and Names +# OOP I: Names and Objects ```{contents} Contents :depth: 2 @@ -33,9 +33,7 @@ kernelspec: ## Overview -[Object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming) (OOP) is one of the major paradigms in programming. - -The traditional programming paradigm (think Fortran, C, MATLAB, etc.) is called *procedural*. +The traditional programming paradigm (think Fortran, C, MATLAB, etc.) is called [procedural](https://en.wikipedia.org/wiki/Procedural_programming). It works as follows @@ -43,22 +41,34 @@ It works as follows * Functions are called to act on these data. * Data are passed back and forth via function calls. -In contrast, in the OOP paradigm +Two other important paradigms are [object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming) (OOP) and [functional programming](https://en.wikipedia.org/wiki/Functional_programming). + + +In the OOP paradigm data and functions are "bundled together" into "objects" (and functions in this context are referred to as **methods**). + +* think of a Python list that contains data and exposes methods such as `append()` and `count()` + +Functional programming languages are built on the idea of composing functions. + +* Influential examples include [Lisp](https://en.wikipedia.org/wiki/Common_Lisp), [Haskell](https://en.wikipedia.org/wiki/Haskell) and [Elixir](https://en.wikipedia.org/wiki/Elixir_(programming_language)). -* data and functions are "bundled together" into "objects" +So which of these categories does Python fit into? -(Functions in this context are referred to as **methods**) +Actually Python is a pragmatic language that blends object-oriented, functional and procedural styles, rather than taking a purist approach. -### Python and OOP +On one hand, this allows Python and its users to cherry pick nice aspects of different paradigms. -Python is a pragmatic language that blends object-oriented and procedural styles, rather than taking a purist approach. +On the other hand, the lack of purity might at times lead to some confusion. -However, at a foundational level, Python *is* object-oriented. +Fortunately this confusion is minimized if you understand that, at a foundational level, Python *is* object-oriented. -In particular, in Python, *everything is an object*. +By this we mean that, in Python, *everything is an object*. In this lecture, we explain what that statement means and why it matters. + + + ## Objects ```{index} single: Python; Objects @@ -891,7 +901,9 @@ print(dir()[-1::-1]) We can find these names appear in the current namespace now. -*This uniform treatment of data in Python (everything is an object) helps keep the language simple and consistent.* +This uniform treatment of data in Python (everything is an object) helps keep the language simple and consistent. + + ## Exercises @@ -900,7 +912,9 @@ We can find these names appear in the current namespace now. ``` We have met the {any}`boolean data type ` previously. -Using what we have learnt in this lecture, print a list of methods of boolean objects. + +Using what we have learnt in this lecture, print a list of methods of the +boolean object `True`. ```{hint} :class: dropdown From e41321b5c3016593ab36cc5cd0ca4b77fc2570a8 Mon Sep 17 00:00:00 2001 From: mmcky Date: Fri, 1 Mar 2024 14:37:36 +1100 Subject: [PATCH 02/10] fix non-unique reference labels --- lectures/mathfoo.py | 1 + lectures/mod.py | 1 + lectures/names.md | 15 ++------------- lectures/oop_intro.md | 15 ++------------- lectures/test.py | 8 ++++++++ 5 files changed, 14 insertions(+), 26 deletions(-) create mode 100644 lectures/mathfoo.py create mode 100644 lectures/mod.py create mode 100644 lectures/test.py diff --git a/lectures/mathfoo.py b/lectures/mathfoo.py new file mode 100644 index 00000000..c41e3d7a --- /dev/null +++ b/lectures/mathfoo.py @@ -0,0 +1 @@ +pi = 'foobar' diff --git a/lectures/mod.py b/lectures/mod.py new file mode 100644 index 00000000..1b0c806d --- /dev/null +++ b/lectures/mod.py @@ -0,0 +1 @@ +print(__name__) diff --git a/lectures/names.md b/lectures/names.md index 8e460ab5..b58635f2 100644 --- a/lectures/names.md +++ b/lectures/names.md @@ -9,7 +9,7 @@ kernelspec: name: python3 --- -(oop_intro)= +(oop_names)= ```{raw} jupyter ``` - - # OOP I: Names and Namespaces -```{contents} Contents -:depth: 2 -``` - ## Overview This lecture is all about variable names, how they can be used and how they are @@ -42,7 +31,7 @@ handling names is elegant and interesting. In addition, you will save yourself many hours of debugging if you have a good understanding of how names work in Python. -(name_res)= +(var_names)= ## Variable Names in Python ```{index} single: Python; Variable Names diff --git a/lectures/oop_intro.md b/lectures/oop_intro.md index ce056754..d7382d77 100644 --- a/lectures/oop_intro.md +++ b/lectures/oop_intro.md @@ -18,19 +18,8 @@ kernelspec: ``` - - # OOP I: Names and Objects -```{contents} Contents -:depth: 2 -``` - ## Overview The traditional programming paradigm (think Fortran, C, MATLAB, etc.) is called [procedural](https://en.wikipedia.org/wiki/Procedural_programming). @@ -714,8 +703,8 @@ Next `g` is called via `y = g(10)`, leading to the following sequence of actions ``` -(mutable_vs_immutable)= -### {index}`Mutable ` Versus {index}`Immutable ` Parameters +(mutable_vs_immutable2)= +### {index}`Mutable ` versus {index}`Immutable ` parameters This is a good time to say a little more about mutable vs immutable objects. diff --git a/lectures/test.py b/lectures/test.py new file mode 100644 index 00000000..71c4f375 --- /dev/null +++ b/lectures/test.py @@ -0,0 +1,8 @@ +def g(x): + a = 1 + x = x + a + return x + +a = 0 +y = g(10) +print("a = ", a, "y = ", y) From 7b12e8b6a67c966b856ba203d91b12adc33b2a0a Mon Sep 17 00:00:00 2001 From: mmcky Date: Fri, 1 Mar 2024 14:43:00 +1100 Subject: [PATCH 03/10] @mmcky tidy up --- lectures/names.md | 20 ++++---------------- lectures/oop_intro.md | 23 +++++------------------ 2 files changed, 9 insertions(+), 34 deletions(-) diff --git a/lectures/names.md b/lectures/names.md index b58635f2..44f486da 100644 --- a/lectures/names.md +++ b/lectures/names.md @@ -451,36 +451,30 @@ First, * The global namespace `{}` is created. ```{figure} /_static/lecture_specific/oop_intro/global.png -:figclass: auto ``` * The function object is created, and `g` is bound to it within the global namespace. * The name `a` is bound to `0`, again in the global namespace. ```{figure} /_static/lecture_specific/oop_intro/global2.png -:figclass: auto ``` Next `g` is called via `y = g(10)`, leading to the following sequence of actions * The local namespace for the function is created. * Local names `x` and `a` are bound, so that the local namespace becomes `{'x': 10, 'a': 1}`. +* Note that the global `a` was not affected by the local `a`. - * Note that the global `a` was not affected by the local `a`. ```{figure} /_static/lecture_specific/oop_intro/local1.png -:figclass: auto ``` * Statement `x = x + a` uses the local `a` and local `x` to compute `x + a`, and binds local name `x` to the result. - - * This value is returned, and `y` is bound to it in the global namespace. * Local `x` and `a` are discarded (and the local namespace is deallocated). ```{figure} /_static/lecture_specific/oop_intro/local_return.png -:figclass: auto ``` @@ -528,13 +522,11 @@ Here's what happens * `f` is registered as a function in the global namespace ```{figure} /_static/lecture_specific/oop_intro/mutable1.png -:figclass: auto ``` * `x` bound to `[1]` in the global namespace ```{figure} /_static/lecture_specific/oop_intro/mutable2.png -:figclass: auto ``` * The call `f(x)` @@ -542,7 +534,6 @@ Here's what happens * Adds `x` to the local namespace, bound to `[1]` ```{figure} /_static/lecture_specific/oop_intro/mutable3.png -:figclass: auto ``` ```{note} @@ -567,26 +558,23 @@ print(f(x), x) * Returns the list `[2]` ```{figure} /_static/lecture_specific/oop_intro/mutable4.png -:figclass: auto ``` * The local namespace is deallocated, and the local `x` is lost ```{figure} /_static/lecture_specific/oop_intro/mutable5.png -:figclass: auto ``` If you want to modify the local `x` and the global `x` separately, you can create a [*copy*](https://docs.python.org/3/library/copy.html) of the list and assign the copy to the local `x`. We will leave this for you to explore. - ## Summary Messages in this lecture are clear: - * In Python, *everything in memory is treated as an object*. - * Zero, one or many names can be bound to a given object. - * Every name resides within a scope defined by its namespace. +* In Python, *everything in memory is treated as an object*. +* Zero, one or many names can be bound to a given object. +* Every name resides within a scope defined by its namespace. This includes not just lists, strings, etc., but also less obvious things, such as diff --git a/lectures/oop_intro.md b/lectures/oop_intro.md index d7382d77..d949bfc1 100644 --- a/lectures/oop_intro.md +++ b/lectures/oop_intro.md @@ -670,36 +670,28 @@ First, * The global namespace `{}` is created. ```{figure} /_static/lecture_specific/oop_intro/global.png -:figclass: auto ``` * The function object is created, and `g` is bound to it within the global namespace. * The name `a` is bound to `0`, again in the global namespace. ```{figure} /_static/lecture_specific/oop_intro/global2.png -:figclass: auto ``` Next `g` is called via `y = g(10)`, leading to the following sequence of actions * The local namespace for the function is created. * Local names `x` and `a` are bound, so that the local namespace becomes `{'x': 10, 'a': 1}`. +* Note that the global `a` was not affected by the local `a`. - * Note that the global `a` was not affected by the local `a`. ```{figure} /_static/lecture_specific/oop_intro/local1.png -:figclass: auto ``` - - * Statement `x = x + a` uses the local `a` and local `x` to compute `x + a`, and binds local name `x` to the result. - - * This value is returned, and `y` is bound to it in the global namespace. * Local `x` and `a` are discarded (and the local namespace is deallocated). ```{figure} /_static/lecture_specific/oop_intro/local_return.png -:figclass: auto ``` @@ -747,13 +739,11 @@ Here's what happens * `f` is registered as a function in the global namespace ```{figure} /_static/lecture_specific/oop_intro/mutable1.png -:figclass: auto ``` * `x` bound to `[1]` in the global namespace ```{figure} /_static/lecture_specific/oop_intro/mutable2.png -:figclass: auto ``` * The call `f(x)` @@ -761,7 +751,6 @@ Here's what happens * Adds `x` to the local namespace, bound to `[1]` ```{figure} /_static/lecture_specific/oop_intro/mutable3.png -:figclass: auto ``` ```{note} @@ -786,12 +775,10 @@ print(f(x), x) * Returns the list `[2]` ```{figure} /_static/lecture_specific/oop_intro/mutable4.png -:figclass: auto ``` * The local namespace is deallocated, and the local `x` is lost ```{figure} /_static/lecture_specific/oop_intro/mutable5.png -:figclass: auto ``` If you want to modify the local `x` and the global `x` separately, you can create a [*copy*](https://docs.python.org/3/library/copy.html) of the list and assign the copy to the local `x`. @@ -803,9 +790,9 @@ We will leave this for you to explore. Messages in this lecture are clear: - * In Python, *everything in memory is treated as an object*. - * Zero, one or many names can be bound to a given object. - * Every name resides within a scope defined by its namespace. +* In Python, *everything in memory is treated as an object*. +* Zero, one or many names can be bound to a given object. +* Every name resides within a scope defined by its namespace. This includes not just lists, strings, etc., but also less obvious things, such as @@ -908,7 +895,7 @@ boolean object `True`. ```{hint} :class: dropdown - You can use `callable()` to test whether an attribute of an object can be called as a function +You can use `callable()` to test whether an attribute of an object can be called as a function ``` ```{exercise-end} From ca500f77b4f778b1a0b407451f76b6bc63089223 Mon Sep 17 00:00:00 2001 From: mmcky Date: Fri, 1 Mar 2024 14:44:53 +1100 Subject: [PATCH 04/10] ignore some build files --- .gitignore | 6 +++++- lectures/mathfoo.py | 1 - lectures/mod.py | 1 - lectures/test.py | 8 -------- 4 files changed, 5 insertions(+), 11 deletions(-) delete mode 100644 lectures/mathfoo.py delete mode 100644 lectures/mod.py delete mode 100644 lectures/test.py diff --git a/.gitignore b/.gitignore index 5a29c4d9..02d30563 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,8 @@ __pycache__/ dask-worker-space .vscode/ -.ipynb_checkpoints/ \ No newline at end of file +.ipynb_checkpoints/ + +lectures/mathfoo.py +lectures/mod.py +lectures/test.py \ No newline at end of file diff --git a/lectures/mathfoo.py b/lectures/mathfoo.py deleted file mode 100644 index c41e3d7a..00000000 --- a/lectures/mathfoo.py +++ /dev/null @@ -1 +0,0 @@ -pi = 'foobar' diff --git a/lectures/mod.py b/lectures/mod.py deleted file mode 100644 index 1b0c806d..00000000 --- a/lectures/mod.py +++ /dev/null @@ -1 +0,0 @@ -print(__name__) diff --git a/lectures/test.py b/lectures/test.py deleted file mode 100644 index 71c4f375..00000000 --- a/lectures/test.py +++ /dev/null @@ -1,8 +0,0 @@ -def g(x): - a = 1 - x = x + a - return x - -a = 0 -y = g(10) -print("a = ", a, "y = ", y) From 10b1b050e7ec020ccddd712839bce078549da829 Mon Sep 17 00:00:00 2001 From: John Stachurski Date: Fri, 1 Mar 2024 16:08:04 +1100 Subject: [PATCH 05/10] misc --- lectures/names.md | 91 ------- lectures/oop_intro.md | 617 +----------------------------------------- 2 files changed, 1 insertion(+), 707 deletions(-) diff --git a/lectures/names.md b/lectures/names.md index 44f486da..1ed69c06 100644 --- a/lectures/names.md +++ b/lectures/names.md @@ -568,96 +568,5 @@ If you want to modify the local `x` and the global `x` separately, you can creat We will leave this for you to explore. -## Summary -Messages in this lecture are clear: - -* In Python, *everything in memory is treated as an object*. -* Zero, one or many names can be bound to a given object. -* Every name resides within a scope defined by its namespace. - -This includes not just lists, strings, etc., but also less obvious things, such as - -* functions (once they have been read into memory) -* modules (ditto) -* files opened for reading or writing -* integers, etc. - -Consider, for example, functions. - -When Python reads a function definition, it creates a **function object** and stores it in memory. - -The following code illustrates further this idea - -```{code-cell} python3 -#reset the current namespace -%reset -``` - -```{code-cell} python3 -def f(x): return x**2 -f -``` - -```{code-cell} python3 -type(f) -``` - -```{code-cell} python3 -id(f) -``` - -```{code-cell} python3 -f.__name__ -``` - -We can see that `f` has type, identity, attributes and so on---just like any other object. - -It also has methods. - -One example is the `__call__` method, which just evaluates the function - -```{code-cell} python3 -f.__call__(3) -``` - -Another is the `__dir__` method, which returns a list of attributes. - -We can also find `f` our current namespace - -```{code-cell} python3 -'f' in dir() -``` - -Modules loaded into memory are also treated as objects - -```{code-cell} python3 -import math - -id(math) -``` - -We can find `math` in our global namespace after the import - -```{code-cell} python3 -print(dir()[-1::-1]) -``` - -We can also find all objects associated with the `math` module in the private namespace of `math` - -```{code-cell} python3 -print(dir(math)) -``` - -We can also directly import objects to our current namespace using `from ... import ...` - -```{code-cell} python3 -from math import log, pi, sqrt - -print(dir()[-1::-1]) -``` - -We can find these names appear in the current namespace now. - -*This uniform treatment of data in Python (everything is an object) helps keep the language simple and consistent.* diff --git a/lectures/oop_intro.md b/lectures/oop_intro.md index d949bfc1..737d6af7 100644 --- a/lectures/oop_intro.md +++ b/lectures/oop_intro.md @@ -18,7 +18,7 @@ kernelspec: ``` -# OOP I: Names and Objects +# OOP I: Objects and Methods ## Overview @@ -251,548 +251,12 @@ x (If you wanted to you could modify the `__setitem__` method, so that square bracket assignment does something totally different) -(name_res)= -## Names and Name Resolution - -### Variable Names in Python - -```{index} single: Python; Variable Names -``` - -Consider the Python statement - -```{code-cell} python3 -x = 42 -``` - -We now know that when this statement is executed, Python creates an object of -type `int` in your computer's memory, containing - -* the value `42` -* some associated attributes - -But what is `x` itself? - -In Python, `x` is called a *name*, and the statement `x = 42` *binds* the name `x` to the integer object we have just discussed. - -Under the hood, this process of binding names to objects is implemented as a dictionary---more about this in a moment. - -There is no problem binding two or more names to the one object, regardless of what that object is - -```{code-cell} python3 -def f(string): # Create a function called f - print(string) # that prints any string it's passed - -g = f -id(g) == id(f) -``` - -```{code-cell} python3 -g('test') -``` - -In the first step, a function object is created, and the name `f` is bound to it. - -After binding the name `g` to the same object, we can use it anywhere we would use `f`. - -What happens when the number of names bound to an object goes to zero? - -Here's an example of this situation, where the name `x` is first bound to one object and then rebound to another - -```{code-cell} python3 -x = 'foo' -id(x) -``` - -```{code-cell} python3 -x = 'bar' # No names bound to the first object -``` - -What happens here is that the first object is garbage collected. - -In other words, the memory slot that stores that object is deallocated, and returned to the operating system. - -Garbage collection is actually an active research area in computer science. - -You can [read more on garbage collection](https://rushter.com/blog/python-garbage-collector/) if you are interested. - -### Namespaces - -```{index} single: Python; Namespaces -``` - -Recall from the preceding discussion that the statement - -```{code-cell} python3 -x = 42 -``` - -binds the name `x` to the integer object on the right-hand side. - -We also mentioned that this process of binding `x` to the correct object is implemented as a dictionary. - -This dictionary is called a *namespace*. - -**Definition:** A namespace is a symbol table that maps names to objects in memory. - -Python uses multiple namespaces, creating them on the fly as necessary. - -For example, every time we import a module, Python creates a namespace for that module. - -To see this in action, suppose we write a script `mathfoo.py` with a single line - -```{code-cell} python3 -%%file mathfoo.py -pi = 'foobar' -``` - -Now we start the Python interpreter and import it - -```{code-cell} python3 -import mathfoo -``` - -Next let's import the `math` module from the standard library - -```{code-cell} python3 -import math -``` - -Both of these modules have an attribute called `pi` - -```{code-cell} python3 -math.pi -``` - -```{code-cell} python3 -mathfoo.pi -``` - -These two different bindings of `pi` exist in different namespaces, each one implemented as a dictionary. - -We can look at the dictionary directly, using `module_name.__dict__` - -```{code-cell} python3 -import math - -math.__dict__.items() -``` - -```{code-cell} python3 -import mathfoo - -mathfoo.__dict__.items() -``` - -As you know, we access elements of the namespace using the dotted attribute notation - -```{code-cell} python3 -math.pi -``` - -In fact this is entirely equivalent to `math.__dict__['pi']` - -```{code-cell} python3 -math.__dict__['pi'] == math.pi -``` - -### Viewing Namespaces - -As we saw above, the `math` namespace can be printed by typing `math.__dict__`. - -Another way to see its contents is to type `vars(math)` - -```{code-cell} python3 -vars(math).items() -``` - -If you just want to see the names, you can type - -```{code-cell} python3 -# Show the first 10 names -dir(math)[0:10] -``` - -Notice the special names `__doc__` and `__name__`. - -These are initialized in the namespace when any module is imported - -* `__doc__` is the doc string of the module -* `__name__` is the name of the module - -```{code-cell} python3 -print(math.__doc__) -``` - -```{code-cell} python3 -math.__name__ -``` - -### Interactive Sessions - -```{index} single: Python; Interpreter -``` - -In Python, **all** code executed by the interpreter runs in some module. - -What about commands typed at the prompt? - -These are also regarded as being executed within a module --- in this case, a module called `__main__`. - -To check this, we can look at the current module name via the value of `__name__` given at the prompt - -```{code-cell} python3 -print(__name__) -``` - -When we run a script using IPython's `run` command, the contents of the file are executed as part of `__main__` too. - -To see this, let's create a file `mod.py` that prints its own `__name__` attribute - -```{code-cell} ipython -%%file mod.py -print(__name__) -``` - -Now let's look at two different ways of running it in IPython - -```{code-cell} python3 -import mod # Standard import -``` - -```{code-cell} ipython -%run mod.py # Run interactively -``` - -In the second case, the code is executed as part of `__main__`, so `__name__` is equal to `__main__`. - -To see the contents of the namespace of `__main__` we use `vars()` rather than `vars(__main__)`. - -If you do this in IPython, you will see a whole lot of variables that IPython -needs, and has initialized when you started up your session. - -If you prefer to see only the variables you have initialized, use `%whos` - -```{code-cell} ipython -x = 2 -y = 3 - -import numpy as np - -%whos -``` - -### The Global Namespace - -```{index} single: Python; Namespace (Global) -``` - -Python documentation often makes reference to the "global namespace". - -The global namespace is *the namespace of the module currently being executed*. - -For example, suppose that we start the interpreter and begin making assignments. - -We are now working in the module `__main__`, and hence the namespace for `__main__` is the global namespace. - -Next, we import a module called `amodule` - -```{code-block} python3 -:class: no-execute - -import amodule -``` - -At this point, the interpreter creates a namespace for the module `amodule` and starts executing commands in the module. - -While this occurs, the namespace `amodule.__dict__` is the global namespace. - -Once execution of the module finishes, the interpreter returns to the module from where the import statement was made. - -In this case it's `__main__`, so the namespace of `__main__` again becomes the global namespace. - -### Local Namespaces - -```{index} single: Python; Namespace (Local) -``` - -Important fact: When we call a function, the interpreter creates a *local namespace* for that function, and registers the variables in that namespace. - -The reason for this will be explained in just a moment. - -Variables in the local namespace are called *local variables*. - -After the function returns, the namespace is deallocated and lost. - -While the function is executing, we can view the contents of the local namespace with `locals()`. - -For example, consider - -```{code-cell} python3 -def f(x): - a = 2 - print(locals()) - return a * x -``` - -Now let's call the function - -```{code-cell} python3 -f(1) -``` - -You can see the local namespace of `f` before it is destroyed. - -### The `__builtins__` Namespace - -```{index} single: Python; Namespace (__builtins__) -``` - -We have been using various built-in functions, such as `max(), dir(), str(), list(), len(), range(), type()`, etc. - -How does access to these names work? - -* These definitions are stored in a module called `__builtin__`. -* They have their own namespace called `__builtins__`. - -```{code-cell} python3 -# Show the first 10 names in `__main__` -dir()[0:10] -``` - -```{code-cell} python3 -# Show the first 10 names in `__builtins__` -dir(__builtins__)[0:10] -``` - -We can access elements of the namespace as follows - -```{code-cell} python3 -__builtins__.max -``` - -But `__builtins__` is special, because we can always access them directly as well - -```{code-cell} python3 -max -``` - -```{code-cell} python3 -__builtins__.max == max -``` - -The next section explains how this works ... - -### Name Resolution - -```{index} single: Python; Namespace (Resolution) -``` - -Namespaces are great because they help us organize variable names. - -(Type `import this` at the prompt and look at the last item that's printed) - -However, we do need to understand how the Python interpreter works with multiple namespaces. - -Understanding the flow of execution will help us to check which variables are in scope and how to operate on them when writing and debugging programs. - - -At any point of execution, there are in fact at least two namespaces that can be accessed directly. - -("Accessed directly" means without using a dot, as in `pi` rather than `math.pi`) - -These namespaces are - -* The global namespace (of the module being executed) -* The builtin namespace - -If the interpreter is executing a function, then the directly accessible namespaces are - -* The local namespace of the function -* The global namespace (of the module being executed) -* The builtin namespace - -Sometimes functions are defined within other functions, like so - -```{code-cell} python3 -def f(): - a = 2 - def g(): - b = 4 - print(a * b) - g() -``` - -Here `f` is the *enclosing function* for `g`, and each function gets its -own namespaces. - -Now we can give the rule for how namespace resolution works: - -The order in which the interpreter searches for names is - -1. the local namespace (if it exists) -1. the hierarchy of enclosing namespaces (if they exist) -1. the global namespace -1. the builtin namespace - -If the name is not in any of these namespaces, the interpreter raises a `NameError`. - -This is called the **LEGB rule** (local, enclosing, global, builtin). - -Here's an example that helps to illustrate. - -Visualizations here are created by [nbtutor](https://github.com/lgpage/nbtutor) in a Jupyter notebook. - -They can help you better understand your program when you are learning a new language. - -Consider a script `test.py` that looks as follows - -```{code-cell} python3 -%%file test.py -def g(x): - a = 1 - x = x + a - return x - -a = 0 -y = g(10) -print("a = ", a, "y = ", y) -``` - -What happens when we run this script? - -```{code-cell} ipython -%run test.py -``` - -First, - -* The global namespace `{}` is created. - -```{figure} /_static/lecture_specific/oop_intro/global.png -``` - -* The function object is created, and `g` is bound to it within the global namespace. -* The name `a` is bound to `0`, again in the global namespace. - -```{figure} /_static/lecture_specific/oop_intro/global2.png -``` - -Next `g` is called via `y = g(10)`, leading to the following sequence of actions - -* The local namespace for the function is created. -* Local names `x` and `a` are bound, so that the local namespace becomes `{'x': 10, 'a': 1}`. -* Note that the global `a` was not affected by the local `a`. - -```{figure} /_static/lecture_specific/oop_intro/local1.png -``` - -* Statement `x = x + a` uses the local `a` and local `x` to compute `x + a`, and binds local name `x` to the result. -* This value is returned, and `y` is bound to it in the global namespace. -* Local `x` and `a` are discarded (and the local namespace is deallocated). - -```{figure} /_static/lecture_specific/oop_intro/local_return.png -``` - - -(mutable_vs_immutable2)= -### {index}`Mutable ` versus {index}`Immutable ` parameters - -This is a good time to say a little more about mutable vs immutable objects. - -Consider the code segment - -```{code-cell} python3 -def f(x): - x = x + 1 - return x - -x = 1 -print(f(x), x) -``` - -We now understand what will happen here: The code prints `2` as the value of `f(x)` and `1` as the value of `x`. - -First `f` and `x` are registered in the global namespace. - -The call `f(x)` creates a local namespace and adds `x` to it, bound to `1`. - -Next, this local `x` is rebound to the new integer object `2`, and this value is returned. - -None of this affects the global `x`. - -However, it's a different story when we use a **mutable** data type such as a list - -```{code-cell} python3 -def f(x): - x[0] = x[0] + 1 - return x - -x = [1] -print(f(x), x) -``` - -This prints `[2]` as the value of `f(x)` and *same* for `x`. - -Here's what happens - -* `f` is registered as a function in the global namespace - -```{figure} /_static/lecture_specific/oop_intro/mutable1.png -``` - -* `x` bound to `[1]` in the global namespace - -```{figure} /_static/lecture_specific/oop_intro/mutable2.png -``` - -* The call `f(x)` - * Creates a local namespace - * Adds `x` to the local namespace, bound to `[1]` - -```{figure} /_static/lecture_specific/oop_intro/mutable3.png -``` - -```{note} -The global `x` and the local `x` refer to the same `[1]` -``` - -We can see the identity of local `x` and the identity of global `x` are the same - -```{code-cell} python3 -def f(x): - x[0] = x[0] + 1 - print(f'the identity of local x is {id(x)}') - return x - -x = [1] -print(f'the identity of global x is {id(x)}') -print(f(x), x) -``` - -* Within `f(x)` - * The list `[1]` is modified to `[2]` - * Returns the list `[2]` - -```{figure} /_static/lecture_specific/oop_intro/mutable4.png -``` -* The local namespace is deallocated, and the local `x` is lost - -```{figure} /_static/lecture_specific/oop_intro/mutable5.png -``` - -If you want to modify the local `x` and the global `x` separately, you can create a [*copy*](https://docs.python.org/3/library/copy.html) of the list and assign the copy to the local `x`. - -We will leave this for you to explore. - - ## Summary Messages in this lecture are clear: * In Python, *everything in memory is treated as an object*. * Zero, one or many names can be bound to a given object. -* Every name resides within a scope defined by its namespace. This includes not just lists, strings, etc., but also less obvious things, such as @@ -801,84 +265,6 @@ This includes not just lists, strings, etc., but also less obvious things, such * files opened for reading or writing * integers, etc. -Consider, for example, functions. - -When Python reads a function definition, it creates a **function object** and stores it in memory. - -The following code illustrates further this idea - -```{code-cell} python3 -#reset the current namespace -%reset -``` - -```{code-cell} python3 -def f(x): return x**2 -f -``` - -```{code-cell} python3 -type(f) -``` - -```{code-cell} python3 -id(f) -``` - -```{code-cell} python3 -f.__name__ -``` - -We can see that `f` has type, identity, attributes and so on---just like any other object. - -It also has methods. - -One example is the `__call__` method, which just evaluates the function - -```{code-cell} python3 -f.__call__(3) -``` - -Another is the `__dir__` method, which returns a list of attributes. - -We can also find `f` our current namespace - -```{code-cell} python3 -'f' in dir() -``` - -Modules loaded into memory are also treated as objects - -```{code-cell} python3 -import math - -id(math) -``` - -We can find `math` in our global namespace after the import - -```{code-cell} python3 -print(dir()[-1::-1]) -``` - -We can also find all objects associated with the `math` module in the private namespace of `math` - -```{code-cell} python3 -print(dir(math)) -``` - -We can also directly import objects to our current namespace using `from ... import ...` - -```{code-cell} python3 -from math import log, pi, sqrt - -print(dir()[-1::-1]) -``` - -We can find these names appear in the current namespace now. - -This uniform treatment of data in Python (everything is an object) helps keep the language simple and consistent. - ## Exercises @@ -946,7 +332,6 @@ Here is a one-line solution print([i for i in attrls if callable(eval(f'True.{i}'))]) ``` -You can explore these methods and see what they are used for. ```{solution-end} ``` From 5bdc0db3fcc62e49b58e42bfd942d62d31555d68 Mon Sep 17 00:00:00 2001 From: John Stachurski Date: Fri, 1 Mar 2024 16:09:41 +1100 Subject: [PATCH 06/10] misc --- lectures/names.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lectures/names.md b/lectures/names.md index 1ed69c06..24909d49 100644 --- a/lectures/names.md +++ b/lectures/names.md @@ -463,13 +463,13 @@ Next `g` is called via `y = g(10)`, leading to the following sequence of actions * The local namespace for the function is created. * Local names `x` and `a` are bound, so that the local namespace becomes `{'x': 10, 'a': 1}`. -* Note that the global `a` was not affected by the local `a`. + +Note that the global `a` was not affected by the local `a`. ```{figure} /_static/lecture_specific/oop_intro/local1.png ``` - * Statement `x = x + a` uses the local `a` and local `x` to compute `x + a`, and binds local name `x` to the result. * This value is returned, and `y` is bound to it in the global namespace. * Local `x` and `a` are discarded (and the local namespace is deallocated). From 6736b565ade489f442b5e1421a0bdbd781ea8f8b Mon Sep 17 00:00:00 2001 From: John Stachurski Date: Sat, 2 Mar 2024 05:45:45 +1100 Subject: [PATCH 07/10] misc --- lectures/names.md | 2 +- lectures/oop_intro.md | 118 ++++++++++++++++++++++++++++++++---------- 2 files changed, 93 insertions(+), 27 deletions(-) diff --git a/lectures/names.md b/lectures/names.md index 24909d49..dbecf57f 100644 --- a/lectures/names.md +++ b/lectures/names.md @@ -18,7 +18,7 @@ kernelspec: ``` -# OOP I: Names and Namespaces +# Names and Namespaces ## Overview diff --git a/lectures/oop_intro.md b/lectures/oop_intro.md index 737d6af7..a9bcdd00 100644 --- a/lectures/oop_intro.md +++ b/lectures/oop_intro.md @@ -27,15 +27,17 @@ The traditional programming paradigm (think Fortran, C, MATLAB, etc.) is called It works as follows * The program has a state corresponding to the values of its variables. -* Functions are called to act on these data. -* Data are passed back and forth via function calls. +* Functions are called to act on and transform the state. +* Final outputs are produced via a sequence of function calls. Two other important paradigms are [object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming) (OOP) and [functional programming](https://en.wikipedia.org/wiki/Functional_programming). -In the OOP paradigm data and functions are "bundled together" into "objects" (and functions in this context are referred to as **methods**). +In the OOP paradigm, data and functions are bundled together into "objects" --- and functions in this context are referred to as **methods**. -* think of a Python list that contains data and exposes methods such as `append()` and `count()` +Methods are called on to transform the data contained in the object. + +* Think of a Python list that contains data and has methods such as `append()` and `pop()` that transform the data. Functional programming languages are built on the idea of composing functions. @@ -55,7 +57,12 @@ By this we mean that, in Python, *everything is an object*. In this lecture, we explain what that statement means and why it matters. +We'll make use of the following third party library + +```{code-cell} python3 +!pip install rich +``` ## Objects @@ -200,7 +207,7 @@ These attributes are important, so let's discuss them in-depth. Methods are *functions that are bundled with objects*. -Formally, methods are attributes of objects that are callable (i.e., can be called as functions) +Formally, methods are attributes of objects that are **callable** -- i.e., attributes that can be called as functions ```{code-cell} python3 x = ['foo', 'bar'] @@ -250,13 +257,82 @@ x (If you wanted to you could modify the `__setitem__` method, so that square bracket assignment does something totally different) +## Inspection Using Rich + +There's a nice package called [rich](https://github.com/Textualize/rich) that +helps us view the contents of an object. + +For example, + +```{code-cell} python3 +from rich import inspect +x = 10 +inspect(10) +``` +If we want to see the methods as well, we can use + +```{code-cell} python3 +inspect(10, methods=True) +``` + +In fact there are still more methods, as you can see if you execute `inspect(10, all=True)`. + +## A Little Mystery + +In this lecture we claimed that Python is object oriented. + +But here's an example that looks more procedural. + +```{code-cell} python3 +x = ['a', 'b'] +m = len(x) +m +``` + +If Python is object oriented, why don't we use `x.len()`? Isn't this +inconsistent? + +The answers are related to the fact that Python aims for consistent style. + +In Python, it is common for users to build custom objects --- we discuss how to +do this [later](python_oop). + +It's quite common for users to add methods to their that measure the length of +the object, suitably defined. + +When naming such a method, natural choices are `len()` and `length()`. + +If some users choose `len()` and others choose `length()`, then the style will +be inconsistent and harder to remember. + +To avoid this, the creator of Python chose to have some built-in functions +like `len()`, to make clear that `len()` is the convention. + +Now, having said all of this, Python still is object oriented under the hood. + +In fact, the list `x` discussed above has a method called `__len__()`. + +All that the function `len()` does is call this method. + +In other words, the following code is equivalent: + +```{code-cell} python3 +x = ['a', 'b'] +len(x) +``` +and + +```{code-cell} python3 +x = ['a', 'b'] +x.__len__() +``` + ## Summary -Messages in this lecture are clear: +The message in this lecture is clear: * In Python, *everything in memory is treated as an object*. -* Zero, one or many names can be bound to a given object. This includes not just lists, strings, etc., but also less obvious things, such as @@ -291,47 +367,37 @@ You can use `callable()` to test whether an attribute of an object can be called :class: dropdown ``` -Firstly, we need to find all attributes of a boolean object. - -You can use one of the following ways: - -*1.* You can call the `.__dir__()` method +Firstly, we need to find all attributes of `True`, which can be done via ```{code-cell} python3 print(sorted(True.__dir__())) ``` -*2.* You can use the built-in function `dir()` +or ```{code-cell} python3 print(sorted(dir(True))) ``` -*3.* Since the boolean data type is a primitive type, you can also find it in the built-in namespace +Since the boolean data type is a primitive type, you can also find it in the built-in namespace ```{code-cell} python3 print(dir(__builtins__.bool)) ``` -Next, we can use a `for` loop to filter out attributes that are callable +Here we use a `for` loop to filter out attributes that are callable ```{code-cell} python3 -attrls = dir(__builtins__.bool) -callablels = list() +attributes = dir(__builtins__.bool) +callablels = [] -for i in attrls: +for attribute in attributes: # Use eval() to evaluate a string as an expression - if callable(eval(f'True.{i}')): - callablels.append(i) + if callable(eval(f'True.{attribute}')): + callablels.append(attribute) print(callablels) ``` -Here is a one-line solution - -```{code-cell} python3 -print([i for i in attrls if callable(eval(f'True.{i}'))]) -``` - ```{solution-end} ``` From 50d0c0175f1a8eae5573a3de806f7327f9499f7d Mon Sep 17 00:00:00 2001 From: mmcky Date: Sat, 2 Mar 2024 08:44:43 +1100 Subject: [PATCH 08/10] update link to doc --- lectures/oop_intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lectures/oop_intro.md b/lectures/oop_intro.md index a9bcdd00..0eb1275b 100644 --- a/lectures/oop_intro.md +++ b/lectures/oop_intro.md @@ -295,7 +295,7 @@ inconsistent? The answers are related to the fact that Python aims for consistent style. In Python, it is common for users to build custom objects --- we discuss how to -do this [later](python_oop). +do this {doc}`lecture `. It's quite common for users to add methods to their that measure the length of the object, suitably defined. From 5fe985b03fd2a4b0bda2158d227086931cbd683f Mon Sep 17 00:00:00 2001 From: John Stachurski Date: Sun, 3 Mar 2024 19:38:44 +1100 Subject: [PATCH 09/10] Update oop_intro.md --- lectures/oop_intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lectures/oop_intro.md b/lectures/oop_intro.md index 0eb1275b..730e1df8 100644 --- a/lectures/oop_intro.md +++ b/lectures/oop_intro.md @@ -295,7 +295,7 @@ inconsistent? The answers are related to the fact that Python aims for consistent style. In Python, it is common for users to build custom objects --- we discuss how to -do this {doc}`lecture `. +do this [later](python_oop)`. It's quite common for users to add methods to their that measure the length of the object, suitably defined. From fe42b933280691390a2a4741b7bcbccaa86a5c2b Mon Sep 17 00:00:00 2001 From: John Stachurski Date: Sun, 3 Mar 2024 19:47:07 +1100 Subject: [PATCH 10/10] misc --- lectures/names.md | 22 +++++++++++----------- lectures/oop_intro.md | 20 +++++++++++--------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/lectures/names.md b/lectures/names.md index dbecf57f..3ce66b14 100644 --- a/lectures/names.md +++ b/lectures/names.md @@ -75,20 +75,20 @@ After binding the name `g` to the same object, we can use it anywhere we would u What happens when the number of names bound to an object goes to zero? -Here's an example of this situation, where the name `x` is first bound to one object and then rebound to another +Here's an example of this situation, where the name `x` is first bound to one object and then **rebound** to another ```{code-cell} python3 x = 'foo' id(x) +x = 'bar' +id(x) ``` -```{code-cell} python3 -x = 'bar' # No names bound to the first object -``` +In this case, after we rebind `x` to `'bar'`, no names bound are to the first object `'foo'`. -What happens here is that the first object is garbage collected. +This is a trigger for `'foo'` to be garbage collected. -In other words, the memory slot that stores that object is deallocated, and returned to the operating system. +In other words, the memory slot that stores that object is deallocated and returned to the operating system. Garbage collection is actually an active research area in computer science. @@ -151,7 +151,7 @@ mathfoo.pi These two different bindings of `pi` exist in different namespaces, each one implemented as a dictionary. -We can look at the dictionary directly, using `module_name.__dict__` +If you wish, you can look at the dictionary directly, using `module_name.__dict__`. ```{code-cell} python3 import math @@ -162,7 +162,7 @@ math.__dict__.items() ```{code-cell} python3 import mathfoo -mathfoo.__dict__.items() +mathfoo.__dict__ ``` As you know, we access elements of the namespace using the dotted attribute notation @@ -171,10 +171,10 @@ As you know, we access elements of the namespace using the dotted attribute nota math.pi ``` -In fact this is entirely equivalent to `math.__dict__['pi']` +This is entirely equivalent to `math.__dict__['pi']` ```{code-cell} python3 -math.__dict__['pi'] == math.pi +math.__dict__['pi'] ``` ## Viewing Namespaces @@ -524,7 +524,7 @@ Here's what happens ```{figure} /_static/lecture_specific/oop_intro/mutable1.png ``` -* `x` bound to `[1]` in the global namespace +* `x` is bound to `[1]` in the global namespace ```{figure} /_static/lecture_specific/oop_intro/mutable2.png ``` diff --git a/lectures/oop_intro.md b/lectures/oop_intro.md index 730e1df8..708f1ee9 100644 --- a/lectures/oop_intro.md +++ b/lectures/oop_intro.md @@ -277,9 +277,11 @@ inspect(10, methods=True) In fact there are still more methods, as you can see if you execute `inspect(10, all=True)`. + + ## A Little Mystery -In this lecture we claimed that Python is object oriented. +In this lecture we claimed that Python is, at heart, an object oriented language. But here's an example that looks more procedural. @@ -289,13 +291,12 @@ m = len(x) m ``` -If Python is object oriented, why don't we use `x.len()`? Isn't this -inconsistent? +If Python is object oriented, why don't we use `x.len()`? -The answers are related to the fact that Python aims for consistent style. +The answer is related to the fact that Python aims for readability and consistent style. In Python, it is common for users to build custom objects --- we discuss how to -do this [later](python_oop)`. +do this {doc}`later `. It's quite common for users to add methods to their that measure the length of the object, suitably defined. @@ -305,10 +306,10 @@ When naming such a method, natural choices are `len()` and `length()`. If some users choose `len()` and others choose `length()`, then the style will be inconsistent and harder to remember. -To avoid this, the creator of Python chose to have some built-in functions -like `len()`, to make clear that `len()` is the convention. +To avoid this, the creator of Python chose to add +`len()` as a built-in function, to help emphasize that `len()` is the convention. -Now, having said all of this, Python still is object oriented under the hood. +Now, having said all of this, Python *is* still object oriented under the hood. In fact, the list `x` discussed above has a method called `__len__()`. @@ -341,7 +342,8 @@ This includes not just lists, strings, etc., but also less obvious things, such * files opened for reading or writing * integers, etc. - +Remember that everything is an object will help you interact with your programs +and write clear Pythonic code. ## Exercises