Skip to content
Snippets Groups Projects
Commit 50a04580 authored by Thomas Röck's avatar Thomas Röck
Browse files

removed assignment 1 spoiler

parent e420f05f
No related branches found
No related tags found
No related merge requests found
%% Cell type:markdown id: tags:
# Intro to OOP in Python II - Attribute Access & Properties
## Attribute Access
In the last session we heard about attributes inside of objects. These attributes can be accessed from outside of an object.
Let us think of a case where we don't want possible users of our code to modify certain attributes directly, what can we do?
Other programming languages have the concepts of private and public to determine whether attributes and methods can be accessed from outside of the object. In python everything is "public". This means, by default, any attribute can be modified from anywhere else in the program without any restrictions. Also, any attribute could be accessed (maybe before they are even specified), possibly causing methods relying on their existence to crash.
If you don't want someone to modify certain attributes directly, you should prefix them with an underscore (`_`). This doesn't actually do anything, but by convention means: "Please don't change me, or something unexpected might happen". E.g. say you use a certain data structure for an attribute, but might change it in the future, users should not rely on that variable always having the same structure. We call this attributes non-public attributes in comparison to public attributes without the underscore (`_`).
Further one can add two underscores (`__`) as a prefix for an attribute. This activates a feature called "name mangling", which means any attribute with two leading underscores (e.g. `__attributename`) is textually replaced with `_ClassName__attributename` (where `ClassName` stands for the name of the class). These attributes are called *class-private* attributes. This is especially useful when using inheritance, which we will look at in a later chapter.
More informations can be found in the [official python tutorial on classes](https://docs.python.org/3/tutorial/classes.html#private-variables).
#### Again in short form:
Every attribute in python is public, but we introduced a naming convention for public and non-public attributes.
**"public" attributes**
All attributes **without** any leading underscores (`_`)
>```python
self.plants = [] # this is a public attribute
>```
**"non-public" attributes**
All attributes **with** a single leading underscore (`_`)
>```python
self._plants = [] # this is a non public attribute
>```
**"class private" attributes**
All attributes **with** double leading underscores (`__`)
>```python
self.__plants = [] # this is a class private attribute
>```
%% Cell type:markdown id: tags:
Now we know about public and non-public attributes. Let's use them in the `Bed` class from last time. Here we had the *public* attribute `plants`. This attribute is only important inside the class and should not be accessed from outside the class. Thus, we define this attribute as *non-public* by adding an underscore in front of the attribute. Again, this does not influence the code behaviour at all! It just lets the user know that this attribute *should* not be accessed or changed from outside.
%% Cell type:code id: tags:
``` python
class Bed:
def __init__(self, location: str) -> None:
self.location = location
self._plants = [] # this is now a non-public attribute
self.water_level = 0
```
%% Cell type:code id: tags:
``` python
bed = Bed("garden")
# bed.plants
```
%% Cell type:markdown id: tags:
As we see when trying to print the attribute `plants`, we get an error that it doesn't exist - since we defined it *with* a leading `_`, this is exactly as expected. If there's no debugger available, a useful way to list all attributes inside an object is looking at an object's `__dict__`. This attribute exists for all objects and contains all the object's attributes and their values as key-value-pairs.
%% Cell type:code id: tags:
``` python
bed.__dict__
```
%% Cell type:markdown id: tags:
## Properties
Non-public attributes might contain data which should be accessible from outside the object. They could be intended to be read-only or require a new value to have certain characteristics. To provide a simple and intuitive interface, while being able to perform verifications or calculations when an attibute is read or changed, python provides *properties*. Using properties allows to define methods to be called in different attribute access contexts. In simpler terms, you can define an arbitrary set of statements to be executed in each of these three situations:
**Reading access**
>```python
bed.plants
print(bed.plants)
list_of_plants = bed.plants
>```
**Assignment**
>```python
bed.plants = []
bed.plants = "whatever"
>```
**Deletion**
>```python
del bed.plants
>```
%% Cell type:markdown id: tags:
Applying the decorator `@property` to a method will create a property attribute using the method's name. Even though a method (expecting only `self`) is implemented, it doesn't need to be called explicitly with parentheses - actually it can't be called at all. The method is called automatically when the attribute is accessed in a _reading_ context (i.e. a _'getter'_). A property can expose an existing non-public attribute publicly (like `_water_level` in the example below) for reading access. It can also create a composite attribute, which is computed from other attributes 'on-the-fly' at access time (like `needs_water`).
%% Cell type:code id: tags:
``` python
class Bed:
def __init__(self, location: str) -> None:
self.location = location
self._plants = []
self._water_level = 0
self._WATERING_THRESHOLD = 5
@property
def water_level(self) -> int:
return self._water_level
@property
def needs_water(self) -> bool:
return self.water_level < self._WATERING_THRESHOLD
def water(self, added_water: int = 1) -> None:
self._water_level += added_water
bed = Bed("garden")
print(bed.water_level)
print(bed.needs_water)
bed.water(10)
print(bed.water_level)
print(bed.needs_water)
bed.water_level = 10 # no setter defined yet -> AttributeError
```
%% Cell type:markdown id: tags:
Decorating a method with `@attributename.setter` will lead to it being called when `attributename` is assigned to (i.e. a _'setter'_). In this case, the method's name can be arbitrary, but for clarity using `attributename` is common. The method expects a second parameter (after `self`), which receives the value to be assigned (i.e. the value to the right of the `=`). This requires the existence of a property called `attributename`. We can use the setter for extra type/valuechecks before the new value of the attribute is set (e.g. make sure `water_level` is always positive).
%% Cell type:code id: tags:
``` python
class Bed:
def __init__(self, location: str) -> None:
self.location = location
self._plants = []
self.water_level = 0
@property
def water_level(self) -> int:
return self._water_level
@water_level.setter
def water_level(self, water_level: int):
if water_level < 0:
raise ValueError(f"No more water in {self!r}.")
self._water_level = water_level
bed = Bed("garden")
print(bed.water_level)
bed.water_level = 10
print(bed.water_level)
bed.water_level = -5
```
%% Cell type:markdown id: tags:
The decorator `@attributename.deleter` creates a _'deleter'_. The decorated method will be called on attribute deletion and expects only `self`. It could e.g. perform cleanup actions or implement custom deletion behavious. This also requires the existence of a property called `attributename`.
%% Cell type:code id: tags:
``` python
# The class Plant since it is needed in the class Bed
class Plant:
def __init__(self, species: str, growth_factor: int):
self.species = species
self._growth_factor = growth_factor
self._size = 1
def __repr__(self):
return f"{self.species}"
class Bed:
def __init__(self, location: str) -> None:
self.location = location
self._plants = []
self.water_level = 0
self._WATERING_THRESHOLD = 5
@property
def plants(self) -> list:
return self._plants
# 'Basic deleter'
# @plants.deleter
# def plants(self):
# del self._plants
# Custom behaviour
@plants.deleter
def plants(self):
self._plants.clear()
def plant_plant(self, plant: Plant) -> None:
self.plants.append(plant)
bed = Bed("garden")
print(bed.plants)
bed.plant_plant(Plant("strawberry", 1.3))
print(bed.plants)
del bed.plants
print(bed.plants)
```
%% Cell type:markdown id: tags:
Combining these additions leads to this implementation of the class `Bed`:
%% Cell type:code id: tags:
``` python
class Bed:
def __init__(self, location: str) -> None:
self.location = location
self._plants = []
self.water_level = 0
self._WATERING_THRESHOLD = 5
def __repr__(self) -> str:
return f"Bed {self.location}"
def __str__(self) -> str:
return (f"{self!r}\n"
f"Water level: {self.water_level}\n"
f"{len(self.plants)} plants: {[p.species for p in self.plants]}")
@property
def water_level(self) -> int:
return self._water_level
@water_level.setter
def water_level(self, water_level: int):
if water_level < 0:
raise ValueError(f"No more water in {self!r}.")
self._water_level = water_level
@property
def plants(self) -> list:
return self._plants
@plants.deleter
def plants(self):
self._plants.clear()
@property
def needs_water(self) -> bool:
return self.water_level < self._WATERING_THRESHOLD
def plant_plant(self, plant: Plant) -> None:
self.plants.append(plant)
def water(self, added_water: int = 1) -> None:
self.water_level += added_water
def update(self) -> None:
for plant in self.plants:
try:
self.water_level -= 1
plant.grow()
except ValueError:
print(f"Bed {self.location} is out of water, plants can't grow")
return
```
%% Cell type:markdown id: tags:
# Exercise
We will now extend the program we started in Tutorial 01. The following little tasks should help you strengthen your knowledge about attribute access and properties in python classes.
The solutions to the exercises will be published in the [TeachCenter](https://tc.tugraz.at/main/mod/folder/view.php?id=262267) and on [GitLab](https://gitlab.tugraz.at/DFF59FD34746BBC2/22s_info2_garden).
Find a handy git-tutorial [here](https://rogerdudler.github.io/git-guide/).
## TODOs:
### Plant class
#### Required Properties
| Name | Type | Getter | Setter | Deleter | Description |
| -------- | -------- | ------- | -------- | -------- | -------- |
| `size` | `int` | yes | no | no | The size of the plant, initialized with `1`. |
| `growth_factor` | `float` | yes | yes | no | The growth factor of a plant. If smaller than 1, raise an Exception. |
| `fruits` | `list[Fruit]` | yes | no | yes | The IBAN of the bank account. |
| `fruits` | `list[Fruit]` | yes | no | yes | A list of Fruit-objects growing on the plant |
* write a getter method for the attribute `size`
* write a getter method for the attribute `growth_factor`
* now write a setter method for the attribute `growth_factor`. Here you want to check if the the `growth_factor` is smaller than `1`. If so, raise an appropriated Exception with a discriptive error message.
* write a getter method for the attribute `fruits` (it is not possible to implement a deleter without implementing a getter property first)
* now write a deleter method for the attribute `fruits` where the list's content is cleared
### Bed class
* write a new method `harvest(self)`:
* iterate over your `plants`
* while there is still `Fruit` to harvest on a plant, pick them one by one with the `pick` method from the `Plant` class
* return a list containing all the harvested `Fruit`
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment