Skip to main content

Day 25 - Pyret to Python

Skills: 8

Pre-reading: 9.1.1, 9.1.2, 9.1.3, 9.1.4, 9.1.5, 9.1.6, 9.1.7

Supplementary Videos

Functions and Conditionals in Python, Lists and Dataclasses in Python

Intro (20 mins)

  • Python and Pyret share many core ideas, but use different notation and conventions -- more significantly, the Pyret programming environment is designed to be easy to use, whereas there is more involved setup for Python (as we saw yesterday).

Example: defining a function

  • Pyret:
    fun gadget-cost(num-gadgets :: Number, label :: String) -> Number:
    doc: "computes cost, at $0.50 per gadget plus $0.05 per character in label"
    num-gadgets * (0.50 + (string-length(label) * 0.05))
    where:
    gadget_cost(1, "hi") is 0.55
    gadget_cost(10, "tech") is 6.00
    end
  • Python:
    gadget.py
    def gadget_cost(num_gadgets: int, label: str) -> float:
    """ computes cost, at $0.50 per gadget plus $0.05 per character in label """
    return num_gadgets * (0.50 + (len(label) * 0.05))
    # Testing -- we'll come back to this

Example: Conditionals

  • Pyret:
    fun add-shipping(order-amt :: Number) -> Number:
    doc: "adds 4 for orders <= 10 (but non-zero), 8 for orders < 30, 12 for larger orders"
    if order-amt == 0:
    0
    else if order-amt <= 10:
    order-amt + 4
    else if order-amt < 30:
    order-amt + 8
    else:
    order-amt + 12
    end
    end
  • Python:
    gadget.py
    def add_shipping(order_amt: float) -> float:
    """ adds 4 for orders <= 10 (but non-zero), 8 for orders < 30, 12 for larger orders """
    if order_amt == 0:
    return 0
    elif order_amt <= 10:
    return order_amt + 4
    elif order_amt < 30:
    return order_amt + 8
    else:
    return order_amt + 12
    # Testing -- we'll come back to this

Testing

  • In Pyret, you use where: blocks for examples/tests. In Python, there are multiple ways to write tests, but a simple (and widely used) mechanism is using pytest, where you write separate test functions (whose names have to start with test_) using assert.

  • Furthermore, convention is that pytest tests are put in separate files, and those files should be named test_something.py (where something can be anything, but the file name must begin with test_). These test files should then import your code from the code file.

  • Save the above code in a file named gadget.py, and then create a new file test_gadget.py.

    test_gadget.py
    # pylint: disable=C0114, C0115, C0116, W0105, W0401
    import pytest
    # from the gadget.py file, get the function `gadget_cost`
    from gadget import gadget_cost

    def test_gadget_cost_small() -> None:
    assert gadget_cost(1, "hi") == pytest.approx(0.6)

    def test_gadget_cost_medium() -> None:
    assert gadget_cost(10, "tech") == pytest.approx(6.5)
    # Intentionally defined incorrectly to view a failing test

    (The first line, with the comment beginning with # pylint:, instructs pylint to not complain about missing doc strings, which are likely less important for test files than they are for code!)

  • Pytest Conventions:

    • Test functions must be named starting with test_.
    • Use assert to check expected results. It takes a boolean expression and causes a test failure if it evaluates to False.
    • Include only a single assert per function, since each test function will stop running at the first assertion failure, so if you have many within a single function, it will only run to the first test failure.
    • For floating-point (any decimal in Python) results, use pytest.approx for comparison. This is analogous to Pyret's is-roughly.
  • How to run tests:

    1. Save both files.
    2. In the VSCode Terminal (Terminal menu -> New Terminal), type:
      pytest
      And hit enter. (If this reports that the command is not found, please consult the previous day's instructions about installing pytest)
    3. pytest will find and run all functions named test_*, in all files named test_* and report results.
    4. One of the tests is defined with an incorrect expected value. Fix the expected value to 7 and re-run pytest to see all tests succeed!

Creating Lists

  • In Python, lists are created with square brackets, e.g.:
    fruits = ["apple", "banana", "cherry", "date"]
  • Python has built-in functions like filter and map for processing lists (just like you learned in Pyret, in Day 14), but you must wrap their results with list():
    fruits_with_a = list(filter(lambda f: "a" in f, fruits))
    # ["apple", "banana", "date"]
    fruits_upper = list(map(lambda f: f.upper(), fruits))
    # ["APPLE", "BANANA", "CHERRY", "DATE"]
  • Anonymous functions use lambda in Python -- but these are more restricted than Pyret's lam, as the body can be a single expression only (i.e., you cannot have an if, for, variable assignment, etc, inside a lambda).

Dataclasses

  • Python's @dataclass decorator, combined with the class feature, allows you to define structured data types, similar to structured data using a single variant with Pyret's data definitions (conditional data is more complicated, and not covered here). Example:
    from dataclasses import dataclass

    @dataclass
    class Fruit:
    name: str
    color: str

Class Exercises (35 mins)

  1. Function Syntax Practice

    • Rewrite the following Pyret function in Python:
      fun greet(name :: String) -> String:
      doc: "produces Hello, name!"
      "Hello, " + name + "!"
      where:
      greet("Alice") is "Hello, Alice!"
      greet("Bob") is "Hello, Bob!"
      end
      Be sure to follow full design recipe, including a docstring and test cases (in a separate file).
    • Now, design a Python function that takes a name and an age, and returns a string like "Alice is 20 years old."
  2. Conditionals

    • Translate this Pyret function to Python:
      fun shipping-cost(weight :: Number) -> Number:
      doc: "if weight <= 1, cost is $5; if weight <= 5, cost is $10; otherwise, $20"
      if weight <= 1:
      5
      else if weight <= 5:
      10
      else:
      20
      end
      end
  3. Return Statement

    • What happens if you forget the return statement in a Python function? Try it and observe the result.
  4. Create a list of integers from 1 to 10.

  5. Write an expression using map that produces a list of their squares.

  6. Design a function all_even(nums: list) -> list that returns a list of all even numbers from the input list.

  7. Design a function capitalize_all(words: list) -> list that returns a list of all words capitalized.

  8. Use map and lambda to produce a list of the lengths of each word in ["hello", "world", "python"].

  9. What happens if you forget to wrap the result in list()? Try it and note the error.

  10. Define a @dataclass called Book with fields title (str), author (str), and pages (int).

  11. Design a function long_books(books: list) -> list that returns a list of titles of books with more than 300 pages. Remember to always include docstring, type signature, and pytest tests.

  12. Create a list of Book instances and use long_books to filter them.

  13. Design a function filter_by_author(books: list, author: str) -> list that returns a list of titles of books by the given author.

Wrap-up (5 mins)

  • Pyret and Pyret share many aspects, but differ in some ways as well.