diff --git a/introductory_courses/python/08_lists.md b/introductory_courses/python/08_lists.md index cdc577f8..ef2b945c 100644 --- a/introductory_courses/python/08_lists.md +++ b/introductory_courses/python/08_lists.md @@ -148,7 +148,7 @@ x = [['pepper', 'zucchini', 'onion'], Here is a visual example of how indexing a list of lists `x` works: -[![x is represented as a pepper shaker containing several packets of pepper. [x[0]] is represented as a pepper shaker containing a single packet of pepper. x[0] is represented as a single packet of pepper. x\[0]\[0] is represented as single grain of pepper. Adapted from @hadleywickham.](fig/indexing_lists_python.png)](https://twitter.com/hadleywickham/status/643381054758363136) +[![x is represented as a pepper shaker containing several packets of pepper. [x[0]] is represented as a pepper shaker containing a single packet of pepper. x[0] is represented as a single packet of pepper. x\[0]\[0] is represented as single grain of pepper. Adapted from @hadleywickham.](fig/indexing_lists_python.png)]() Using the previously declared list `x`, these would be the results of the index operations shown in the image: diff --git a/software_architecture_and_design/functional/higher_order_functions_cpp.md b/software_architecture_and_design/functional/higher_order_functions_cpp.md index 2a4c5010..1b28f021 100644 --- a/software_architecture_and_design/functional/higher_order_functions_cpp.md +++ b/software_architecture_and_design/functional/higher_order_functions_cpp.md @@ -1,24 +1,20 @@ --- name: Higher Order Functions -dependsOn: [ - software_architecture_and_design.functional.side_effects_cpp, -] +dependsOn: [software_architecture_and_design.functional.side_effects_cpp] tags: [cpp] -attribution: - - citation: > - This material was adapted from an "Introduction to C++" course developed by the - Oxford RSE group. - url: https://www.rse.ox.ac.uk - image: https://www.rse.ox.ac.uk/images/banner_ox_rse.svg - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - +attribution: + - citation: > + This material was adapted from an "Introduction to C++" course developed by the + Oxford RSE group. + url: https://www.rse.ox.ac.uk + image: https://www.rse.ox.ac.uk/images/banner_ox_rse.svg + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- - ## First Class Functions Languages that treat functions as first-class citizens allow functions to be @@ -27,7 +23,7 @@ variables. In C++ this is typically done via lamda functions or function objects ### Lambda Functions -*Lambda functions* are small, nameless functions which are defined in the +_Lambda functions_ are small, nameless functions which are defined in the normal flow of the program, typically as they are needed. They consist of three part, delimited by square, round, then curly brackets. The curly brackets form the body of the function, for example @@ -147,7 +143,7 @@ for (const auto& op: ops) { std::cout << result << std::end; // prints 6 ``` -`std::function` is an example of *type erasure*. +`std::function` is an example of _type erasure_. ## Higher Order Functions @@ -191,17 +187,17 @@ int reduce(const std::vector& data, std::function bin_op) { int main() { std::vector data = {1, 2, 3, 4, -1}; - std::cout << reduce(data, std::plus()) << std::endl; - std::cout << reduce(data, std::multiplies()) << std::endl; - std::cout << reduce(data, [](int a, int b) { return std::max(a, b); }) << std::endl; - std::cout << reduce(data, [](int a, int b) { return std::min(a, b); }) << std::endl; + std::cout << reduce(data, std::plus()) << std::endl; + std::cout << reduce(data, std::multiplies()) << std::endl; + std::cout << reduce(data, [](int a, int b) { return std::max(a, b); }) << std::endl; + std::cout << reduce(data, [](int a, int b) { return std::min(a, b); }) << std::endl; } ``` Excellent! We have reduced the amount of code we need to write, reducing the number of possible bugs and making the code easier to maintain in the future. -C++ actually has a `std::reduce`, which is part of the *algorithms* standard library. +C++ actually has a `std::reduce`, which is part of the _algorithms_ standard library. ### The Algorithms Library @@ -213,7 +209,7 @@ recognising their conceptual similarities. Using the algorithms library means: (a) you reduce the amount of (algorithmic) code you need to write, reducing bugs and increasing maintainability (b) you make clear to the reader what your code is doing, since these are commonly used algorithms (b) you benifit from bullet proof, efficient implementations written by the same teams that write the compiler you are using -(c) you can benifit from *executors* to instantly parallise or vectorise your code for high performance. +(c) you can benifit from _executors_ to instantly parallise or vectorise your code for high performance. Lets go through a few examples inspired by the common functional algorithms "map", "filter" and "reduce" (also the inspiration for the MapReduce @@ -225,13 +221,13 @@ First the map, or `std::transform`: std::vector data = {1.0, 2.0, -1.1, 5.0}; // transform in-place -std::transform(std::begin(data), std::end(data), std::begin(data), +std::transform(std::begin(data), std::end(data), std::begin(data), [](const double& x) { return 2.0 * x; } ); std::vector new_data(data.size()); // transform to a new collection -std::transform(std::begin(data), std::end(data), std::begin(new_data), +std::transform(std::begin(data), std::end(data), std::begin(new_data), [](const double& x) { return 3.14 * std::pow(x, 2); } ); ``` @@ -259,7 +255,7 @@ bool is_prime(int n) { } int main() { - std::vector data(1000); + std::vector data(1000); std::iota(data.begin(), data.end(), 1); // fill with numbers 1 -> 1000 std::copy_if(data.begin(), data.end(), std::ostream_iterator(std::cout, " "), @@ -278,7 +274,7 @@ maximum elements of an vector. At the same time we introduce another algorithm `std::generate`, which assigns values to a range based on a generator function, and some of the random number generation options in the standard library. -``` cpp +```cpp #include #include #include @@ -295,7 +291,7 @@ int main() { std::normal_distribution dist(5, 2); auto gen_random = [&]() { return dist(gen);}; - std::vector data(1000); + std::vector data(1000); std::generate(data.begin(), data.end(), gen_random); auto calc_min_max = [](std::tuple acc, double x) { @@ -314,7 +310,7 @@ int main() { Use `std::accumulate` to write a function that calculates the sum of the squares of the values in a vector. Your function should behave as below: -``` cpp +```cpp std::cout << sum_of_squares({0}) << std::endl; std::cout << sum_of_squares({1, 3, -2}) << std::endl; ``` @@ -343,7 +339,7 @@ int sum_of_squares(const std::vector& data) { Now let's assume we're reading in these numbers from an input file, so they arrive as a list of strings. Write a new function `map_str_to_int` using `std::transform` that passes the following tests: -``` cpp +```cpp std::cout << sum_of_squares(map_str_to_int({"1", "2", "3"})) << std::endl; std::cout << sum_of_squares(map_str_to_int({"-1", "-2", "-3"})) << std::endl; ``` @@ -369,7 +365,7 @@ const std::vector map_str_to_int(const std::vector& data) { Finally, we'd like it to be possible for users to comment out numbers in the input file they give to our program. Extend your `map_str_to_int` function so that the following tests pass: -``` cpp +```cpp std::cout << sum_of_squares(map_str_to_int({"1", "2", "3"})) << std::endl; std::cout << sum_of_squares(map_str_to_int({"1", "2", "#100", "3"})) << std::endl; ``` @@ -408,13 +404,13 @@ std::vector map_str_to_int(const std::vector& data) { } new_data.push_back(std::atoi(x.c_str())); } - return new_data; + return new_data; } ``` Here you can start to see a limitation of the algorithms in the standard library, in that it is difficult to efficiently compose together multiple -elemental algorithms into more complex algorithm. The *ranges* library is an +elemental algorithms into more complex algorithm. The _ranges_ library is an C++20 addition to the standard library aims to solve this problem, you can read more about the ranges library [here](https://en.cppreference.com/w/cpp/ranges). diff --git a/software_architecture_and_design/functional/higher_order_functions_python.md b/software_architecture_and_design/functional/higher_order_functions_python.md index 7aaca630..fdcece3c 100644 --- a/software_architecture_and_design/functional/higher_order_functions_python.md +++ b/software_architecture_and_design/functional/higher_order_functions_python.md @@ -1,19 +1,16 @@ --- name: Higher Order Functions -dependsOn: [ - software_architecture_and_design.functional.side_effects_python, -] +dependsOn: [software_architecture_and_design.functional.side_effects_python] tags: [python] -attribution: - - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. - url: https://www.sabsr3.ox.ac.uk - image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - +attribution: + - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. + url: https://www.sabsr3.ox.ac.uk + image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- ## First Class Functions @@ -24,7 +21,7 @@ variables. This is a powerful feature of functional programming languages, and i In Python, functions are first-class citizens, which means that they can be passed to other functions as arguments, for example: -``` python +```python def add_one(x): return x + 1 @@ -40,15 +37,15 @@ print(apply_function(add_one, 1)) ## Lambda Functions -*Lambda functions* are small, nameless functions which are defined in the +_Lambda functions_ are small, nameless functions which are defined in the normal flow of the program, typically as they are needed. The structure of these functions is not dissimilar to a normal python function definition - we have a -keyword `lambda`, a list of parameters, a colon, then the function body. In +keyword `lambda`, a list of parameters, a colon, then the function body. In Python, the function body is limited to a single expression, which becomes the return value. -``` python -add_one = lambda x: x + 1 +```python +add_one = lambda x: x + 1 # NOQA E731 print(add_one(1)) ``` @@ -127,14 +124,14 @@ number of possible bugs and making the code easier to maintain in the future. Python has a number of higher order functions built in, including `map`, `filter` and `reduce`. Note that the `map` and `filter` functions in Python use -**lazy evaluation**. This means that values in an iterable collection are not -actually calculated until you need them. We'll explain some of the implications +**lazy evaluation**. This means that values in an iterable collection are not +actually calculated until you need them. We'll explain some of the implications of this a little later, but for now, we'll just use `list()` to convert the -results to a normal list. In these examples we also see the more typical usage +results to a normal list. In these examples we also see the more typical usage of lambda functions. The `map` function, takes a function and applies it to each value in an -**iterable**. Here, 'iterable' means any object that can be iterated over - for +**iterable**. Here, 'iterable' means any object that can be iterated over - for more details see the [Iterable Abstract Base Class documentation](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable). The results of each of those applications become the values in the **iterable** @@ -158,7 +155,7 @@ print(list(map(lambda x: x + 1, l))) Like `map`, `filter` takes a function and applies it to each value in an iterable, keeping the value if the result of the function application is `True`. -``` python +```python l = [1, 2, 3] def is_gt_one(x): @@ -178,7 +175,7 @@ The `reduce` function is different. This function uses a function which accepts two values to accumulate the values in the iterable. The simplest uses here are to calculate the sum or product of a sequence. -``` python +```python from functools import reduce l = [1, 2, 3] @@ -202,9 +199,10 @@ These are the fundamental components of the MapReduce style, and can be combined Using `map` and `reduce`, write a function that calculates the sum of the squares of the values in a list. Your function should behave as below: -``` python +```python def sum_of_squares(l): # Your code here + return print(sum_of_squares([0])) print(sum_of_squares([1])) @@ -223,7 +221,7 @@ print(sum_of_squares([-1, -2, -3])) :::solution -``` python +```python from functools import reduce def sum_of_squares(l): @@ -236,7 +234,7 @@ def sum_of_squares(l): Now let's assume we're reading in these numbers from an input file, so they arrive as a list of strings. Modify your function so that it passes the following tests: -``` python +```python print(sum_of_squares(['1', '2', '3'])) print(sum_of_squares(['-1', '-2', '-3'])) ``` @@ -248,7 +246,7 @@ print(sum_of_squares(['-1', '-2', '-3'])) :::solution -``` python +```python from functools import reduce def sum_of_squares(l): @@ -262,7 +260,7 @@ def sum_of_squares(l): Finally, like comments in Python, we'd like it to be possible for users to comment out numbers in the input file they give to our program. Extend your function so that the following tests pass (don't worry about passing the first set of tests with lists of integers): -``` python +```python print(sum_of_squares(['1', '2', '3'])) print(sum_of_squares(['-1', '-2', '-3'])) print(sum_of_squares(['1', '2', '#100', '3'])) @@ -276,7 +274,7 @@ print(sum_of_squares(['1', '2', '#100', '3'])) :::solution -``` python +```python from functools import reduce def sum_of_squares(l): @@ -324,14 +322,14 @@ with, rather than always getting back a `map` or `filter` iterable. ### List Comprehensions The **list comprehension** is probably the most commonly used comprehension -type. As you might expect from the name, list comprehensions produce a list -from some other iterable type. In effect they are the same as using `map` +type. As you might expect from the name, list comprehensions produce a list +from some other iterable type. In effect they are the same as using `map` and/or `filter` and using `list()` to cast the result to a list, as we did previously. All comprehension types are structured in a similar way, using the syntax for a literal of that type (in the case below, a list literal) containing what looks -like the top of a for loop. To the left of the `for` we put the equivalent of +like the top of a for loop. To the left of the `for` we put the equivalent of the map operation we want to use: ```python @@ -550,10 +548,10 @@ Took 0.124199753 seconds ## Key Points -- *First-Class Functions*: functions that can be passed as arguments to other functions, returned from functions, or assigned to variables. -- *Lambda Functions*: small, nameless functions defined in the normal flow of the program with a keyword lambda. -- *Higher-Order Functions*: a function that has other functions as one of its arguments. -- *Map, Filter and Reduce*: built-in higher order functions in Python that use lazy evaluation. -- *Comprehensions*: a more Pythonic way to structure map and filter operations. -- *Generators*: similar to list comprehensions, but behave differently and not evaluated until you iterate over them. -- *Decorators*: higher-order functions that take a function as an argument, modify it, and return it. +- _First-Class Functions_: functions that can be passed as arguments to other functions, returned from functions, or assigned to variables. +- _Lambda Functions_: small, nameless functions defined in the normal flow of the program with a keyword lambda. +- _Higher-Order Functions_: a function that has other functions as one of its arguments. +- _Map, Filter and Reduce_: built-in higher order functions in Python that use lazy evaluation. +- _Comprehensions_: a more Pythonic way to structure map and filter operations. +- _Generators_: similar to list comprehensions, but behave differently and not evaluated until you iterate over them. +- _Decorators_: higher-order functions that take a function as an argument, modify it, and return it. diff --git a/software_architecture_and_design/functional/index.md b/software_architecture_and_design/functional/index.md index e3348e98..6424d36e 100644 --- a/software_architecture_and_design/functional/index.md +++ b/software_architecture_and_design/functional/index.md @@ -1,37 +1,35 @@ --- id: functional name: Functional Programming -dependsOn: [ - software_architecture_and_design.procedural, -] -files: [ +dependsOn: [software_architecture_and_design.procedural] +files: + [ side_effects_cpp.md, side_effects_python.md, recursion_cpp.md, recursion_python.md, higher_order_functions_cpp.md, higher_order_functions_python.md, -] + ] summary: | - Functional Programming is based around the idea that programs are constructed - by applying and composing/chaining **functions**. This course will introduce - you to the basics of functional programming in either Python or C++. -attribution: - - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. - url: https://www.sabsr3.ox.ac.uk - image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - + Functional Programming is based around the idea that programs are constructed + by applying and composing/chaining **functions**. This course will introduce + you to the basics of functional programming in either Python or C++. +attribution: + - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. + url: https://www.sabsr3.ox.ac.uk + image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- Functional programming is a programming paradigm where programs are constructed by applying and composing/chaining **functions**. Functional programming is based on the [mathematical definition of a -function](https://en.wikipedia.org/wiki/Function_(mathematics)) `f()`, which +function]() `f()`, which applies a transformation to some input data giving us some other data as a result (i.e. a mapping from input `x` to output `f(x)`). Thus, a program written in a functional style becomes a series of transformations on data which are @@ -56,4 +54,4 @@ In his introduction to functional programming in Advanced R, Hadley Wickham give > Each function taken by itself is simple and straightforward to understand; complexity is handled by composing functions in various ways. > > -- Hadley Wickham - [Functional Style](https://adv-r.hadley.nz/fp.html) -::: +> ::: diff --git a/software_architecture_and_design/functional/recursion_cpp.md b/software_architecture_and_design/functional/recursion_cpp.md index 07d89b4a..c86b418e 100644 --- a/software_architecture_and_design/functional/recursion_cpp.md +++ b/software_architecture_and_design/functional/recursion_cpp.md @@ -1,28 +1,25 @@ --- name: Recursion -dependsOn: [ - software_architecture_and_design.functional.higher_order_functions_cpp, -] +dependsOn: [software_architecture_and_design.functional.higher_order_functions_cpp] tags: [cpp] -attribution: - - citation: > - This material was adapted from an "Introduction to C++" course developed by the - Oxford RSE group. - url: https://www.rse.ox.ac.uk - image: https://www.rse.ox.ac.uk/images/banner_ox_rse.svg - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - +attribution: + - citation: > + This material was adapted from an "Introduction to C++" course developed by the + Oxford RSE group. + url: https://www.rse.ox.ac.uk + image: https://www.rse.ox.ac.uk/images/banner_ox_rse.svg + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- ## Recursion Recursion is one of the common strategies used in Functional Programming. Instead of using loops to **iteratively** apply an operation, we can express a -result in terms of previous results. To do this, the function needs to call +result in terms of previous results. To do this, the function needs to call itself to get the previous result, this is called **recursion**. The following two code examples implement the calculation of a factorial using @@ -42,11 +39,11 @@ int factorial(int n): } ``` -Functions in procedural programming are *procedures* that describe a detailed +Functions in procedural programming are _procedures_ that describe a detailed list of instructions to tell the computer what to do step by step and how to change the state of the program and advance towards the result. They often use -*iteration* to repeat a series of steps. Functional programming, on the other -hand, often uses *recursion* - an ability of a function to call/repeat +_iteration_ to repeat a series of steps. Functional programming, on the other +hand, often uses _recursion_ - an ability of a function to call/repeat itself until a particular condition is reached. ```cpp @@ -64,7 +61,7 @@ int factorial(int n) { } ``` -Note: this implementation is an example of *tail recursion*, which is typically +Note: this implementation is an example of _tail recursion_, which is typically optimised by the compiler back to an iterative implementation (since this is faster). @@ -88,8 +85,8 @@ int main() { // 1 * // / \ // 2 3 - Node t = Node('+', { Node(1), - Node('*', { Node(2), + Node t = Node('+', { Node(1), + Node('*', { Node(2), Node(3) }) } diff --git a/software_architecture_and_design/functional/recursion_python.md b/software_architecture_and_design/functional/recursion_python.md index 95cf0e5d..47575e94 100644 --- a/software_architecture_and_design/functional/recursion_python.md +++ b/software_architecture_and_design/functional/recursion_python.md @@ -1,26 +1,23 @@ --- name: Recursion -dependsOn: [ - software_architecture_and_design.functional.higher_order_functions_python, -] +dependsOn: [software_architecture_and_design.functional.higher_order_functions_python] tags: [python] -attribution: - - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. - url: https://www.sabsr3.ox.ac.uk - image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - +attribution: + - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. + url: https://www.sabsr3.ox.ac.uk + image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- ## Recursion Recursion is one of the common strategies used in Functional Programming. Instead of using loops to **iteratively** apply an operation, we can express a -result in terms of previous results. To do this, the function needs to call +result in terms of previous results. To do this, the function needs to call itself to get the previous result, this is called **recursion**. The following two code examples implement the calculation of a factorial using @@ -45,11 +42,11 @@ def factorial(n): return factorial ``` -Functions in procedural programming are *procedures* that describe a detailed +Functions in procedural programming are _procedures_ that describe a detailed list of instructions to tell the computer what to do step by step and how to change the state of the program and advance towards the result. They often use -*iteration* to repeat a series of steps. Functional programming, on the other -hand, typically uses *recursion* - an ability of a function to call/repeat +_iteration_ to repeat a series of steps. Functional programming, on the other +hand, typically uses _recursion_ - an ability of a function to call/repeat itself until a particular condition is reached. ```python diff --git a/software_architecture_and_design/functional/side_effects_cpp.md b/software_architecture_and_design/functional/side_effects_cpp.md index 6886ebaa..348a73c0 100644 --- a/software_architecture_and_design/functional/side_effects_cpp.md +++ b/software_architecture_and_design/functional/side_effects_cpp.md @@ -1,17 +1,16 @@ --- name: State and Side Effects -dependsOn: [ -] +dependsOn: [] tags: [cpp] -attribution: - - citation: This material was adapted from an "Introduction to C++" course developed by the Oxford RSE group. - url: https://www.rse.ox.ac.uk - image: https://www.rse.ox.ac.uk/images/banner_ox_rse.svg - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 +attribution: + - citation: This material was adapted from an "Introduction to C++" course developed by the Oxford RSE group. + url: https://www.rse.ox.ac.uk + image: https://www.rse.ox.ac.uk/images/banner_ox_rse.svg + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- ## Program state @@ -76,7 +75,7 @@ std::getline(myfile, line); // Same call to getline, but result is different! ``` The main downside of having a state that is constantly updated is that it makes -it harder for us to *reason* about our code, to work out what it is doing. +it harder for us to _reason_ about our code, to work out what it is doing. However, the main upside is that we can use state to make calculations more efficient, for example to sum up the values in a vector we use a single variable `sum` to hold the state of the computation @@ -94,15 +93,15 @@ for (const auto& x: data) { Functional computations only rely on the values that are provided as inputs to a function and not on the state of the program that precedes the function call. They do not modify data that exists outside the current function, including the -input data - this property is referred to as the *immutability of data*. This -means that such functions do not create any *side effects*, i.e. do not perform +input data - this property is referred to as the _immutability of data_. This +means that such functions do not create any _side effects_, i.e. do not perform any action that affects anything other than the value they return. A pure function is therefore the computational version of a mathematical function. For example: printing text, writing to a file, modifying the value of an input argument, or changing the value of a global variable. Functions without side affects that return the same data each time the same input arguments are provided are called -*pure functions*. +_pure functions_. ::::challenge{id="pure-functions" title="Pure Functions"} @@ -352,7 +351,7 @@ will be, or how to measure them. **Composability** refers to the ability to make a new function from a chain of other functions by piping the output of one as the input to the next. If a -function does not have side effects or non-deterministic behaviour, then all +function does not have side effects or non-deterministic behaviour, then all of its behaviour is reflected in the value it returns. As a consequence of this, any chain of combined pure functions is itself pure, so we keep all these benefits when we are combining functions into a larger program. @@ -362,14 +361,14 @@ benefits when we are combining functions into a larger program. *a lot of data, we can often improve performance by splitting data and *distributing the computation across multiple processors. The output of a pure *function depends only on its input, so we will get the right result regardless -*of when or where the code runs. +\*of when or where the code runs. There are other advantageous properties that can be derived from the functional approach to coding. In languages which support functional programming, a -function is a *first-class object* like any other object - not only can you +function is a _first-class object_ like any other object - not only can you compose/chain functions together, but functions can be used as inputs to, passed around or returned as results from other functions (remember, in functional -programming *code is data*). This is why functional programming is suitable for +programming _code is data_). This is why functional programming is suitable for processing data efficiently - in particular in the world of Big Data, where code is much smaller than the data, sending the code to where data is located is cheaper and faster than the other way round. Let's see how we can do data diff --git a/software_architecture_and_design/functional/side_effects_python.md b/software_architecture_and_design/functional/side_effects_python.md index 2c54d5a6..98cc8230 100644 --- a/software_architecture_and_design/functional/side_effects_python.md +++ b/software_architecture_and_design/functional/side_effects_python.md @@ -1,21 +1,18 @@ --- name: Side Effects -dependsOn: [ -] +dependsOn: [] tags: [python] -attribution: - - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. - url: https://www.sabsr3.ox.ac.uk - image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - +attribution: + - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. + url: https://www.sabsr3.ox.ac.uk + image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- - ## Program state In programming, the term "state" refers to the current status or condition of a program @@ -60,8 +57,9 @@ more unclear that this is being updated. The global variable and function might even be declared in a separate file and brought in via an `import` ```python -z = 3 +z = 3 def my_cool_function(x, y): + global z x = y z = z + 1 @@ -86,13 +84,13 @@ line = myfile.readline() # Same call to readline, but result is different! ``` The main downside of having a state that is constantly updated is that it makes -it harder for us to *reason* about our code, to work out what it is doing. +it harder for us to _reason_ about our code, to work out what it is doing. However, the upside is that we can use state to store temporary data to make calculations more efficient and store temporary data. For example an iteration loop that keeps track of a running total is a common pattern in procedural programming: -```python +```python nolint result = 0 for x in data: result += expensive_computation(x) @@ -111,13 +109,13 @@ efficiency of our code. Functional computations only rely on the values that are provided as inputs to a function and not on the state of the program that precedes the function call. They do not modify data that exists outside the current function, including the -input data - this property is referred to as the *immutability of data*. This -means that such functions do not create any *side effects*, i.e. do not perform +input data - this property is referred to as the _immutability of data_. This +means that such functions do not create any _side effects_, i.e. do not perform any action that affects anything other than the value they return. For example: printing text, writing to a file, modifying the value of an input argument, or changing the value of a global variable. Functions without side affects that return the same data each time the same input arguments are provided are called -*pure functions*. +_pure functions_. ::::challenge{id="pure-functions" title="Pure Functions"} @@ -191,7 +189,7 @@ def get_neighbors(grid, i, j): (indices[:, 1] >= 0) & (indices[:, 1] < cols) valid_indices[4] = False # exclude current cell return grid[indices[valid_indices][:, 0], indices[valid_indices][:, 1]] - + # Test grid = np.array([[0, 0, 0, 0, 0], [0, 0, 1, 0, 0], @@ -277,7 +275,7 @@ will be, or how to measure them. **Composability** refers to the ability to make a new function from a chain of other functions by piping the output of one as the input to the next. If a -function does not have side effects or non-deterministic behaviour, then all +function does not have side effects or non-deterministic behaviour, then all of its behaviour is reflected in the value it returns. As a consequence of this, any chain of combined pure functions is itself pure, so we keep all these benefits when we are combining functions into a larger program. As an example @@ -316,10 +314,10 @@ as they return new data objects instead of changing existing ones. There are other advantageous properties that can be derived from the functional approach to coding. In languages which support functional programming, a -function is a *first-class object* like any other object - not only can you +function is a _first-class object_ like any other object - not only can you compose/chain functions together, but functions can be used as inputs to, passed around or returned as results from other functions (remember, in functional -programming *code is data*). This is why functional programming is suitable for +programming _code is data_). This is why functional programming is suitable for processing data efficiently - in particular in the world of Big Data, where code is much smaller than the data, sending the code to where data is located is cheaper and faster than the other way round. Let's see how we can do data diff --git a/software_architecture_and_design/object_orientated/classes.md b/software_architecture_and_design/object_orientated/classes.md index 15572a78..ba7e4751 100644 --- a/software_architecture_and_design/object_orientated/classes.md +++ b/software_architecture_and_design/object_orientated/classes.md @@ -1,21 +1,19 @@ --- name: Classes -dependsOn: [ -] +dependsOn: [] tags: [python] learningOutcomes: - Explain the object orientated programming paradigm. - Define a class to encapsulate data. -attribution: - - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. - url: https://www.sabsr3.ox.ac.uk - image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - +attribution: + - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. + url: https://www.sabsr3.ox.ac.uk + image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- ## Structuring Data @@ -23,11 +21,11 @@ attribution: One of the main difficulties we encounter when building more complex software is how to structure our data. So far, we've been processing data from a single source and with a simple tabular structure, but it would be useful to be able to combine data from a range of different sources and with more data than just an array of numbers. -~~~ python +```python import numpy as np data = np.array([[1., 2., 3.], [4., 5., 6.]]) -~~~ +``` Using this data structure has the advantage of being able to use NumPy operations to process the data and Matplotlib to plot it, but often we need to have more structure than this. For example, we may need to attach more information about the patients and store this alongside our measurements of inflammation. @@ -35,7 +33,7 @@ For example, we may need to attach more information about the patients and store We can do this using the Python data structures we're already familiar with, dictionaries and lists. For instance, say we wish to store a list of patients on a clinical inflammation trial. We could attach a name to each of our patients: -~~~ python +```python patients = [ { 'name': 'Alice', @@ -46,7 +44,7 @@ patients = [ 'data': [4., 5., 6.], }, ] -~~~ +``` ::::challenge{id=structuring-data title="Structuring Data"} @@ -56,15 +54,15 @@ When used as below, it should produce the expected output. If you're not sure where to begin, think about ways you might be able to effectively loop over two collections at once. Also, don't worry too much about the data type of the `data` value, it can be a Python list, or a NumPy array - either is fine. -~~~ python +```python nolint data = np.array([[1., 2., 3.], [4., 5., 6.]]) output = attach_names(data, ['Alice', 'Bob']) print(output) -~~~ +``` -~~~text +```text [ { 'name': 'Alice', @@ -75,13 +73,13 @@ print(output) 'data': [4., 5., 6.], }, ] -~~~ +``` :::solution One possible solution, perhaps the most obvious, is to use the `range` function to index into both lists at the same location: -~~~ python +```python def attach_names(data, names): """Create datastructure containing patient records.""" output = [] @@ -91,10 +89,10 @@ def attach_names(data, names): 'data': data[i]}) return output -~~~ +``` However, this solution has a potential problem that can occur sometimes, depending on the input. -What might go wrong with this solution? How could we fix it? +What might go wrong with this solution? How could we fix it? ::: @@ -111,7 +109,7 @@ Checking that our inputs are valid in this way is an example of a precondition, If you've not previously come across the `zip` function, read [this section](https://docs.python.org/3/library/functions.html#zip) of the Python documentation. -~~~ python +```python def attach_names(data, names): """Create datastructure containing patient records.""" assert len(data) == len(names) @@ -122,7 +120,7 @@ def attach_names(data, names): 'data': data_row}) return output -~~~ +``` ::: @@ -132,15 +130,15 @@ def attach_names(data, names): Using nested dictionaries and lists should work for some of the simpler cases where we need to handle structured data, but they get quite difficult to manage -once the structure becomes a bit more complex. For this reason, in the object +once the structure becomes a bit more complex. For this reason, in the object oriented paradigm, we use **classes** to help with managing this data and the -operations we would want to perform on it. A class is a **template** +operations we would want to perform on it. A class is a **template** (blueprint) for a structured piece of data, so when we create some data using a class, we can be certain that it has the same structure each time. With our list of dictionaries we had in the example above, we have no real guarantee that each dictionary has the same structure, e.g. the same keys -(`name` and `data`) unless we check it manually. With a class, if an object is +(`name` and `data`) unless we check it manually. With a class, if an object is an **instance** of that class (i.e. it was made using that template), we know it will have the structure defined by that class. Different programming languages make slightly different guarantees about how strictly the structure will match, @@ -150,7 +148,7 @@ derived from the same class must follow the same behaviour. You may not have realised, but you should already be familiar with some of the classes that come bundled as part of Python, for example: -~~~ python +```python my_list = [1, 2, 3] my_dict = {1: '1', 2: '2', 3: '3'} my_set = {1, 2, 3} @@ -158,13 +156,13 @@ my_set = {1, 2, 3} print(type(my_list)) print(type(my_dict)) print(type(my_set)) -~~~ +``` -~~~text +```text -~~~ +``` Lists, dictionaries and sets are a slightly special type of class, but they behave in much the same way as a class we might define ourselves: @@ -185,7 +183,7 @@ The behaviours we may have seen previously include: Let's start with a minimal example of a class representing our patients. -~~~ python +```python # file: inflammation/models.py class Patient: @@ -195,18 +193,18 @@ class Patient: alice = Patient('Alice') print(alice.name) -~~~ +``` -~~~text +```text Alice -~~~ +``` -Here we've defined a class with one method: `__init__`. This method is the +Here we've defined a class with one method: `__init__`. This method is the **initialiser** method, which is responsible for setting up the initial values and structure of the data inside a new instance of the class - this is very similar to **constructors** in other languages, so the term is often used in -Python too. The `__init__` method is called every time we create a new instance -of the class, as in `Patient('Alice')`. The argument `self` refers to the +Python too. The `__init__` method is called every time we create a new instance +of the class, as in `Patient('Alice')`. The argument `self` refers to the instance on which we are calling the method and gets filled in automatically by Python - we do not need to provide a value for this when we call the method. @@ -231,16 +229,16 @@ we add functions which operate on the data the class contains. These functions are the member functions or methods. Methods on classes are the same as normal functions, except that they live -inside a class and have an extra first parameter `self`. Using the name `self` +inside a class and have an extra first parameter `self`. Using the name `self` is not strictly necessary, but is a very strong convention - it is extremely -rare to see any other name chosen. When we call a method on an object, the -value of `self` is automatically set to this object - hence the name. As we saw +rare to see any other name chosen. When we call a method on an object, the +value of `self` is automatically set to this object - hence the name. As we saw with the `__init__` method previously, we do not need to explicitly provide a value for the `self` argument, this is done for us by Python. Let's add another method on our Patient class that adds a new observation to a Patient instance. -~~~ python +```python # file: inflammation/models.py class Patient: @@ -271,13 +269,13 @@ print(alice) observation = alice.add_observation(3) print(observation) print(alice.observations) -~~~ +``` -~~~text +```text <__main__.Patient object at 0x7fd7e61b73d0> {'day': 0, 'value': 3} [{'day': 0, 'value': 3}] -~~~ +``` Note also how we used `day=None` in the parameter list of the `add_observation` method, then initialise it if the value is indeed `None`. This is one of the common ways to handle an optional argument in Python, so we'll see this pattern quite a lot in real projects. @@ -305,7 +303,7 @@ There are a few special method names that we can use which Python will use to pr When writing your own Python classes, you'll almost always want to write an `__init__` method, but there are a few other common ones you might need sometimes. You may have noticed in the code above that the method `print(alice)` returned `<__main__.Patient object at 0x7fd7e61b73d0>`, which is the string represenation of the `alice` object. We may want the print statement to display the object's name instead. We can achieve this by overriding the `__str__` method of our class. -~~~ python +```python # file: inflammation/models.py class Patient: @@ -337,11 +335,11 @@ class Patient: alice = Patient('Alice') print(alice) -~~~ +``` -~~~text +```text Alice -~~~ +``` These dunder methods are not usually called directly, but rather provide the implementation of some functionality we can use - we didn't call `alice.__str__()`, but it was called for us when we did `print(alice)`. Some we see quite commonly are: @@ -362,19 +360,19 @@ Your class should: - Have an author - When printed using `print(book)`, show text in the format "title by author" -~~~ python +```python nolint book = Book('A Book', 'Me') print(book) -~~~ +``` -~~~text +```text A Book by Me -~~~ +``` :::solution -~~~ python +```python class Book: def __init__(self, title, author): self.title = title @@ -382,7 +380,7 @@ class Book: def __str__(self): return self.title + ' by ' + self.author -~~~ +``` ::: :::: @@ -392,7 +390,7 @@ class Book: The final special type of method we will introduce is a **property**. Properties are methods which behave like data - when we want to access them, we do not need to use brackets to call the method manually. -~~~ python +```python # file: inflammation/models.py class Patient: @@ -409,16 +407,16 @@ alice.add_observation(4) obs = alice.last_observation print(obs) -~~~ +``` -~~~text +```text {'day': 1, 'value': 4} -~~~ +``` You may recognise the `@` syntax from episodes on functional programming - -`property` is another example of a **decorator**. In this case the `property` +`property` is another example of a **decorator**. In this case the `property` decorator is taking the `last_observation` function and modifying its behaviour, -so it can be accessed as if it were a normal attribute. It is also possible to +so it can be accessed as if it were a normal attribute. It is also possible to make your own decorators, but we won't cover it here. ## Key Points diff --git a/software_architecture_and_design/object_orientated/classes_cpp.md b/software_architecture_and_design/object_orientated/classes_cpp.md index 964337f0..043ee65b 100644 --- a/software_architecture_and_design/object_orientated/classes_cpp.md +++ b/software_architecture_and_design/object_orientated/classes_cpp.md @@ -1,20 +1,18 @@ --- name: Classes -dependsOn: [ -] +dependsOn: [] tags: [cpp] -attribution: - - citation: > - This material was adapted from an "Introduction to C++" course developed by the - Oxford RSE group. - url: https://www.rse.ox.ac.uk - image: https://www.rse.ox.ac.uk/images/banner_ox_rse.svg - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - +attribution: + - citation: > + This material was adapted from an "Introduction to C++" course developed by the + Oxford RSE group. + url: https://www.rse.ox.ac.uk + image: https://www.rse.ox.ac.uk/images/banner_ox_rse.svg + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- ## Prerequisites @@ -22,7 +20,7 @@ attribution: The code blocks in this lesson will assume that some boilerplate C++ code is present. In particular, we will assume that the following headers are included: -``` cpp +```cpp #include #include #include @@ -39,7 +37,7 @@ Class definitions will be assumed to exist before `main`, and all other code wil Let's assume we're writing a game and we have data related to characters and items. In a procedural style, with no classes, we might structure our game data with separate vectors for each attribute: -``` cpp +```cpp // Character data std::vector character_names; std::vector character_healthPoints; @@ -68,7 +66,7 @@ Write a function, called `move_character` that will take an index and update the One possible solution is as follows. -``` cpp +```cpp void move_character(int character_index, float new_x, float new_y) { character_positions_x[character_index] = new_x; character_positions_y[character_index] = new_y; @@ -111,7 +109,7 @@ Let's start by tidying up one of the problems with the previous solution where w It's not ideal that the position is two unrelated floats. Let's create an class called `Position`: -``` cpp +```cpp class Position { public: float x; @@ -136,14 +134,14 @@ Let's break down the syntax. Here's an example of how to create an object of the Position class: -``` cpp +```cpp // Creates a Position object at coordinates (10.0, 20.0) called pos Position pos(10.0, 20.0); ``` We can then modify the position as follows: -``` cpp +```cpp pos.x = 30.0; // Changes the x coordinate to 30.0 pos.y = 40.0; // Changes the y coordinate to 40.0 ``` @@ -157,7 +155,7 @@ Then, update the `move_character` method appropriately. By using a `std::vector` we can reduce the number of vectors we are storing. -``` cpp +```cpp // Character data std::vector character_names; std::vector character_healthPoints; @@ -170,16 +168,16 @@ std::vector item_positions; We can use the Position object just like it's a float. -- Because the object is simple*, the compiler will automatically generate the methods necessary to assign one Position to another. +- Because the object is simple\*, the compiler will automatically generate the methods necessary to assign one Position to another. - Because the object is small (it just contains two floats), it does not need to be passed by reference. -``` cpp +```cpp void move_character(int character_index, Position new_position) { character_positions[character_index] = new_position; } ``` -If you're interested in the * next to simple above, you may want to read about the [rule of zero](https://en.cppreference.com/w/cpp/language/rule_of_three). +If you're interested in the \* next to simple above, you may want to read about the [rule of zero](https://en.cppreference.com/w/cpp/language/rule_of_three). :::: ::::: @@ -190,7 +188,7 @@ Write a class that encapsulates the data relating to characters and items. ::::solution -``` cpp +```cpp class Character { public: std::string name; @@ -216,7 +214,7 @@ public: After writing the three classes `Position`, `Character` and `Item`, we can re-write all of our data that we originally had has: -``` cpp +```cpp std::vector characters; std::vector items; ``` @@ -229,7 +227,7 @@ To define the behaviour of a class we add functions which operate on the data th Methods on classes are the same as normal functions, except that they live inside a class. We can relocate our `move_character` method from being a free function to being a member function of the class `character`: -``` cpp +```cpp class Character { public: std::string name; @@ -238,7 +236,7 @@ public: Character(std::string name, int healthPoints, Position position) : name(name), healthPoints(healthPoints), position(position) {} - + void move(Position new_position) { position = new_position; } @@ -247,7 +245,7 @@ public: We can then create an object of type `Character` and change its position: -``` cpp +```cpp // Create a Character object Position initialPosition(10.0, 20.0); // Position at coordinates (10.0, 20.0) Character character("Hero", 100, initialPosition); // Character named "Hero" with 100 health points at initialPosition @@ -272,7 +270,7 @@ Similarly, with a setter, you can control how data is modified. Our `move` method is an example of a setter, although it would be more standard to call the method `setPosition`. For example, you could check if new data is valid before setting a variable, or you could make a variable read-only (by providing a getter but not a setter). -``` cpp +```cpp class Character { private: Position position; @@ -300,7 +298,7 @@ Make all data members private, and implement getters to access the data. :::solution -``` cpp +```cpp class Character { private: std::string name; @@ -360,7 +358,7 @@ This can make your classes more intuitive to use. Now let's overload the `==` operator to compare two Character objects. We'll say that two characters are the same if they have the same name and healthPoints: -``` cpp +```cpp class Character { // ...existing code... @@ -372,7 +370,7 @@ class Character { You can now compare two `Character` objects like this: -``` cpp +```cpp Character character1("Hero", 100, Position(10.0, 20.0)); Character character2("Hero", 100, Position(30.0, 40.0)); if (character1 == character2) { @@ -390,7 +388,7 @@ They are declared with the keyword `static`. Let's add a static member to the Character class to keep track of how many characters have been created. Every time a new character is created, we'll increment this counter: -``` cpp +```cpp class Character { // ...existing code... @@ -399,7 +397,7 @@ class Character { public: Character(std::string name, int healthPoints, Position position) : name(name), healthPoints(healthPoints), position(position) { - characterCount++; // this line is new, and the counter is + characterCount++; // this line is new, and the counter is } static int getCharacterCount() { @@ -429,7 +427,7 @@ Add data or behaviour to these classes. Here is working code for this lession that defines the classes and then gives an example of how to use them. You can also see this code in action, and play with it and run it, on [Compiler Explorer](https://gcc.godbolt.org/z/x7b38ba4e): -``` cpp +```cpp #include #include #include diff --git a/software_architecture_and_design/object_orientated/index.md b/software_architecture_and_design/object_orientated/index.md index 7b23b7ff..547944df 100644 --- a/software_architecture_and_design/object_orientated/index.md +++ b/software_architecture_and_design/object_orientated/index.md @@ -1,30 +1,28 @@ --- id: object_orientated name: Object-Orientated Programming -dependsOn: [ - software_architecture_and_design.procedural, -] -files: [ +dependsOn: [software_architecture_and_design.procedural] +files: + [ classes.md, classes_cpp.md, inheritance_and_composition.md, inheritance_and_composition_cpp.md, polymorphism.md, polymorphism_cpp.md, -] + ] summary: | - The Object Oriented Paradigm builds upon the Procedural Paradigm, but builds code around data. - This course will introduce you to the basics of Object Oriented Programming in either Python or C++. -attribution: - - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. - url: https://www.sabsr3.ox.ac.uk - image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - + The Object Oriented Paradigm builds upon the Procedural Paradigm, but builds code around data. + This course will introduce you to the basics of Object Oriented Programming in either Python or C++. +attribution: + - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. + url: https://www.sabsr3.ox.ac.uk + image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- ## The Object Oriented Paradigm diff --git a/software_architecture_and_design/object_orientated/inheritance_and_composition.md b/software_architecture_and_design/object_orientated/inheritance_and_composition.md index fd03ea2d..47c8c608 100644 --- a/software_architecture_and_design/object_orientated/inheritance_and_composition.md +++ b/software_architecture_and_design/object_orientated/inheritance_and_composition.md @@ -1,23 +1,20 @@ --- name: Inheritance and Composition -dependsOn: [ - software_architecture_and_design.object_orientated.classes, -] +dependsOn: [software_architecture_and_design.object_orientated.classes] tags: [python] learningOutcomes: - Define composition in relation to a class. - Define inhertitance in relation to a class. - Explain the different between composition and inheritance. -attribution: - - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. - url: https://www.sabsr3.ox.ac.uk - image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - +attribution: + - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. + url: https://www.sabsr3.ox.ac.uk + image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- ## Relationships Between Classes @@ -37,12 +34,12 @@ That time, we used a function which converted temperatures in Celsius to Kelvin In the same way, in object oriented programming, we can make things components of other things. -We often use composition where we can say 'x *has a* y' - for example in our inflammation project, we might want to say that a doctor *has* patients or that a patient *has* observations. +We often use composition where we can say 'x _has a_ y' - for example in our inflammation project, we might want to say that a doctor _has_ patients or that a patient _has_ observations. In the case of our example, we're already saying that patients have observations, so we're already using composition here. We're currently implementing an observation as a dictionary with a known set of keys though, so maybe we should make an `Observation` class as well. -~~~ python +```python # file: inflammation/models.py class Observation: @@ -80,19 +77,19 @@ alice = Patient('Alice') obs = alice.add_observation(3) print(obs) -~~~ +``` -~~~text +```text 3 -~~~ +``` Now we're using a composition of two custom classes to describe the relationship between two types of entity in the system that we're modelling. ### Inheritance The other type of relationship used in object oriented programming is **inheritance**. -Inheritance is about data and behaviour shared by classes, because they have some shared identity - 'x *is a* y'. -If class `X` inherits from (*is a*) class `Y`, we say that `Y` is the **superclass** or **parent class** of `X`, or `X` is a **subclass** of `Y`. +Inheritance is about data and behaviour shared by classes, because they have some shared identity - 'x _is a_ y'. +If class `X` inherits from (_is a_) class `Y`, we say that `Y` is the **superclass** or **parent class** of `X`, or `X` is a **subclass** of `Y`. If we want to extend the previous example to also manage people who aren't patients we can add another class `Person`. But `Person` will share some data and behaviour with `Patient` - in this case both have a name and show that name when you print them. @@ -101,7 +98,7 @@ Since we expect all patients to be people (hopefully!), it makes sense to implem To write our class in Python, we used the `class` keyword, the name of the class, and then a block of the functions that belong to it. If the class **inherits** from another class, we include the parent class name in brackets. -~~~ python +```python # file: inflammation/models.py class Observation: @@ -149,14 +146,14 @@ print(bob) obs = bob.add_observation(4) print(obs) -~~~ +``` -~~~text +```text Alice 3 Bob AttributeError: 'Person' object has no attribute 'add_observation' -~~~ +``` As expected, an error is thrown because we cannot add an observation to `bob`, who is a Person but not a Patient. @@ -174,9 +171,9 @@ This is quite a common pattern, particularly for `__init__` methods, where we ne ## Composition vs Inheritance When deciding how to implement a model of a particular system, you often have a choice of either composition or inheritance, where there is no obviously correct choice. -For example, it's not obvious whether a photocopier *is a* printer and *is a* scanner, or *has a* printer and *has a* scanner. +For example, it's not obvious whether a photocopier _is a_ printer and _is a_ scanner, or _has a_ printer and _has a_ scanner. -~~~ python +```python class Machine: pass @@ -189,9 +186,9 @@ class Scanner(Machine): class Copier(Printer, Scanner): # Copier `is a` Printer and `is a` Scanner pass -~~~ +``` -~~~ python +```python class Machine: pass @@ -206,7 +203,7 @@ class Copier(Machine): # Copier `has a` Printer and `has a` Scanner self.printer = Printer() self.scanner = Scanner() -~~~ +``` Both of these would be perfectly valid models and would work for most purposes. However, unless there's something about how you need to use the model which would benefit from using a model based on inheritance, it's usually recommended to opt for **composition over inheritance**. @@ -230,24 +227,24 @@ Above we gave an example of a `Patient` class which inherits from `Person`. Let' In addition to these, try to think of an extra feature you could add to the models which would be useful for managing a dataset like this - imagine we're -running a clinical trial, what else might we want to know? Try using Test +running a clinical trial, what else might we want to know? Try using Test Driven Development for any features you add: write the tests first, then add the feature. Once you've finished the initial implementation, do you have much duplicated -code? Is there anywhere you could make better use of composition or inheritance +code? Is there anywhere you could make better use of composition or inheritance to improve your implementation? For any extra features you've added, explain them and how you implemented them -to your neighbour. Would they have implemented that feature in the same way? +to your neighbour. Would they have implemented that feature in the same way? :::solution One example solution is shown below. You may start by writing some tests (that will initially fail), and then develop the code to satisfy the new requirements and pass the tests. -~~~ python -# file: tests/test_patient.py -"""Tests for the Patient model.""" +```python +# file: tests/test_patient.py +"""Tests for the Patient model.""" def test_create_patient(): """Check a patient is created correctly given a name.""" @@ -291,11 +288,11 @@ def test_no_duplicate_patients(): alice = Patient("Alice") doc.add_patient(alice) doc.add_patient(alice) - assert len(doc.patients) == 1 + assert len(doc.patients) == 1 ... -~~~ +``` -~~~ python +```python # file: inflammation/models.py ... class Person: @@ -320,7 +317,7 @@ class Patient(Person): day = 0 new_observation = Observation(day, value) self.observations.append(new_observation) - return new_observation + return new_observation class Doctor(Person): """A doctor in an inflammation study.""" @@ -336,11 +333,11 @@ class Doctor(Person): return self.patients.append(new_patient) ... -~~~ +``` ::: :::: ## Key Points -- Relationships between concepts can be described using inheritance (*is a*) and composition (*has a*). +- Relationships between concepts can be described using inheritance (_is a_) and composition (_has a_). diff --git a/software_architecture_and_design/object_orientated/inheritance_and_composition_cpp.md b/software_architecture_and_design/object_orientated/inheritance_and_composition_cpp.md index f5d80629..62d48597 100644 --- a/software_architecture_and_design/object_orientated/inheritance_and_composition_cpp.md +++ b/software_architecture_and_design/object_orientated/inheritance_and_composition_cpp.md @@ -1,21 +1,18 @@ --- name: Inheritance and Composition -dependsOn: [ - software_architecture_and_design.object_orientated.classes_cpp, -] +dependsOn: [software_architecture_and_design.object_orientated.classes_cpp] tags: [cpp] -attribution: - - citation: > - This material was adapted from an "Introduction to C++" course developed by the - Oxford RSE group. - url: https://www.rse.ox.ac.uk - image: https://www.rse.ox.ac.uk/images/banner_ox_rse.svg - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - +attribution: + - citation: > + This material was adapted from an "Introduction to C++" course developed by the + Oxford RSE group. + url: https://www.rse.ox.ac.uk + image: https://www.rse.ox.ac.uk/images/banner_ox_rse.svg + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- ## Prerequisites @@ -23,11 +20,11 @@ attribution: The code blocks in this lesson will assume that some boilerplate C++ code is present. In particular, we will assume that the following headers are included: -~~~ cpp +```cpp #include #include #include -~~~ +``` We will also assume that you are using the C++17 language standard, or later. This will be the default with most modern compilers. @@ -52,9 +49,9 @@ That time, we used a function which converted temperatures in Celsius to Kelvin In the same way, in object oriented programming, we can make things components of other things. -We often use composition where we can say 'x *has a* y' - for example in our game, we might want to say that a character *has* an inventory, and that an inventory *has* items. +We often use composition where we can say 'x _has a_ y' - for example in our game, we might want to say that a character _has_ an inventory, and that an inventory _has_ items. -In the case of our example, we're already saying that a character *has a* position, so we're already using composition here. +In the case of our example, we're already saying that a character _has a_ position, so we're already using composition here. :::::challenge{id=inventory title="Write an inventory"} @@ -67,7 +64,7 @@ Modify your `Character` class to contain an `Inventory` data member. Here is an example of what that might look like: -~~~ cpp +```cpp class Inventory { private: std::vector items; @@ -113,17 +110,17 @@ public: return inventory.getItem(index); } }; -~~~ +``` :::: ::::: We now have several examples of composition: -- Character *has a* position -- Item *has a* position -- Characher *has an* inventory -- Inventory *has many* items +- Character _has a_ position +- Item _has a_ position +- Characher _has an_ inventory +- Inventory _has many_ items You can see how we can build quickly build up complex behaviours. Now have a think: would it be simple to build this behavour without classes? @@ -132,15 +129,15 @@ It would probably be very messy. ### Inheritance The other type of relationship used in object oriented programming is **inheritance**. -Inheritance is about data and behaviour shared by classes, because they have some shared identity - 'x *is a* y'. +Inheritance is about data and behaviour shared by classes, because they have some shared identity - 'x _is a_ y'. For instance, we might have two types of character: warriors and mages. We can create two classes: `Warrior` and `Mage`. But, fundamentally, they are both characters and have common code such as an inventory and a position. We should not duplicate this code. -We achieve this through *inheritance*. -If class `Warrior` inherits from (*is a*) `Character`, we say that `Character` is the **base class**, **parent class**, or **superclass** of `Warrior`. +We achieve this through _inheritance_. +If class `Warrior` inherits from (_is a_) `Character`, we say that `Character` is the **base class**, **parent class**, or **superclass** of `Warrior`. We say that `Warrior` is a **derived class**, **child class**, or **subclass** of `Character`. The base class provides a set of attributes and behaviors that the derived class can inherit. @@ -149,7 +146,7 @@ This terminology is common across many object-oriented programming languages. A Warrior class may look something like this: -~~~ cpp +```cpp class Warrior : public Character { private: int strength; @@ -166,7 +163,7 @@ public: return strength; } }; -~~~ +``` Let's examine the syntax: @@ -191,7 +188,7 @@ Write a class called `Mage` that inherits from `Character`, and give it some uni Here is an example of what that might look like: -~~~ cpp +```cpp class Mage : public Character { private: int manaPoints; @@ -208,7 +205,7 @@ public: return manaPoints; } }; -~~~ +``` :::: ::::: @@ -216,7 +213,7 @@ public: ## Composition vs Inheritance When deciding how to implement a model of a particular system, you often have a choice of either composition or inheritance, where there is no obviously correct choice. -For example, it's not obvious whether a photocopier *is a* printer and *is a* scanner, or *has a* printer and *has a* scanner. +For example, it's not obvious whether a photocopier _is a_ printer and _is a_ scanner, or _has a_ printer and _has a_ scanner. Both of these would be perfectly valid models and would work for most purposes. However, unless there's something about how you need to use the model which would benefit from using a model based on inheritance, it's usually recommended to opt for **composition over inheritance**. @@ -238,7 +235,7 @@ Update your code to reflect this, and identify the inheritance and composition n Here is an example of what that might look like: -~~~ cpp +```cpp class Sword : public Item { private: int damage; @@ -307,11 +304,11 @@ public: return equippedSword; } }; -~~~ +``` Then we can use that functionality like this: -~~~ cpp +```cpp Sword sword("Excalibur", 10); Shield shield("Aegis", 5); @@ -340,21 +337,21 @@ if (mage.getEquippedSword()) { } return 0; -~~~ +``` :::: ::::: ## Key Points -- Relationships between concepts can be described using inheritance (*is a*) and composition (*has a*). +- Relationships between concepts can be described using inheritance (_is a_) and composition (_has a_). ## Full code sample for lession Here is working code for this lession that defines the classes and then gives an example of how to use them. You can also see this code in action, and play with it and run it, on [Compiler Explorer](https://gcc.godbolt.org/z/K51dPz1os): -~~~ cpp +```cpp #include #include #include @@ -533,4 +530,4 @@ int main() { return 0; } -~~~ +``` diff --git a/software_architecture_and_design/object_orientated/polymorphism.md b/software_architecture_and_design/object_orientated/polymorphism.md index 125c7a3e..5ef079a4 100644 --- a/software_architecture_and_design/object_orientated/polymorphism.md +++ b/software_architecture_and_design/object_orientated/polymorphism.md @@ -1,22 +1,19 @@ --- name: Polymorphism -dependsOn: [ - software_architecture_and_design.object_orientated.inheritance_and_composition, -] +dependsOn: [software_architecture_and_design.object_orientated.inheritance_and_composition] tags: [python] learningOutcomes: - Define polymorphism. - Apply polymorphism principles to class design. -attribution: - - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. - url: https://www.sabsr3.ox.ac.uk - image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - +attribution: + - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. + url: https://www.sabsr3.ox.ac.uk + image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- ## Class-based polymorphism @@ -37,7 +34,7 @@ want to be able to: We can implement this in our classes like so: -``` python +```python ... class Person: """A person.""" @@ -49,7 +46,7 @@ class Person: return self.name def set_id(self, id): - raise NotImplementedError('set_id not implemented') + raise NotImplementedError('set_id not implemented') def get_id(self): return self.id diff --git a/software_architecture_and_design/object_orientated/polymorphism_cpp.md b/software_architecture_and_design/object_orientated/polymorphism_cpp.md index a55a33a1..b67fe872 100644 --- a/software_architecture_and_design/object_orientated/polymorphism_cpp.md +++ b/software_architecture_and_design/object_orientated/polymorphism_cpp.md @@ -1,21 +1,18 @@ --- name: Polymorphism -dependsOn: [ - software_architecture_and_design.object_orientated.inheritance_and_composition_cpp, -] +dependsOn: [software_architecture_and_design.object_orientated.inheritance_and_composition_cpp] tags: [cpp] -attribution: - - citation: > - This material was adapted from an "Introduction to C++" course developed by the - Oxford RSE group. - url: https://www.rse.ox.ac.uk - image: https://www.rse.ox.ac.uk/images/banner_ox_rse.svg - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - +attribution: + - citation: > + This material was adapted from an "Introduction to C++" course developed by the + Oxford RSE group. + url: https://www.rse.ox.ac.uk + image: https://www.rse.ox.ac.uk/images/banner_ox_rse.svg + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- ## Prerequisites @@ -23,7 +20,7 @@ attribution: The code blocks in this lesson will assume that some boilerplate C++ code is present. In particular, we will assume that the following headers are included: -``` cpp +```cpp #include #include #include @@ -52,7 +49,7 @@ In C++ we archive this with **method overriding**: For this lesson we'll simplify the overall example, but feel free to modify your more extensive classes: -``` cpp +```cpp class Character { public: virtual void performAttack() const { @@ -64,7 +61,7 @@ public: Here, the **virtual** keyword indicates that this function can be overridden in derived classes. We can then add the `performAttack()` method to the derived classes: -``` cpp +```cpp class Warrior : public Character { public: void performAttack() const override { @@ -92,7 +89,7 @@ It is not mandatory to add the **override** keyword, but it is considered best p We can use this new code in many ways, but in general we will need a pointer or reference to the base class. Here's an example which we will then break down: -``` cpp +```cpp std::vector> characters; characters.push_back(std::make_unique()); characters.push_back(std::make_unique()); @@ -127,7 +124,7 @@ In our example, it may be that we can never have a character that is not either In this case, we would like `Character` to become an abstract class. An abstract class cannot be instantiated directly, and it is meant to serve as a base for derived classes by providing an interface that derived classes must implement. -A class becomes abstract if it has at least one *pure virtual function*, that is, a virtual function that does not have an implementaiton. +A class becomes abstract if it has at least one _pure virtual function_, that is, a virtual function that does not have an implementaiton. 1. **Pure Virtual Function**: The `Character` class would have at least one pure virtual function, declared as follows: @@ -191,7 +188,7 @@ virtual ~Character() = default; ``` - In C++, when an object is deleted through a pointer to a base class type, the destructor of the base class is called, but not the derived class destructors. -This can lead to a problem known as *slicing*, where only the base class portion of the object is destroyed, resulting in a potential resource leak or undefined behavior. + This can lead to a problem known as _slicing_, where only the base class portion of the object is destroyed, resulting in a potential resource leak or undefined behavior. - When deleting an object through a base class pointer or reference, the derived class destructor is also called, ensuring that the derived class's resources are properly released. - In the given example, although the `Character` class does not contain any member variables that need explicit cleanup, adding a virtual destructor is a good practice for future-proofing the code. If derived classes add their own resources or dynamically allocated memory, the virtual destructor will ensure proper destruction of those resources when deleting derived class objects through base class pointers. - Therefore, when making a class abstract and intended to be used as a base class, it is generally advisable to include a virtual destructor in the base class, even if it has no explicit cleanup to perform. @@ -206,7 +203,7 @@ This can lead to a problem known as *slicing*, where only the base class portion Here is working code for this lession that defines the classes and then gives an example of how to use them. You can also see this code in action, and play with it and run it, on [Compiler Explorer](https://gcc.godbolt.org/z/KoaoET9v9): -``` cpp +```cpp #include #include #include diff --git a/software_architecture_and_design/procedural/arrays_python.md b/software_architecture_and_design/procedural/arrays_python.md index e7b84547..3ff41eb7 100644 --- a/software_architecture_and_design/procedural/arrays_python.md +++ b/software_architecture_and_design/procedural/arrays_python.md @@ -1,19 +1,16 @@ --- name: Arrays -dependsOn: [ - software_architecture_and_design.procedural.functions_python, -] +dependsOn: [software_architecture_and_design.procedural.functions_python] tags: [python] -attribution: - - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. - url: https://www.sabsr3.ox.ac.uk - image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - +attribution: + - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. + url: https://www.sabsr3.ox.ac.uk + image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- NumPy is a widely-used Python library for numerical computing that provides @@ -34,18 +31,18 @@ install NumPy in. For example, say we wish to create a new "learning numpy" project, and create a new environment within this project. We can type the following: -~~~bash +```bash mkdir learning_numpy cd learning_numpy python3 -m venv venv source venv/bin/activate -~~~ +``` Then we can use `pip` to install `numpy`: -~~~bash +```bash pip install numpy -~~~ +``` This reflects a typical way of working with virtual environments: a new project comes along, we create a new virtual environment in a location close to where @@ -58,57 +55,57 @@ NumPy's array type represents a multidimensional array or tensor **M**~i,j,k...n The NumPy array seems at first to be just like a list: -~~~python +```python import numpy as np my_array = np.array(range(5)) my_array -~~~ +``` Note here we are importing the NumPy module as `np`, an established convention for using NumPy which means we can refer to NumPy using `np.` instead of the slightly more laborious `numpy.`. -~~~text +```text array([0, 1, 2, 3, 4]) -~~~ +``` -Ok, so they *look* like a list. +Ok, so they _look_ like a list. -~~~python +```python my_array[2] -~~~ +``` -~~~text +```text 2 -~~~ +``` We can access them like a list. We can also access NumPy arrays as a collection in `for` loops: -~~~python +```python for element in my_array: print("Hello" * element) -~~~ +``` -~~~text +```text Hello HelloHello HelloHelloHello HelloHelloHelloHello -~~~ +``` However, we can also see our first weakness of NumPy arrays versus Python lists: -~~~python +```python my_array.append(4) -~~~ +``` -~~~text +```text Traceback (most recent call last): File "", line 1, in AttributeError: 'numpy.ndarray' object has no attribute 'append' -~~~ +``` For NumPy arrays, you typically don't change the data size once you've defined your array, whereas for Python lists, you can do this efficiently. Also, NumPy @@ -119,15 +116,15 @@ return... One great advantage of NumPy arrays is that most operations can be applied element-wise automatically, and in a very Pythonic way! -~~~python +```python my_array + 2 -~~~ +``` `+` in this context is an elementwise operation performed on all the matrix elements, and gives us: -~~~text +```text array([2, 3, 4, 5, 6]) -~~~ +``` ::::challenge{id=elementwise-operations Title="Other elementwise operations"} @@ -135,19 +132,19 @@ Try using `-`, `*`, `/` in the above statement instead. Do they do what you expe :::solution -~~~python +```python my_array - 2 my_array * 2 my_array / 2 -~~~ +``` Will yield the following respectively: -~~~text +```text array([-2, -1, 0, 1, 2]) array([0, 2, 4, 6, 8]) array([0. , 0.5, 1. , 1.5, 2. ]) -~~~ +``` Note the final one with `/` - digits after the `.` are omitted if they don't show anything interesting (i.e. they are zero). @@ -161,18 +158,18 @@ performance of NumPy over Python lists. First, using Python lists we can do the following, that creates a 2D list of size 10000x10000, sets all elements to zero, then adds 10 to all those elements: -~~~python +```python nested_list = [[0 for _ in range(10000)] for _ in range(10000)] nested_list = [[x+10 for x in column] for column in nested_list] -~~~ +``` That took a while! In NumPy we replicate this by doing: -~~~python +```python import numpy as np array = np.zeros((10000, 10000)) array = array + 10 -~~~ +``` Here, we import the NumPy library, use a specialised function to set up a NumPy array of size 5000x5000 with elements set to zero, and then - in a very Pythonic @@ -187,36 +184,36 @@ inflammation in patients who have been given a new treatment for arthritis. Let's download this dataset now. First, create a new directory inflammation and `cd` to it: -~~~bash +```bash mkdir inflammation cd inflammation -~~~ +``` If on WSL or Linux (e.g. Ubuntu or the Ubuntu VM), then do: -~~~bash +```bash wget https://www.uhpc-training.co.uk/material/software_architecture_and_design/procedural/inflammation/inflammation.zip -~~~ +``` Or, if on a Mac, do: -~~~bash +```bash curl -O https://www.uhpc-training.co.uk/material/software_architecture_and_design/procedural/inflammation/inflammation.zip -~~~ +``` Once done, you can unzip this file using the `unzip` command in Bash, which will unpack all the files in this zip archive into the current directory: -~~~bash +```bash unzip inflammation.zip -~~~ +``` This zip file contains some code as well as the datasets we need stored in the `data` directory (which is what we're interested in). -~~~bash +```bash cd data -~~~ +``` :::callout @@ -225,7 +222,7 @@ cd data Each dataset records inflammation measurements from a separate clinical trial of the drug, and each dataset contains information for 60 patients, who had their inflammation levels recorded for 40 days whilst participating in the trial. ![Snapshot of the inflammation dataset](fig/inflammation-study-pipeline.png) -*Inflammation study pipeline from the [Software Carpentry Python novice lesson](https://swcarpentry.github.io/python-novice-inflammation/fig/lesson-overview.svg)* +_Inflammation study pipeline from the [Software Carpentry Python novice lesson](https://swcarpentry.github.io/python-novice-inflammation/fig/lesson-overview.svg)_ Each of the data files uses the popular [comma-separated (CSV) format](https://en.wikipedia.org/wiki/Comma-separated_values) to represent the data, where: @@ -237,12 +234,12 @@ Each of the data files uses the popular [comma-separated (CSV) format](https://e We can use first NumPy to load our dataset into a Python variable: -~~~python +```python data = np.loadtxt(fname='../data/inflammation-01.csv', delimiter=',') data -~~~ +``` -~~~text +```text array([[0., 0., 1., ..., 3., 0., 0.], [0., 1., 2., ..., 1., 0., 1.], [0., 1., 1., ..., 2., 1., 1.], @@ -250,7 +247,7 @@ array([[0., 0., 1., ..., 3., 0., 0.], [0., 1., 1., ..., 1., 1., 1.], [0., 0., 0., ..., 0., 2., 0.], [0., 0., 1., ..., 1., 1., 0.]]) -~~~ +``` So, the data in this case has 60 rows (one for each patient) and 40 columns (one for each day) as we would expect. Each cell in the data represents an inflammation reading on a given day for a patient. So this shows the results of measuring the inflammation of 60 patients over a 40 day period. @@ -271,13 +268,13 @@ which can be confusing when plotting data. Let's ask what type of thing `data` refers to: -~~~python +```python print(type(data)) -~~~ +``` -~~~text +```text -~~~ +``` The output tells us that `data` currently refers to an N-dimensional array, the functionality for which is provided by the NumPy library. @@ -287,48 +284,48 @@ The output tells us that `data` currently refers to an N-dimensional array, the A Numpy array contains one or more elements of the same type. The `type` function will only tell you that a variable is a NumPy array but won't tell you the type of thing inside the array. We can find out the type of the data contained in the NumPy array. -~~~python +```python print(data.dtype) -~~~ +``` -~~~text +```text float64 -~~~ +``` This tells us that the NumPy array's elements are 64-bit floating-point numbers. ::: -With the following command, we can see the array's *shape*: +With the following command, we can see the array's _shape_: -~~~python +```python print(data.shape) -~~~ +``` -~~~text +```text (60, 40) -~~~ +``` The output tells us that the `data` array variable contains 60 rows and 40 columns, as we would expect. We can also access specific elements of our 2D array (such as the first value) like this: -~~~python +```python data[0, 0] -~~~ +``` -~~~text +```text 0.0 -~~~ +``` Or the value in the middle of the dataset: -~~~python +```python data[30, 20] -~~~ +``` -~~~text +```text 13.0 -~~~ +``` ### Slicing our Inflammation Data @@ -336,16 +333,16 @@ An index like `[30, 20]` selects a single element of an array, but similar to ho For example, we can select the first ten days (columns) of values for the first four patients (rows) like this: -~~~python +```python data[0:4, 0:10] -~~~ +``` -~~~text +```text array([[0., 0., 1., 3., 1., 2., 4., 7., 8., 3.], [0., 1., 2., 1., 2., 1., 3., 2., 2., 6.], [0., 1., 1., 3., 3., 2., 6., 2., 5., 9.], [0., 0., 2., 0., 4., 2., 2., 1., 6., 7.]]) -~~~ +``` So here `0:4` means, "Start at index 0 and go up to, but not including, index 4." Again, the up-to-but-not-including takes a bit of getting used to, but the @@ -354,34 +351,34 @@ values in the slice. And as we saw with lists, we don't have to start slices at 0: -~~~python +```python data[5:10, 0:10] -~~~ +``` Which will show us data from patients 5-9 (rows) across the first 10 days (columns): -~~~text +```text array([[0., 0., 1., 2., 2., 4., 2., 1., 6., 4.], [0., 0., 2., 2., 4., 2., 2., 5., 5., 8.], [0., 0., 1., 2., 3., 1., 2., 3., 5., 3.], [0., 0., 0., 3., 1., 5., 6., 5., 5., 8.], [0., 1., 1., 2., 1., 3., 5., 3., 5., 8.]]) -~~~ +``` We also don't have to include the upper and lower bound on the slice. If we don't include the lower bound, Python uses 0 by default; if we don't include the upper, the slice runs to the end of the axis, and if we don't include either (i.e., if we just use ':' on its own), the slice includes everything: -~~~python +```python small = data[:3, 36:] small -~~~ +``` The above example selects rows 0 through 2 and columns 36 through to the end of the array: -~~~text +```text array([[2., 3., 0., 0.], [1., 1., 0., 1.], [2., 2., 1., 1.]]) -~~~ +``` :::callout @@ -389,51 +386,51 @@ array([[2., 3., 0., 0.], Numpy memory management can be tricksy: -~~~python +```python x = np.arange(5) y = x[:] -~~~ +``` -~~~python +```python y[2] = 0 x -~~~ +``` -~~~text +```text array([0, 1, 0, 3, 4]) -~~~ +``` It does not behave like lists! -~~~python +```python x = list(range(5)) y = x[:] -~~~ +``` -~~~python +```python y[2] = 0 x -~~~ +``` -~~~text +```text [0, 1, 2, 3, 4] -~~~ +``` We can use `np.copy()` to force the use of separate memory and actually copy the -values. Otherwise NumPy tries its hardest to make slices be *views* on data, +values. Otherwise NumPy tries its hardest to make slices be _views_ on data, referencing existing values and not copying them. So an example using `np.copy()`: -~~~python +```python x = np.arange(5) y = np.copy(x) y[2] = 0 x -~~~ +``` -~~~text +```text array([0, 1, 2, 3, 4]) -~~~ +``` ::: @@ -441,20 +438,20 @@ array([0, 1, 2, 3, 4]) As we've seen, arrays also know how to perform common mathematical operations on their values element-by-element: -~~~python +```python doubledata = data * 2.0 -~~~ +``` Will create a new array `doubledata`, each element of which is twice the value of the corresponding element in `data`: -~~~python +```python print('original:') data[:3, 36:] print('doubledata:') doubledata[:3, 36:] -~~~ +``` -~~~text +```text original: array([[2., 3., 0., 0.], [1., 1., 0., 1.], @@ -463,37 +460,37 @@ doubledata: array([[4., 6., 0., 0.], [2., 2., 0., 2.], [4., 4., 2., 2.]]) -~~~ +``` If, instead of taking an array and doing arithmetic with a single value (as above), you did the arithmetic operation with another array of the same shape, the operation will be done on corresponding elements of the two arrays: -~~~python +```python tripledata = doubledata + data -~~~ +``` Will give you an array where `tripledata[0,0]` will equal `doubledata[0,0]` plus `data[0,0]`, and so on for all other elements of the arrays. -~~~python +```python print('tripledata:') print(tripledata[:3, 36:]) -~~~ +``` -~~~text +```text tripledata: array([[6., 9., 0., 0.], [3., 3., 0., 3.], [6., 6., 3., 3.]]) -~~~ +``` ::::challenge{id=stacking-arrays title="Stacking Arrays"} Arrays can be concatenated and stacked on top of one another, using NumPy's `vstack` and `hstack` functions for vertical and horizontal stacking, respectively. -~~~python +```python import numpy as np A = np.array([[1,2,3], [4,5,6], [7,8,9]]) @@ -507,9 +504,9 @@ print(B) C = np.vstack([A, A]) print('C = ') print(C) -~~~ +``` -~~~text +```text A = [[1 2 3] [4 5 6] @@ -525,7 +522,7 @@ C = [1 2 3] [4 5 6] [7 8 9]] -~~~ +``` Write some additional code that slices the first and last columns of our inflammation `data` array, and stacks them into a 60x2 array, to give us data from the first and last days of our trial across all patients. @@ -540,20 +537,20 @@ the index itself can be a slice or array. For example, `data[:, :1]` returns a two dimensional array with one singleton dimension (i.e. a column vector). -~~~python +```python D = np.hstack([data[:, :1], data[:, -1:]]) print('D = ') print(D) -~~~ +``` -~~~text +```text D = [[0. 0.] [0. 1.] ... [0. 0.] [0. 0.]] -~~~ +``` ::: :::: @@ -562,29 +559,29 @@ D = You can also do [dot products](https://en.wikipedia.org/wiki/Dot_product) of NumPy arrays: -~~~python +```python a = np.array([[1, 2], [3, 4]]) b = np.array([[5, 6], [7, 8]]) np.dot(a, b) -~~~ +``` -~~~text +```text array([[19, 22], [43, 50]]) -~~~ +``` ### More Complex Operations Often, we want to do more than add, subtract, multiply, and divide array elements. NumPy knows how to do more complex operations, too. If we want to find the average inflammation for all patients on all days, for example, we can ask NumPy to compute `data`'s mean value: -~~~python +```python print(np.mean(data)) -~~~ +``` -~~~text +```text 6.14875 -~~~ +``` `mean` is a function that takes an array as an argument. @@ -597,16 +594,16 @@ However, some functions produce outputs without needing any input. For example, checking the current time doesn't require any input. -~~~text +```text import time print(time.ctime()) -~~~ +``` {: .language-python} -~~~text +```text Fri Sep 30 14:52:40 2022 -~~~ +``` {: .output} @@ -621,34 +618,34 @@ Let's use three of those functions to get some descriptive values about the dataset. We'll also use multiple assignment, a convenient Python feature that will enable us to do this all in one line. -~~~python +```python maxval, minval, stdval = np.max(data), np.min(data), np.std(data) print('max inflammation:', maxval) print('min inflammation:', minval) print('std deviation:', stdval) -~~~ +``` Here we've assigned the return value from `np.max(data)` to the variable `maxval`, the value from `np.min(data)` to `minval`, and so on. -~~~text +```text max inflammation: 20.0 min inflammation: 0.0 std deviation: 4.613833197118566 -~~~ +``` When analyzing data, though, we often want to look at variations in statistical values, such as the maximum inflammation per patient or the average inflammation per day. One way to do this is to create a new temporary array of the data we want, then ask it to do the calculation: -~~~python +```python np.max(data[0, :]) -~~~ +``` So here, we're looking at the maximum inflammation across all days for the first patient, which is -~~~text +```text 18.0 -~~~ +``` What if we need the maximum inflammation for each patient over all days (as in the next diagram on the left) or the average for each day (as in the diagram on @@ -659,11 +656,11 @@ an axis: To support this functionality, most array functions allow us to specify the axis we want to work on. If we ask for the average across axis 0 (rows in our 2D example), we get: -~~~python +```python print(np.mean(data, axis=0)) -~~~ +``` -~~~text +```text [ 0. 0.45 1.11666667 1.75 2.43333333 3.15 3.8 3.88333333 5.23333333 5.51666667 5.95 5.9 8.35 7.73333333 8.36666667 9.5 9.58333333 @@ -672,66 +669,66 @@ print(np.mean(data, axis=0)) 7.33333333 6.58333333 6.06666667 5.95 5.11666667 3.6 3.3 3.56666667 2.48333333 1.5 1.13333333 0.56666667] -~~~ +``` As a quick check, we can ask this array what its shape is: -~~~python +```python print(np.mean(data, axis=0).shape) -~~~ +``` -~~~text +```text (40,) -~~~ +``` The expression `(40,)` tells us we have an N×1 vector, so this is the average inflammation per day for all patients. If we average across axis 1 (columns in our 2D example), we get: -~~~python +```python patients_avg = np.mean(data, axis=1) patients_avg -~~~ +``` -~~~text +```text [ 5.45 5.425 6.1 5.9 5.55 6.225 5.975 6.65 6.625 6.525 6.775 5.8 6.225 5.75 5.225 6.3 6.55 5.7 5.85 6.55 5.775 5.825 6.175 6.1 5.8 6.425 6.05 6.025 6.175 6.55 6.175 6.35 6.725 6.125 7.075 5.725 5.925 6.15 6.075 5.75 5.975 5.725 6.3 5.9 6.75 5.925 7.225 6.15 5.95 6.275 5.7 6.1 6.825 5.975 6.725 5.7 6.25 6.4 7.05 5.9 ] -~~~ +``` Which is the average inflammation per patient across all days. ::::challenge{id=change-in-inflammation title="Change in Inflammation"} -This patient data is *longitudinal* in the sense that each row represents a -series of observations relating to one individual. This means that +This patient data is _longitudinal_ in the sense that each row represents a +series of observations relating to one individual. This means that the change in inflammation over time is a meaningful concept. The `np.diff()` function takes a NumPy array and returns the -differences between two successive values along a specified axis. For +differences between two successive values along a specified axis. For example, a NumPy array that looks like this: -~~~python +```python npdiff = np.array([ 0, 2, 5, 9, 14]) -~~~ +``` Calling `np.diff(npdiff)` would do the following calculations and put the answers in another array. -~~~python +```python [ 2 - 0, 5 - 2, 9 - 5, 14 - 9 ] -~~~ +``` -~~~python +```python np.diff(npdiff) -~~~ +``` -~~~python +```python array([2, 3, 4, 5]) -~~~ +``` Which axis would it make sense to use this function along? @@ -741,9 +738,9 @@ difference between two arbitrary patients. The column axis (1) is in days, so the difference is the change in inflammation -- a meaningful concept. -~~~python +```python np.diff(data, axis=1) -~~~ +``` ::: @@ -761,42 +758,42 @@ it matter if the change in inflammation is an increase or a decrease? :::solution By using the `np.max()` function after you apply the `np.diff()` -function, you will get the largest difference between days. We can *functionally -compose* these together. +function, you will get the largest difference between days. We can _functionally +compose_ these together. -~~~python +```python np.max(np.diff(data, axis=1), axis=1) -~~~ +``` -~~~python +```python array([ 7., 12., 11., 10., 11., 13., 10., 8., 10., 10., 7., 7., 13., 7., 10., 10., 8., 10., 9., 10., 13., 7., 12., 9., 12., 11., 10., 10., 7., 10., 11., 10., 8., 11., 12., 10., 9., 10., 13., 10., 7., 7., 10., 13., 12., 8., 8., 10., 10., 9., 8., 13., 10., 7., 10., 8., 12., 10., 7., 12.]) -~~~ +``` -If inflammation values *decrease* along an axis, then the difference from +If inflammation values _decrease_ along an axis, then the difference from one element to the next will be negative. If you are interested in the **magnitude** of the change and not the direction, the `np.absolute()` function will provide that. -Notice the difference if you get the largest *absolute* difference +Notice the difference if you get the largest _absolute_ difference between readings. -~~~python +```python np.max(np.absolute(np.diff(data, axis=1)), axis=1) -~~~ +``` -~~~python +```python array([ 12., 14., 11., 13., 11., 13., 10., 12., 10., 10., 10., 12., 13., 10., 11., 10., 12., 13., 9., 10., 13., 9., 12., 9., 12., 11., 10., 13., 9., 13., 11., 11., 8., 11., 12., 13., 9., 10., 13., 11., 11., 13., 11., 13., 13., 10., 9., 10., 10., 9., 9., 13., 10., 9., 10., 11., 13., 10., 10., 12.]) -~~~ +``` ::: :::: @@ -807,63 +804,63 @@ This is another really powerful feature of NumPy, and covers a 'special case' of By default, array operations are element-by-element: -~~~python +```python np.arange(5) * np.arange(5) -~~~ +``` -~~~text +```text array([ 0, 1, 4, 9, 16]) -~~~ +``` If we multiply arrays with non-matching shapes we get an error: -~~~python +```python np.arange(5) * np.arange(6) -~~~ +``` -~~~text +```text Traceback (most recent call last): File "", line 1, in ValueError: operands could not be broadcast together with shapes (5,) (6,) -~~~ +``` Or with a multi-dimensional array: -~~~python +```python np.zeros([2,3]) * np.zeros([2,4]) -~~~ +``` -~~~text +```text Traceback (most recent call last): File "", line 1, in ValueError: operands could not be broadcast together with shapes (2,3) (2,4) -~~~ +``` Arrays must match in all dimensions in order to be compatible: -~~~python +```python np.ones([3, 3]) * np.ones([3, 3]) # Note elementwise multiply, *not* matrix multiply. -~~~ +``` -~~~text +```text array([[ 1., 1., 1.], [ 1., 1., 1.], [ 1., 1., 1.]]) -~~~ +``` **Except**, that if one array has any Dimension size of 1, then the data is -*automatically REPEATED to match the other dimension. This is known as -**broadcasting*. +_automatically REPEATED to match the other dimension. This is known as +\*\*broadcasting_. So, let's consider a subset of our inflammation data (just so we can easily see what's going on): -~~~python +```python subset = data[:10, :10] subset -~~~ +``` -~~~text +```text array([[0., 0., 1., 3., 1., 2., 4., 7., 8., 3.], [0., 1., 2., 1., 2., 1., 3., 2., 2., 6.], [0., 1., 1., 3., 3., 2., 6., 2., 5., 9.], @@ -874,29 +871,29 @@ array([[0., 0., 1., 3., 1., 2., 4., 7., 8., 3.], [0., 0., 1., 2., 3., 1., 2., 3., 5., 3.], [0., 0., 0., 3., 1., 5., 6., 5., 5., 8.], [0., 1., 1., 2., 1., 3., 5., 3., 5., 8.]]) -~~~ +``` Let's assume we wanted to multiply each of the 10 individual day values in a patient row for every patient, by contents of the following array: -~~~python +```python multiplier = np.arange(1, 11) multiplier -~~~ +``` -~~~text +```text array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) -~~~ +``` So, the first day value in a patient row is multiplied by 1, the second day by 2, the third day by 3, etc. We can just do: -~~~python +```python subset * multiplier -~~~ +``` -~~~text +```text array([[ 0., 0., 3., 12., 5., 12., 28., 56., 72., 30.], [ 0., 2., 6., 4., 10., 6., 21., 16., 18., 60.], [ 0., 2., 3., 12., 15., 12., 42., 16., 45., 90.], @@ -907,7 +904,7 @@ array([[ 0., 0., 3., 12., 5., 12., 28., 56., 72., 30.], [ 0., 0., 3., 8., 15., 6., 14., 24., 45., 30.], [ 0., 0., 0., 12., 5., 30., 42., 40., 45., 80.], [ 0., 2., 3., 8., 5., 18., 35., 24., 45., 80.]]) -~~~ +``` Which gives us what we want, since each value in `multiplier` is applied successively to each value in a patient's row, but over every patient's row. So, diff --git a/software_architecture_and_design/procedural/containers_cpp.md b/software_architecture_and_design/procedural/containers_cpp.md index 3b8474b2..52779cb0 100644 --- a/software_architecture_and_design/procedural/containers_cpp.md +++ b/software_architecture_and_design/procedural/containers_cpp.md @@ -1,24 +1,21 @@ --- name: Containers -dependsOn: [ - software_architecture_and_design.procedural.variables_cpp, -] +dependsOn: [software_architecture_and_design.procedural.variables_cpp] tags: [cpp] -attribution: - - citation: > - This material was adapted from an "Introduction to C++" course developed by the - Oxford RSE group. - url: https://www.rse.ox.ac.uk - image: https://www.rse.ox.ac.uk/images/banner_ox_rse.svg - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - +attribution: + - citation: > + This material was adapted from an "Introduction to C++" course developed by the + Oxford RSE group. + url: https://www.rse.ox.ac.uk + image: https://www.rse.ox.ac.uk/images/banner_ox_rse.svg + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- -*Container* types are those that can hold other objects, and C++ supports a number of +_Container_ types are those that can hold other objects, and C++ supports a number of different [containers](https://en.cppreference.com/w/cpp/container) we can use to hold data of differing types in a multitude of ways. @@ -31,24 +28,24 @@ C++, depending on if you have a fixed sized array (`std::array`), or variable si container, and for the `std::array` you also need to specify the length. For example, to define a `std::vector` of `double` values you could write -~~~cpp +```cpp std::vector x; -~~~ +``` or a `std::array` of five `int` values is declared as: -~~~cpp +```cpp std::array y; -~~~ +``` -The angle bracket syntax here is an example of using *templates* in C++, and both -`std::vector` and `std::array` are examples of *templated* classes. Templates in C++ are -a form of *generic programming*, and allow us to write classes (and functions) that can +The angle bracket syntax here is an example of using _templates_ in C++, and both +`std::vector` and `std::array` are examples of _templated_ classes. Templates in C++ are +a form of _generic programming_, and allow us to write classes (and functions) that can accept many different types. All the container in C++ need to be able to hold any type of value, and therefore all of the container types in C++ are templated on the value type. The `std::array` class represents an array with a pre-defined size, and so this size is another template arguement. Note that unlike arguements to functions, all -template arguements must be know *at compile time*. +template arguements must be know _at compile time_. Since `std::array` has more limited use compared with `std::vector`, we will focus the remainder of this section on `std::vector`. The interface to `std::array` is very @@ -63,24 +60,24 @@ To define a vector initialised to a list of values, we can simply write a comma separated list of items in curly brackets. We can also define a two dimensional vector by defining a "vector of vectors". -~~~cpp +```cpp std::vector odds = {1, 3, 5, 7, 9, 11, 15}; std::vector> more_numbers = { {1, 2}, {3, 4, 5}, { {6, 7}, {8} } } -~~~ +``` We can see that our multi-dimensional vector can contain elements themselves of any size and depth. This could be used as way of representing matrices, but later we'll learn a better way to represent these. -This curly bracket syntax is for representing *initializer lists* in C++. These +This curly bracket syntax is for representing _initializer lists_ in C++. These initializer lists can only be used when initialising, or constructing, an instance of a class, and cannot be used once the instance has been already created. For example, the following code will give a compile error: -~~~cpp +```cpp std::vector odds; odds = {1, 3, 5, 7, 9, 11, 15}; -~~~ +``` Note that every value in a vector must be of the same type, and this must match the type that the `std::vector` is templated on. @@ -92,28 +89,28 @@ list: For example: -~~~cpp +```cpp std::cout << odds[0] << ' ' << odds[-1] << std::endl; -~~~ +``` This will print the first and last elements of a list: -~~~text +```text 1 15 -~~~ +``` We can replace elements within a specific part of the list (note that in C++, indexes start at 0): -~~~cpp +```cpp odds[6] = 13; -~~~ +``` -To add elements to the *end* of the vector use `push_back`, remove elements from -the *end* of the vector using `pop_back`. You can resize the vector using +To add elements to the _end_ of the vector use `push_back`, remove elements from +the _end_ of the vector using `pop_back`. You can resize the vector using `resize`. Get the current size of the vector using `size`. -~~~cpp +```cpp std::vector x; x.push_back(1.0); x.push_back(2.0); // x holds {1.0, 2.0} @@ -121,19 +118,19 @@ x.pop_back(); // x holds {1.0} x.resize(3); // x holds {1.0, ?, ?} std::cout << x.size() << std::endl; // 3 -~~~ +``` ## Loop or iterate over a Vector -Every container in C++ defines its own *iterators*, which can be used to iterate +Every container in C++ defines its own _iterators_, which can be used to iterate over that container. -~~~cpp +```cpp for (std::vector::iterator i = x.begin(); i != x.end(); ++i) { std:cout << *i << std::endl; } -~~~ +``` An iterator acts like a pointer to each element of the vector, and thus it can be dereferenced using `*` to obtain a reference to the value pointed to. @@ -142,52 +139,52 @@ We can simplify this rather verbose iterator classname by using the `auto`{.Cpp} keyword. This tells the compiler to infer the correct type (i.e. what is returned from `x.begin()`{.Cpp}: -~~~cpp +```cpp for (auto i = x.begin(); i != x.end(); ++i) { std:cout << *i << std::endl; } -~~~ +``` -Another `for` loop in C++ is the *range-based* loop, and these have the most compact +Another `for` loop in C++ is the _range-based_ loop, and these have the most compact syntax, and work with any container that has `begin` and `end` methods. -~~~cpp +```cpp std::vector x = {1.0, 2.0, 3.0, 4.0}; for (double i: x) { std:cout << i << std::endl; } -~~~ +``` You can also use `auto`{.Cpp} here to simplify things... -~~~cpp +```cpp for (auto i: x) { std:cout << i << std::endl; } -~~~ +``` The previous code snippet could not alter the contents of the vector -because `i` was a *copy* of each element of x. You can instead make `i` a +because `i` was a _copy_ of each element of x. You can instead make `i` a reference to either edit values -~~~cpp +```cpp for (auto& i: x) { i = 1.0; // set each element to 1.0 } -~~~ +``` or to provide a constant reference to each value (thus avoiding any copies) -~~~cpp +```cpp for (const auto& i: x) { std::cout << i << std::endl; // print each element to the console } -~~~ +``` ::::challenge{id=dot_product title="Dot Product" } @@ -195,7 +192,7 @@ Write code to calculate the scalar (dot) product of two `std::vector` va :::solution -~~~cpp +```cpp std::vector x = {1.0, 2.0, 3.0}; std::vector y = {1.0, 2.0, 3.0}; @@ -206,7 +203,7 @@ for (int i = 0; i < x.size(); ++i) { } std::cout << "dot with vectors = "<< dot << std::endl; -~~~ +``` ::: :::: @@ -218,16 +215,16 @@ store your matrices. You could use a flat array `std::array`, or you could use nested arrays `std::array, 3>`. Output the result in a nicely formatted way, for example: -~~~text +```text C = | 1, 2, 3 | | 4, 5, 6 | | 7, 8, 9 | -~~~ +``` :::solution -~~~cpp +```cpp std::array,3> A = {{{5, 8, 2}, {8, 3, 1}, {5, 3, 9}}}; std::array,3> B = {{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}}; std::array,3> C = {}; @@ -235,7 +232,7 @@ std::array,3> C = {}; for (int i = 0; i < 3; ++i) { for (int j = 0; j < 3; ++j) { for (int k = 0; k < 3; ++k) { - C[i][j] += A[i][k] * B[k][j]; + C[i][j] += A[i][k] * B[k][j]; } } } @@ -252,7 +249,7 @@ for (int i = 0; i < 3; ++i) { } } } -~~~ +``` ::: @@ -261,9 +258,9 @@ for (int i = 0; i < 3; ++i) { Deleting elements from the end of a vector is simple and fast and can be done using the `pop_back` function, which takes constant, or $\mathcal{O}(1)$ time using big-O notation. This means that the time taken is a constant or fixed amount of time independent of the size -of the vector. Deleting elements from the *start* or *middle* of the vector is more -difficult. An vector in C++ is an implementation of an *array* data structure, and -therefore the values contained occupy a *contiguous* section of memory, the start of +of the vector. Deleting elements from the _start_ or _middle_ of the vector is more +difficult. An vector in C++ is an implementation of an _array_ data structure, and +therefore the values contained occupy a _contiguous_ section of memory, the start of which is also the start of the vector. When deleting an element from the start or middle, the remainder of the vector must be shifted down to maintain the contiguous nature of the vector and the alignment of the first element to the start of the @@ -273,9 +270,9 @@ $\mathcal{O}(n)$ time. For example, if we want to delete an element from the middle of a vector while preserving the order of the elements, we can do the - following: +following: -~~~cpp +```cpp std::vector x = {1, 2, 3, 4}; auto delete_this = x.begin() + 1; // an iterator to "2" for (auto i = x.begin(); i != x.end(); i++) { @@ -289,20 +286,20 @@ for (auto i = x.begin(); i != x.end(); i++) { std::cout << *i << ", "; } std::cout << "]" << std::endl; -~~~ +``` Notice that this requires a loop through all the elements of the vector, hence the time taken is $\mathcal{O}(n)$. The output of this program will show us the vector with a '2' removed: -~~~text +```text [1, 3, 4, ] -~~~ +``` A linked list is a data structure that provides constant-time insertion or deletion of elements in the middle/start of the container. The C++ implmentation of a linked list is `std::list`, which you can use like this: -~~~cpp +```cpp std::list x = {1, 2, 3, 4}; auto delete_this = x.begin() + 1; // an iterator to "2" x.erase(delete_this); @@ -312,7 +309,7 @@ for (auto i = x.begin(); i != x.end(); i++) { std::cout << *i << ", "; } std::cout << "]" << std::endl; -~~~ +``` ## Move semantics for containers @@ -320,13 +317,13 @@ Recall that we can use `std::move` to move rather than copy values in C++. This is often useful to efficiently move values into a container without the expense of copying them, e.g. -~~~cpp +```cpp std::string war_and_peace = "...."; std::string moby_dick = "...."; std::list books; books.push_back(std::move(war_and_peace)); books.push_back(std::move(moby_dick)); -~~~ +``` ## Memory allocation and iterator invalidation @@ -338,7 +335,7 @@ certain amount of memory (its capacity) which might be greater than the size of the vector. Whenever the size of the vector exceeds this capacity the allocator reallocates the memory for that vector, reserving a greater amount. -~~~cpp +```cpp std::vector x; int old_capacity = x.capacity(); for (int i = 0; i < 3000; i++) { @@ -348,9 +345,9 @@ for (int i = 0; i < 3000; i++) { std::cout << "Size = " << x.size() << " Capacity = " << x.capacity() << std::endl; } } -~~~ +``` -~~~text +```text Size = 1 Capacity = 1 Size = 2 Capacity = 2 Size = 3 Capacity = 4 @@ -364,16 +361,16 @@ Size = 257 Capacity = 512 Size = 513 Capacity = 1024 Size = 1025 Capacity = 2048 Size = 2049 Capacity = 4096 -~~~ +``` Memory allocations are in general slow, so if the user has knowledge of the neccessary size of the vector, then this process can be optimised by reserving the correct amount of memory using `std::vector::reserve()`{.cpp} -~~~cpp +```cpp std::vector x; x.reserve(3000); -~~~ +``` Another implication of memory reallocation for any container is that memory reallocation neccessarily invalidates any iterators currently pointing at @@ -381,7 +378,7 @@ specific elements (since they are now at a new memory address). This can be a source of bugs, so be aware that growing or resizing a vector can invalidate your iterators! -~~~cpp +```cpp std::vector data = {1, 2, 3, 4}; // keep track of how much data we've already processed auto processed = data.begin(); @@ -398,16 +395,16 @@ for (int i = 0; i < 10; i++) { for (; processed != data.end(); processed++) { process_data(*processed); } -~~~ +``` If the function `process_data` prints out the value given, then the output might look like the below. In this case the reallocated vector has been moved to a section of memory far away from the original location, and all the intermediate memory locations are processed as well as the vector itself: -~~~text +```text 1 2 3 4 0 0 1041 0 540155953 540287027 540024880 825503793 891301920 892416052 859126069 808727840 808925234 891303730 842018868 808990772 892483616 926101557 941634361 808661305 808597809 842610720 808857908 941634101 842086709 959852598 942684192 943141431 941633588 842610736 875770421 825833504 926101555 941633587 825242164 943077432 942684192 925907257 941634103 942944825 909194803 909261088 892416049 958412597 859189556 825635636 942684192 858863158 941634864 959789104 959461431 842283040 925905206 941633586 892876848 942684471 825506080 825504566 941633840 942682676 959461174 959789344 892482872 958412857 943075892 842608948 859060512 875639857 958411059 859189556 943207731 842283040 925905206 941635123 926364983 825373744 892483616 892547896 958411824 808531506 892679473 825506080 892547894 941635384 875705650 875966770 859060512 876033840 958411315 943075892 842608948 892483872 842477625 958412597 859189556 858796340 842283296 942945337 958412082 959527216 858798132 959461664 808531506 941635640 825504313 959721526 943012128 892481844 941635385 942750005 909456697 892483616 909456182 958412339 943075892 842608948 943011872 825439800 958412853 859189556 875968564 959789344 825833527 958411824 909392181 825439281 842283040 808663090 958410804 809055538 909128245 825506080 892547894 941635128 926429753 942946358 842283296 875837494 941633847 808793394 808988726 892483616 892612661 958412342 859189556 808728627 842283296 909260854 958412343 909392181 876032305 959789344 859387959 941634612 942944825 842479666 943012128 942813492 958412597 925905716 842610741 842283040 959983670 941635636 909130037 842085680 892811296 943272758 958412597 825505845 959787057 959789088 891303992 808661305 842610995 942684192 858863158 941634864 825635380 892942640 842283296 825505846 941634105 909654069 943010099 825506080 942945078 941634614 859190578 808989493 842610720 909259833 941633588 942813748 909718067 892483616 943009845 958412340 859189556 892350772 959461664 808727862 958413110 825242420 960049200 892483616 808857653 958410808 876163636 943140917 825506080 909390646 941634609 959527221 943142192 942684192 876165177 941634361 825635380 808597296 959461664 943273266 958411571 859189556 943207731 842283296 926101816 958412852 825702704 926298168 842610720 909326388 958412337 808465204 892614713 943012128 858927412 941633588 942750005 909456697 842610720 925906227 958411319 909392181 875968049 942684192 943141431 958411318 825505845 808530227 892483616 875705394 958410802 875573302 808464953 842610720 909326388 941635121 892876848 859125303 0 0 49 0 1641085147 5 469321016 -564037215 0 1 2 3 0 0 81 0 1 2 3 4 0 1 2 3 4 5 6 7 8 9 -~~~ +``` ## Strings as Containers @@ -416,7 +413,7 @@ Conceptually, a string is a type of container, in this case of letters. The C++. The `std::string`{.cpp} container class has most of the same iterators and functions as `std::vector`{.cpp} and behaves in a very similar way: -~~~cpp +```cpp #include //... @@ -428,19 +425,19 @@ for (auto i = element.begin(); i != element.end(); i++) { std::cout << *i; } std::cout << std::endl; -~~~ +``` gives the output -~~~text +```text x oxygen! -~~~ +``` As well as acting like a vector, `std::string`{.cpp} also has some useful string-specific functionality, like being able to concatenate strings, to find and extract substrings or to easily print out to screen -~~~cpp +```cpp using namespace std::string_literals; const std::string oxygen = "oxygen"; // initialise with a const char * @@ -451,12 +448,12 @@ const auto first_hydrogen = water.substr(0, first_dash); // first_hydrogen is a std::cout << "water is " << water << std::endl; std::cout << "first element in water is " << first_hydrogen << std::endl; -~~~ +``` -~~~text +```text water is hydrogen-oxygen-hydrogen first element in water is hydrogen -~~~ +``` ## Map and Set @@ -471,14 +468,14 @@ map, the other is the value type that is stored in the map. The `std::map` class implements a mapping between the key type to the value type. For example, we can store and access the populations of various UK cities like so: -~~~cpp +```cpp #include //... -std::map> populations = { +std::map> populations = { {"Liverpool": 467995}, - {"Edinburgh": 448850}, + {"Edinburgh": 448850}, {"Manchester": 430818} } @@ -490,13 +487,13 @@ for (const auto& [key, value] : m) { std::cout << std::endl; const auto key = "Liverpool"s; -std::cout << "The population of " << key << " is " << populations[key] << std::endl; -~~~ +std::cout << "The population of " << key << " is " << populations[key] << std::endl; +``` -~~~text -[Edinburgh] = 448850; [Liverpool] = 467995; [Manchester] = 430818; [Oxford] = 137343; +```text +[Edinburgh] = 448850; [Liverpool] = 467995; [Manchester] = 430818; [Oxford] = 137343; The population of Liverpool is 467995 -~~~ +``` A set is similar to a map that only contains keys (no values). The C++ implementation of a set is `std::set`. Each element of the set is unique (just @@ -504,29 +501,29 @@ like the keys in a map). ## Tuples -A tuple is a fixed-size container of values that can be of *different* types. It +A tuple is a fixed-size container of values that can be of _different_ types. It is most useful for holding a collection of useful variables, or returning multiple values from a function. -~~~cpp +```cpp std::tuple y = {'Apple', 1, 'Cherry'}; auto fruits = sdt::make_tuple(3.14, 2, 'Cherry'); -~~~ +``` -Values can be obtained from a tuple via *destructuring*. For C++17 and onwards, the syntax is +Values can be obtained from a tuple via _destructuring_. For C++17 and onwards, the syntax is -~~~cpp +```cpp auto [weight, number, name] = fruits; -~~~ +``` Note that previously the syntax was more cumbersome: -~~~cpp -double weight; +```cpp +double weight; int number; std::string name; std::tie(weight, number, name) = fruits; -~~~ +``` ## General Rule @@ -548,7 +545,7 @@ Download the iris dataset hosted by the UCI Machine Learning Repository [here](h 5. class: -- Iris Setosa -- Iris Versicolour - -- Iris Virginica)_) + -- Iris Virginica)\_) Your goal is to provide minimum and maximum bounds of sepal length for each class of Iris. Below is an example code for reading in the dataset and printing @@ -557,7 +554,7 @@ more `std::vector`'s. Subsequently, calculate the minimum/maximum sepal length using your data vector(s) and store the result in a `std::map` which maps class name to min/max bounds. -~~~cpp +```cpp #include #include #include @@ -577,7 +574,7 @@ int main() { continue; } std::replace(line.begin(), line.end(), ',', ' '); - + std::istringstream iss(line); double sepal_len, unused; std::string iris_class; @@ -585,11 +582,11 @@ int main() { std::cout << sepal_len << ' ' << iris_class << std::endl; } } -~~~ +``` :::solution -~~~cpp +```cpp #include #include #include @@ -612,7 +609,7 @@ int main() { continue; } std::replace(line.begin(), line.end(), ',', ' '); - + std::istringstream iss(line); double sepal_len, unused; std::string iris_class; @@ -638,9 +635,9 @@ int main() { for (const auto& [iris_class, bounds]: stats) { const auto& [min, max] = bounds; - std::cout << iris_class << " = (" << min << " - " << max << ")" << std::endl; + std::cout << iris_class << " = (" << min << " - " << max << ")" << std::endl; } } -~~~ +``` :::: diff --git a/software_architecture_and_design/procedural/containers_python.md b/software_architecture_and_design/procedural/containers_python.md index 7b87e817..70ce77d9 100644 --- a/software_architecture_and_design/procedural/containers_python.md +++ b/software_architecture_and_design/procedural/containers_python.md @@ -1,26 +1,23 @@ --- name: Containers -dependsOn: [ - software_architecture_and_design.procedural.variables_python, -] +dependsOn: [software_architecture_and_design.procedural.variables_python] tags: [python] learningOutcomes: - Use container-type variables to hold multiple sets of data. - Use indexing and other access methods to access data within containers. - Differentiate between mutable and immutable variable types. -attribution: - - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. - url: https://www.sabsr3.ox.ac.uk - image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - +attribution: + - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. + url: https://www.sabsr3.ox.ac.uk + image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- -*Container* types are those that can hold other objects, and Python supports a +_Container_ types are those that can hold other objects, and Python supports a number of different containers we can use to hold data of differing types in a multitude of ways. @@ -77,11 +74,11 @@ We can replace elements within a specific part of the list (note that in Python, odds[6] = 13 ``` -We can also *slice* lists to either extract or set an arbitrary subset of the list. +We can also _slice_ lists to either extract or set an arbitrary subset of the list. ![slice-list](fig/05-slice-list-odd.png) -Note that here, we are selecting the *boundaries* between elements, and not the indexes. +Note that here, we are selecting the _boundaries_ between elements, and not the indexes. For example, to show us elements 3 to 5 (inclusive) in our list: @@ -189,10 +186,10 @@ Which demonstrates a key design principle behind Python: "there should be one - ## Mutability -An important thing to remember is that Python variables are simply *references* to values, and also that they fall into two distinct types: +An important thing to remember is that Python variables are simply _references_ to values, and also that they fall into two distinct types: -* Immutable types: value changes by referencing a newly created value (e.g. when adding a letter in a string). Note you cannot change individual elements of an immutable container (e.g. you can't change a single character in a string directly 'in place') -* Mutable types: values can be changed 'in place', e.g. changing or adding an item in a list +- Immutable types: value changes by referencing a newly created value (e.g. when adding a letter in a string). Note you cannot change individual elements of an immutable container (e.g. you can't change a single character in a string directly 'in place') +- Mutable types: values can be changed 'in place', e.g. changing or adding an item in a list ::: @@ -320,7 +317,7 @@ names[1] In a dictionary, we look up an element using another object of our choice: ```python -me = { 'name': 'Joe', 'age': 39, +me = { 'name': 'Joe', 'age': 39, 'Jobs': ['Programmer', 'Teacher'] } me ``` @@ -411,18 +408,18 @@ One consequence of this implementation is that you can only use immutable things ```python good_match = { - ("Lamb", "Mint"): True, + ("Lamb", "Mint"): True, ("Bacon", "Chocolate"): False - } +} ``` But: -```python +```python nolint illegal = { - ["Lamb", "Mint"]: True, + ["Lamb", "Mint"]: True, ["Bacon", "Chocolate"]: False - } +} ``` ```text @@ -435,7 +432,7 @@ Remember -- square brackets denote lists, round brackets denote tuples. ## Beware 'Copying' of Containers -Here, note that `y` is not equal to the contents of `x`, it is a second label on the *same object*. So when we change `y`, we are also changing `x`. This is generally true for mutable types in Python. +Here, note that `y` is not equal to the contents of `x`, it is a second label on the _same object_. So when we change `y`, we are also changing `x`. This is generally true for mutable types in Python. ```python x = [1, 2, 3] @@ -487,7 +484,7 @@ The copies that we make through slicing are called shallow copies: we don't copy all the objects they contain, only the references to them. This is why the nested list in `x[0]` is not copied, so `z[0]` still refers to it. It is possible to actually create copies of all the contents, however deeply nested -they are - this is called a *deep copy*. Python provides methods for that in its +they are - this is called a _deep copy_. Python provides methods for that in its standard library in the `copy` module. ## General Rule @@ -499,13 +496,13 @@ for anything which feels like a mapping from keys to values. ## Key Points -* Python containers can contain values of any type. -* Lists, sets, and dictionaries are mutable types whose values can be changed after creation. -* Lists store elements as an ordered sequence of potentially non-unique values. -* Dictionaries store elements as unordered key-value pairs. -* Dictionary keys are required to be of an immutable type. -* Sets are an unordered collection of unique elements. -* Containers can contain other containers as elements. -* Use `x[a:b]` to extract a subset of data from `x`, with `a` and `b` representing element *boundaries*, not indexes. -* Tuples are an immutable type whose values cannot be changed after creation and must be re-created. -* Doing `x = y`, where `y` is a container, doesn't copy its elements, it just creates a new reference to it. +- Python containers can contain values of any type. +- Lists, sets, and dictionaries are mutable types whose values can be changed after creation. +- Lists store elements as an ordered sequence of potentially non-unique values. +- Dictionaries store elements as unordered key-value pairs. +- Dictionary keys are required to be of an immutable type. +- Sets are an unordered collection of unique elements. +- Containers can contain other containers as elements. +- Use `x[a:b]` to extract a subset of data from `x`, with `a` and `b` representing element _boundaries_, not indexes. +- Tuples are an immutable type whose values cannot be changed after creation and must be re-created. +- Doing `x = y`, where `y` is a container, doesn't copy its elements, it just creates a new reference to it. diff --git a/software_architecture_and_design/procedural/functions_cpp.md b/software_architecture_and_design/procedural/functions_cpp.md index 0b745b5f..b386976c 100644 --- a/software_architecture_and_design/procedural/functions_cpp.md +++ b/software_architecture_and_design/procedural/functions_cpp.md @@ -3,14 +3,14 @@ name: Functions dependsOn: [software_architecture_and_design.procedural.containers_cpp] tags: [cpp] attribution: -- citation: This material was adapted from an "Introduction to C++" course developed by the Oxford RSE group. - url: https://www.rse.ox.ac.uk - image: https://www.rse.ox.ac.uk/images/banner_ox_rse.svg - license: CC-BY-4.0 -- citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 + - citation: This material was adapted from an "Introduction to C++" course developed by the Oxford RSE group. + url: https://www.rse.ox.ac.uk + image: https://www.rse.ox.ac.uk/images/banner_ox_rse.svg + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- # Functions diff --git a/software_architecture_and_design/procedural/functions_python.md b/software_architecture_and_design/procedural/functions_python.md index c0d75b5c..43d499b4 100644 --- a/software_architecture_and_design/procedural/functions_python.md +++ b/software_architecture_and_design/procedural/functions_python.md @@ -1,26 +1,24 @@ --- name: Functions -dependsOn: [ - software_architecture_and_design.procedural.containers_python, -] +dependsOn: [software_architecture_and_design.procedural.containers_python] tags: [python] learningOutcomes: - - Define a function that takes parameters. - - Return a value from a function. - - Test and debug a function. - - Set default values for function parameters. - - Explain why we should divide programs into small, single-purpose functions. -attribution: - - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. - url: https://www.sabsr3.ox.ac.uk - image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - + - Define a function that takes parameters. + - Return a value from a function. + - Test and debug a function. + - Set default values for function parameters. + - Explain why we should divide programs into small, single-purpose functions. +attribution: + - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. + url: https://www.sabsr3.ox.ac.uk + image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- + ## Using Functions In most modern programming languages these procedures are called **functions**. @@ -67,7 +65,7 @@ print(nums) The append function is actually also one of these functions that return `None`. We can test this again by printing its output. -``` python +```python nums = [1, 2, 3] result = nums.append(4) @@ -88,7 +86,7 @@ That's the case here - the purpose of the `append` function is to append a value Although Python has many built in functions, it wouldn't be much use if we couldn't also define our own. Most languages use a keyword to signify a **function definition**, in Python that keyword is `def`. -``` python +```python def add_one(value): return value + 1 @@ -100,7 +98,7 @@ print(two) 2 ``` -``` python +```python def say_hello(name): return 'Hello, ' + name + '!' @@ -125,7 +123,7 @@ When we call a function, parameters with default values can be used in one of th 2. We can provide our own value in the normal way 3. We can provide a value in the form of a **named argument** - arguments which are not named are called **positional arguments** -``` python +```python def say_hello(name='World'): return 'Hello, ' + name + '!' @@ -161,7 +159,7 @@ See [this page](https://docs.microsoft.com/en-us/cpp/cpp/declarations-and-defini Write a short function called `fence` that takes two parameters called original and wrapper and returns a new string that has the wrapper character at the beginning and end of the original. A call to your function should look like this: -``` python +```python nolint print(fence('name', '*')) ``` @@ -171,7 +169,7 @@ print(fence('name', '*')) :::solution -``` python +```python def fence(original, wrapper): return wrapper + original + wrapper ``` @@ -186,7 +184,7 @@ How many different ways can you call this function using combinations of named a :::solution -``` python +```python def say_hello(greeting='Hello', name='World'): return greeting + ', ' + name + '!' @@ -224,7 +222,7 @@ Within a function, any variables that are created (such as parameters or other v For example, what would be the output from the following: -``` python +```python f = 0 k = 0 @@ -249,7 +247,7 @@ with those defined outside of the function. This is really useful, since it means we don’t have to worry about conflicts with variable names that are defined outside of our function that may cause it -to behave incorrectly. This is known as variable scoping. +to behave incorrectly. This is known as variable scoping. ::: :::: @@ -258,15 +256,15 @@ to behave incorrectly. This is known as variable scoping. One of the main reasons for defining a function is to encapsulate our code, so that it can be used without having to worry about exactly how the computation is -performed. This means we're free to implement the function however we want, +performed. This means we're free to implement the function however we want, including deferring some part of the task to another function that already exists. For example, if some data processing code we're working on needs to be able to accept temperatures in Fahrenheit, we might need a way to convert these into -Kelvin. So we could write these two temperature conversion functions: +Kelvin. So we could write these two temperature conversion functions: -``` python +```python def fahr_to_cels(fahr): # Convert temperature in Fahrenheit to Celsius cels = (fahr + 32) * (5 / 9) @@ -283,14 +281,14 @@ print(fahr_to_kelv(212)) ``` But if we look at these two functions, we notice that the conversion from -Fahrenheit to Celsius is actually duplicated in both functions. This makes +Fahrenheit to Celsius is actually duplicated in both functions. This makes sense, since this is a necessary step in both functions, but duplicated code is wasteful and increases the chance of us making an error - what if we made a typo in one of the equations? So, we can remove the duplicated code, by calling one function from inside the other: -``` python +```python def fahr_to_cels(fahr): # Convert temperature in Fahrenheit to Celsius cels = (fahr + 32) * (5 / 9) @@ -309,7 +307,7 @@ print(fahr_to_kelv(212)) Now we've removed the duplicated code, but we might actually want to go one step further and remove some of the other unnecessary bits: -``` python +```python def fahr_to_cels(fahr): # Convert temperature in Fahrenheit to Celsius return (fahr + 32) * (5 / 9) @@ -332,7 +330,7 @@ As a common example to illustrate each of the paradigms, we'll write some code t First, let's create a data structure to keep track of the papers that a group of academics are publishing. Note that we could use an actual `date` type to store the publication date, but they're much more complicated to work with, so we'll just use the year. -``` python +```python academics = [ { 'name': 'Alice', @@ -361,7 +359,7 @@ academics = [ We want a convenient way to add new papers to the data structure. -``` python +```python def write_paper(academics, name, title, date): paper = { 'title': title, @@ -376,7 +374,7 @@ def write_paper(academics, name, title, date): We're introducing a new keyword here, `break`, which exits from inside a loop. When the `break` keyword is encountered, execution jumps to the next line -outside of the loop. If there isn't a next line, as in our example here, then +outside of the loop. If there isn't a next line, as in our example here, then it's the end of the current block of code. This is useful when we have to search for something in a list - once we've found @@ -398,7 +396,7 @@ For the moment we'll just raise the exception, and assume that it will get handl In Python, exceptions may also be used to alter the flow of execution even when an error has not occured. For example, when iterating over a collection, a `StopIteration` exception is the way in which Python tells a loop construct to terminate, though this is hidden from you. -``` python +```python def write_paper(academics, name, title, date): paper = { 'title': title, @@ -427,7 +425,7 @@ For more information see [this section](https://docs.python.org/3/tutorial/contr We have seen previously that functions are not able to change the value of a variable which is used as their argument. -``` python +```python def append_to_list(l): l.append('appended') l = [1, 2, 3] @@ -469,7 +467,7 @@ Write a function called `count_papers`, that when called with `count_papers(acad One possible solution is: -``` python +```python def count_papers(academics): count = 0 @@ -497,7 +495,7 @@ Write a function called `list_papers`, that when called with `list_papers(academ One possible solution is: -``` python +```python def list_papers(academics): papers = [] diff --git a/software_architecture_and_design/procedural/variables_cpp.md b/software_architecture_and_design/procedural/variables_cpp.md index 22b82cd0..54e825c5 100644 --- a/software_architecture_and_design/procedural/variables_cpp.md +++ b/software_architecture_and_design/procedural/variables_cpp.md @@ -107,7 +107,7 @@ If we try to use a variable that hasn't been defined, we get a compiler error: int seven = sixe + 1; ``` -```text +````text /Users/martinjrobins/git/thing/procedural.cpp:7:17: error: use of undeclared identifier 'sixe'; did you mean 'six'? int seven = sixe + 1; ^``` @@ -116,7 +116,7 @@ int seven = sixe + 1; int six = 2 * 3; ^ 1 error generated. -``` +```` Note here we accidentally wrote `sixe` instead of `six`, so the compiler recognised this as an _undeclared identifier_ and gave an error. It even @@ -138,11 +138,11 @@ const int six = 2 * 3; six = 7; ``` -```text +````text /Users/martinjrobins/git/thing/procedural.cpp:8:9: error: cannot assign to variable 'six' with const-qualified type 'const int' six = 7; ``` ^ -``` +```` The compiler has saved us again! You can assist the compiler (and perhaps more importantly, other readers of your code!) by always marking variables that you diff --git a/software_architecture_and_design/procedural/variables_python.md b/software_architecture_and_design/procedural/variables_python.md index de4eb014..c6eb8ec9 100644 --- a/software_architecture_and_design/procedural/variables_python.md +++ b/software_architecture_and_design/procedural/variables_python.md @@ -1,24 +1,20 @@ --- name: Variables -dependsOn: [ - technology_and_tooling.bash_shell.bash, - technology_and_tooling.ide.cpp, -] +dependsOn: [technology_and_tooling.bash_shell.bash, technology_and_tooling.ide.cpp] tags: [python] learningOutcomes: - Describe the fundamental types of variables. - Assign values to basic variables and make use of them. - Print the content of variables. -attribution: - - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. - url: https://www.sabsr3.ox.ac.uk - image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png - license: CC-BY-4.0 - - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 - url: https://www.universe-hpc.ac.uk - image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png - license: CC-BY-4.0 - +attribution: + - citation: This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training. + url: https://www.sabsr3.ox.ac.uk + image: https://www.sabsr3.ox.ac.uk/sites/default/files/styles/site_logo/public/styles/site_logo/public/sabsr3/site-logo/sabs_r3_cdt_logo_v3_111x109.png + license: CC-BY-4.0 + - citation: This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1 + url: https://www.universe-hpc.ac.uk + image: https://www.universe-hpc.ac.uk/assets/images/universe-hpc.png + license: CC-BY-4.0 --- ## Getting started @@ -51,7 +47,7 @@ And then you are presented with something like: ```text Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] on linux Type "help", "copyright", "credits" or "license" for more information. ->>> +>>> ``` And lo and behold! You are presented with yet another prompt. @@ -98,7 +94,7 @@ the command "Run Selection/Line in Python Terminal"). If we look for a variable that hasn't ever been defined, we get an error telling us so: -```python +```python nolint print(seven) ``` @@ -183,7 +179,7 @@ Note we don't need to use `print` - the Python interpreter will just output the ``` -Depending on its type, an object can have different properties: data fields *inside* the object. +Depending on its type, an object can have different properties: data fields _inside_ the object. Consider a Python complex number for example, which Python supports natively: @@ -247,7 +243,7 @@ dir(z) 'conjugate' 'imag' 'real'] - ``` +``` You can see that there are several methods whose name starts and ends with `__` (e.g. `__init__`): these are special methods that Python uses internally, and @@ -286,8 +282,8 @@ A property of an object is accessed with a dot. The jargon is that the "dot oper Since we're not declaring the type of a variable, how does it work it out? -Python is an interpreted language that is *dynamically typed*, which means the -type of a variable is determined and *bound* to the variable at runtime from its +Python is an interpreted language that is _dynamically typed_, which means the +type of a variable is determined and _bound_ to the variable at runtime from its given value. So when we assign a floating point number, for example, it's type is inferred: @@ -301,13 +297,13 @@ print('Weight in lb', weight_lb) Note we can add as many things that we want to `print` by separating them with a comma. -For a float, a number after a point is optional. But the *dot* makes it a float. +For a float, a number after a point is optional. But the _dot_ makes it a float. ```text Weight in lb 121.00000000000001 ``` -So the thing with floats is that they are *representation* of a real number. +So the thing with floats is that they are _representation_ of a real number. Representing a third or the root of 2 would be impossible for a computer, so these are really approximations of real numbers using an ubiquitous standard ([IEEE-754](https://docs.python.org/3/tutorial/floatingpoint.html#representation-error)). @@ -351,7 +347,7 @@ Joe Frederick 'Bloggs' With quotes, the main thing is to be consistent in how you use them (i.e. not like we've used them above!). -We've looked at properties on objects. But many objects can also have *methods* (types of functions) associated with them, which we can use to perform operations on the object. +We've looked at properties on objects. But many objects can also have _methods_ (types of functions) associated with them, which we can use to perform operations on the object. For strings, we also can do things like: