|
6 | 6 | fromtextwrapimportdedent |
7 | 7 | fromtypingimportList |
8 | 8 |
|
| 9 | +fromcore.exercisesimportassert_equal,ExerciseError |
9 | 10 | fromcore.textimportExerciseStep,Page,MessageStep,Disallowed,VerbatimStep |
10 | 11 | fromcore.utilsimportreturns_stdout |
11 | 12 |
|
@@ -1573,3 +1574,224 @@ def generate_inputs(cls): |
1573 | 1574 | final_text=""" |
1574 | 1575 | Brilliant! You're almost ready to put it all together, keep going! |
1575 | 1576 | """ |
| 1577 | + |
| 1578 | + |
| 1579 | +classMakingTheBoard(Page): |
| 1580 | +title="Making the Board" |
| 1581 | + |
| 1582 | +classnaive_make_board(VerbatimStep): |
| 1583 | +""" |
| 1584 | +So far the board has been provided for you as a nested list. |
| 1585 | +But for the full program, you need to create it yourself. |
| 1586 | +Should be easy, right? Here's some code to do that: |
| 1587 | +
|
| 1588 | + __copyable__ |
| 1589 | + __program_indented__ |
| 1590 | +
|
| 1591 | +It's close, but there's a subtle problem with it. |
| 1592 | +Make sure you understand the code, |
| 1593 | +and bonus points if you can spot the bug! |
| 1594 | +If not, don't feel bad or waste too much time on it. |
| 1595 | + """ |
| 1596 | + |
| 1597 | +defprogram(self): |
| 1598 | +defmake_board(size): |
| 1599 | +row= [] |
| 1600 | +for_inrange(size): |
| 1601 | +row.append(' ') |
| 1602 | +board= [] |
| 1603 | +for_inrange(size): |
| 1604 | +board.append(row) |
| 1605 | +returnboard |
| 1606 | + |
| 1607 | +deftest(): |
| 1608 | +board=make_board(3) |
| 1609 | +assert_equal(board, [ |
| 1610 | + [' ',' ',' '], |
| 1611 | + [' ',' ',' '], |
| 1612 | + [' ',' ',' '], |
| 1613 | + ]) |
| 1614 | +board[0][0]='X' |
| 1615 | +assert_equal(board, [ |
| 1616 | + ['X',' ',' '], |
| 1617 | + [' ',' ',' '], |
| 1618 | + [' ',' ',' '], |
| 1619 | + ]) |
| 1620 | + |
| 1621 | +test() |
| 1622 | + |
| 1623 | +classfix_make_board(ExerciseStep): |
| 1624 | +""" |
| 1625 | +Can you see what happened? |
| 1626 | +
|
| 1627 | +Every row got an `'X'` in the first position! |
| 1628 | +It's as if the code actually did this: |
| 1629 | +
|
| 1630 | + board[0][0] = 'X' |
| 1631 | + board[1][0] = 'X' |
| 1632 | + board[2][0] = 'X' |
| 1633 | +
|
| 1634 | +Try and figure out what's wrong by yourself. |
| 1635 | +But again, it's tricky, so don't drive yourself crazy over it. |
| 1636 | +
|
| 1637 | +If you want, here's some hints: |
| 1638 | +
|
| 1639 | + - Try running the code through some debuggers. |
| 1640 | + - Experiment. Make changes to the code and see what happens. |
| 1641 | + - No, the code didn't do 3 assignments like I suggested above. There was just one list assignment. |
| 1642 | + - There's no hidden loops or anything. |
| 1643 | + - How many lists does `board` contain? 3? |
| 1644 | + - The previous page has a subtle hint at what happened. |
| 1645 | + - There is a page from a previous chapter where this kind of problem is explained directly. |
| 1646 | + - Specifically [this page](#EqualsVsIs). |
| 1647 | + - Try running the code with Python Tutor. |
| 1648 | +
|
| 1649 | +OK, if you're ready, here's the answer. |
| 1650 | +
|
| 1651 | +The list `row` was only created once, and reused several times. |
| 1652 | +`board` contains the same list three times. Not copies, just one list in three places. |
| 1653 | +It's like it did this: |
| 1654 | +
|
| 1655 | + board = [row, row, row] |
| 1656 | +
|
| 1657 | +Which means that this code: |
| 1658 | +
|
| 1659 | + board[0][0] = 'X' |
| 1660 | +
|
| 1661 | +is equivalent to: |
| 1662 | +
|
| 1663 | + row[0] = 'X' |
| 1664 | +
|
| 1665 | +which affects 'all the lists' in `board` because they're all just the one list `row`. |
| 1666 | +In other words, the above line is *also* equivalent to each of these two lines: |
| 1667 | +
|
| 1668 | + board[1][0] = 'X' |
| 1669 | + board[2][0] = 'X' |
| 1670 | +
|
| 1671 | +because `row` is `board[0]`, `board[1]`, and `board[2]` all at once. |
| 1672 | +
|
| 1673 | +Your job now is to fix `make_board` to not have this problem. |
| 1674 | +It should still return a list of length `size` where each |
| 1675 | +element is also list of length `size` where each element is the string `' '`. |
| 1676 | +The sublists should all be separate list objects, not the same |
| 1677 | +list repeated. |
| 1678 | + """ |
| 1679 | + |
| 1680 | +parsons_solution=True |
| 1681 | + |
| 1682 | +hints=""" |
| 1683 | +The existing code is almost correct. |
| 1684 | +There are several ways to solve this. |
| 1685 | +Some solutions involve adding something small. |
| 1686 | +You can also rearrange the code without adding or removing anything (except spaces). |
| 1687 | +The problem is that a single list `row` is used several times. |
| 1688 | +So one solution is to make copies of `row` which will all be separate. |
| 1689 | +Another solution is to make a new `row` from scratch each time. |
| 1690 | +There are a few ways to copy a list in Python with a tiny bit of code. |
| 1691 | +Making a new row each time can be done by just rearranging the code. |
| 1692 | +""" |
| 1693 | + |
| 1694 | +defsolution(self): |
| 1695 | +defmake_board(size): |
| 1696 | +board= [] |
| 1697 | +for_inrange(size): |
| 1698 | +row= [] |
| 1699 | +for_inrange(size): |
| 1700 | +row.append(' ') |
| 1701 | +board.append(row) |
| 1702 | +returnboard |
| 1703 | + |
| 1704 | +returnmake_board |
| 1705 | + |
| 1706 | +tests= { |
| 1707 | +2: [ |
| 1708 | + [' ',' '], |
| 1709 | + [' ',' '] |
| 1710 | + ], |
| 1711 | +3: [ |
| 1712 | + [' ',' ',' '], |
| 1713 | + [' ',' ',' '], |
| 1714 | + [' ',' ',' '], |
| 1715 | + ], |
| 1716 | + } |
| 1717 | + |
| 1718 | +@classmethod |
| 1719 | +defgenerate_inputs(cls): |
| 1720 | +returndict(size=randint(4,12)) |
| 1721 | + |
| 1722 | +@classmethod |
| 1723 | +defcheck_result(cls,func,inputs,expected_result): |
| 1724 | +result=super().check_result(func,inputs,expected_result) |
| 1725 | +iflen(result)!=len(set(map(id,result))): |
| 1726 | +raiseExerciseError("The sublists in the result are not all separate objects") |
| 1727 | + |
| 1728 | +final_text=""" |
| 1729 | +Well done! |
| 1730 | +
|
| 1731 | +This could be solved by moving the first loop inside the second to make a new `row` each time: |
| 1732 | +
|
| 1733 | + def make_board(size): |
| 1734 | + board = [] |
| 1735 | + for _ in range(size): |
| 1736 | + row = [] |
| 1737 | + for _ in range(size): |
| 1738 | + row.append(' ') |
| 1739 | + board.append(row) |
| 1740 | + return board |
| 1741 | +
|
| 1742 | +Another way is to make a copy of `row` each time, e.g. keep the original code but change one line: |
| 1743 | +
|
| 1744 | + board.append(row.copy()) |
| 1745 | +
|
| 1746 | +You can also copy `row` with `row[:]` or `list(row)`. But it's important to know that |
| 1747 | +all these methods make a *shallow copy* of the list. |
| 1748 | +That means they copy the whole list at the top level, without making copies of each element. |
| 1749 | +That's fine in this case where `row` only contains strings which can't be modified |
| 1750 | +and don't need copying. But if the elements are mutable objects like lists, |
| 1751 | +as is the case with `board`, you may run into the same problem again. |
| 1752 | +Here's an example: |
| 1753 | +
|
| 1754 | + __copyable__ |
| 1755 | + def make_board(size): |
| 1756 | + row = [] |
| 1757 | + for _ in range(size): |
| 1758 | + row.append(' ') |
| 1759 | + board = [] |
| 1760 | + for _ in range(size): |
| 1761 | + board.append(row.copy()) |
| 1762 | + return board |
| 1763 | +
|
| 1764 | + def make_cube(size): |
| 1765 | + cube = [] |
| 1766 | + board = make_board(size) |
| 1767 | + for _ in range(size): |
| 1768 | + cube.append(board.copy()) |
| 1769 | + return cube |
| 1770 | +
|
| 1771 | + def test(): |
| 1772 | + cube = make_cube(2) |
| 1773 | + print(cube) |
| 1774 | + cube[0][0][0] = 'X' |
| 1775 | + print(cube) |
| 1776 | + print(cube[0] is cube[1]) |
| 1777 | + print(cube[0][0] is cube[0][1]) |
| 1778 | + print(cube[0][0] is cube[1][0]) |
| 1779 | +
|
| 1780 | + test() |
| 1781 | +
|
| 1782 | +Here each element of `cube` is a separate list, a copy of `board`. |
| 1783 | +And within each of those copies, each element is also a separate list, a copy of `row`. |
| 1784 | +But the shallow copies of `board` all have the same first element as each other (the first copy of `row`), |
| 1785 | +the same second element, and so on. |
| 1786 | +Changing `make_board` won't fix anything here, the solution is to either: |
| 1787 | +
|
| 1788 | +- Call `make_board` repeatedly to make a new `board` each time, or |
| 1789 | +- Use the `deepcopy` function instead of `board.copy()`. |
| 1790 | + `deepcopy` makes copies at every level of nested objects. |
| 1791 | +
|
| 1792 | +If you're still confused, don't worry. |
| 1793 | +This is just preparing you to deal with your code behaving weirdly in the future. |
| 1794 | +You're not required to understand this right now and this lesson will still be valuable. |
| 1795 | +
|
| 1796 | +Either way, we're ready to make the full game. You can do it! |
| 1797 | +""" |