Diferències
Ací es mostren les diferències entre la revisió seleccionada i la versió actual de la pàgina.
| Ambdós costats versió prèvia Revisió prèvia | |||
| info:cursos:pue:python-pcpp1:m1:2.7 [05/11/2023 21:33] – suprimit - edició externa (Unknown date) 127.0.0.1 | info:cursos:pue:python-pcpp1:m1:2.7 [05/11/2023 21:33] (actual) – ↷ Page moved from info:cursos:pue:python-pcpp1:2.7 to info:cursos:pue:python-pcpp1:m1:2.7 mate | ||
|---|---|---|---|
| Línia 1: | Línia 1: | ||
| + | = 2.7 Encapsulation | ||
| + | == Attribute encapsulation | ||
| + | Encapsulation is one of the fundamental concepts in object-oriented programming (amongst inheritance, | ||
| + | Encapsulation is used to hide the **attributes** inside a class like in a **capsule**, | ||
| + | |||
| + | This picture presents the idea: direct access to the object attribute should not be possible, but you can always invoke methods, acting like proxies, to perform some actions on the attributes. | ||
| + | |||
| + | {{ : | ||
| + | |||
| + | Python introduces the concept of **properties** that act like proxies to encapsulated attributes. | ||
| + | |||
| + | This concept has some interesting features: | ||
| + | |||
| + | * the code calling the proxy methods might not realize if it is " | ||
| + | * in Python, you can change your class implementation from a class that allows simple and direct access to attributes to a class that fully controls access to the attributes, and what is most important –consumer implementation does not have to be changed; by consumer we understand someone or something (it could be a legacy code) that makes use of your objects. | ||
| + | |||
| + | Let's start with a few analogies from real life: Imagine a washing machine door ( **object** ) that protects access to your laundry ( **attribute values** ) while your appliance is washing it ( **processing** ). You have a set of controls ( **methods** ) that allow you to manage your laundry, or even see it (many wash machines are equipped with a transparent window). | ||
| + | |||
| + | So, while the washing machine is processing your laundry, you are not able to directly access the laundry. This is how attribute encapsulation works. | ||
| + | |||
| + | Another good example is a money bank; this time it’s a more IT-related example: | ||
| + | |||
| + | When your money ( **attribute value** ) is deposited in the bank account ( **object** ), you **cannot** access it directly and without some checks or security. This is a basic countermeasure to protect your account from withdrawals exceeding certain limits or the account balance. But you can always make use of some dedicated interfaces (methods like a mobile application or a web application) to spend money up to an appropriate amount. | ||
| + | |||
| + | Attribute encapsulation can be also used to limit unauthorized access: reading and modifying the account balance. Remember that this is not full access control, the programmer can still get access to your attributes **intentionally** as Python does not deliver true privacy. | ||
| + | |||
| + | Why? | ||
| + | |||
| + | Guido Van Rossum, best known as the author of Python, once said: "// | ||
| + | |||
| + | So, if your code does intentionally access the attributes marked as private (prefixed with a double underscore) in a direct way, then remember that this behavior is **unpythonic**. | ||
| + | |||
| + | The last example could be presented with the behavior of a class representing a water or fuel tank: | ||
| + | |||
| + | It would not be wise to pour any amount of liquid **directly** into the tank ( **object** ) exceeding the total tank capacity, or request setting the liquid level to a negative value. | ||
| + | |||
| + | Python allows you to control access to attributes with the built-in '' | ||
| + | |||
| + | This decorator plays a very important role: | ||
| + | * it designates a method which will be called automatically when another object wants to read the encapsulated attribute value; | ||
| + | * the name of the designated method will be used as the name of the instance attribute corresponding to the encapsulated attribute; | ||
| + | * it should be defined before the method responsible for setting the value of the encapsulated attribute, and before the method responsible for deleting the encapsulated attribute. | ||
| + | |||
| + | Let's have look at the code in the editor. | ||
| + | |||
| + | <code python> | ||
| + | class TankError(Exception): | ||
| + | pass | ||
| + | |||
| + | |||
| + | class Tank: | ||
| + | def __init__(self, | ||
| + | self.capacity = capacity | ||
| + | self.__level = 0 | ||
| + | |||
| + | @property | ||
| + | def level(self): | ||
| + | return self.__level | ||
| + | |||
| + | @level.setter | ||
| + | def level(self, amount): | ||
| + | if amount > 0: | ||
| + | # fueling | ||
| + | if amount <= self.capacity: | ||
| + | self.__level = amount | ||
| + | else: | ||
| + | raise TankError(' | ||
| + | elif amount < 0: | ||
| + | raise TankError(' | ||
| + | |||
| + | @level.deleter | ||
| + | def level(self): | ||
| + | if self.__level > 0: | ||
| + | print(' | ||
| + | self.__level = None | ||
| + | </ | ||
| + | |||
| + | We see that every **Tank** class object has a '' | ||
| + | |||
| + | The @property decorated method is a method to be called when some other code wants to read the level of liquid in our tank. We call such a read method **getter**. | ||
| + | |||
| + | Pay attention to the fact that the method following the decorator gives the name (tank) to the attribute visible outside of the class. Moreover, we see that two other methods are named the same way, but as we are using specially crafted decorators to distinguish them, this won’t cause any problems: | ||
| + | * '' | ||
| + | * '' | ||
| + | |||
| + | As those attribute name repetitions could be misleading, let's explain the naming convention: | ||
| + | * the getter method is decorated with ' | ||
| + | * the setter method is decorated with ' | ||
| + | * the deleter method is decorated with ' | ||
| + | |||
| + | Let's instantiate the class and perform some operations on the object' | ||
| + | |||
| + | <code python> | ||
| + | class TankError(Exception): | ||
| + | pass | ||
| + | |||
| + | |||
| + | class Tank: | ||
| + | def __init__(self, | ||
| + | self.capacity = capacity | ||
| + | self.__level = 0 | ||
| + | |||
| + | @property | ||
| + | def level(self): | ||
| + | return self.__level | ||
| + | |||
| + | @level.setter | ||
| + | def level(self, amount): | ||
| + | if amount > 0: | ||
| + | # fueling | ||
| + | if amount <= self.capacity: | ||
| + | self.__level = amount | ||
| + | else: | ||
| + | raise TankError(' | ||
| + | elif amount < 0: | ||
| + | raise TankError(' | ||
| + | |||
| + | @level.deleter | ||
| + | def level(self): | ||
| + | if self.__level > 0: | ||
| + | print(' | ||
| + | self.__level = None | ||
| + | |||
| + | # our_tank object has a capacity of 20 units | ||
| + | our_tank = Tank(20) | ||
| + | |||
| + | # our_tank' | ||
| + | our_tank.level = 10 | ||
| + | print(' | ||
| + | |||
| + | # adding additional 3 units (setting liquid level to 13) | ||
| + | our_tank.level += 3 | ||
| + | print(' | ||
| + | |||
| + | # let's try to set the current level to 21 units | ||
| + | # this should be rejected as the tank's capacity is 20 units | ||
| + | try: | ||
| + | our_tank.level = 21 | ||
| + | except TankError as e: | ||
| + | print(' | ||
| + | |||
| + | # similar example - let's try to add an additional 15 units | ||
| + | # this should be rejected as the total capacity is 20 units | ||
| + | try: | ||
| + | our_tank.level += 15 | ||
| + | except TankError as e: | ||
| + | print(' | ||
| + | |||
| + | # let's try to set the liquid level to a negative amount | ||
| + | # this should be rejected as it is senseless | ||
| + | try: | ||
| + | our_tank.level = -3 | ||
| + | except TankError as e: | ||
| + | print(' | ||
| + | |||
| + | print(' | ||
| + | |||
| + | del our_tank.level | ||
| + | |||
| + | </ | ||
| + | |||
| + | As you can see, access to the '' | ||
| + | |||
| + | The other code can make use of the ' | ||
| + | |||
| + | It’s worth mentioning another useful and interesting feature of properties: properties are inherited, so you can call setters as if they were attributes. | ||
| + | |||
| + | Examine the code and run it to see if it follows your expectations. | ||
| + | |||
| + | <code ; output> | ||
| + | Current liquid level: 10 | ||
| + | Current liquid level: 13 | ||
| + | Trying to set liquid level to 21 units, result: Too much liquid in the tank | ||
| + | Trying to add an additional 15 units, result: Too much liquid in the tank | ||
| + | Trying to set liquid level to -3 units, result: Not possible to set negative liquid level | ||
| + | Current liquid level: 13 | ||
| + | It is good to remember to sanitize the remains from the tank! | ||
| + | </ | ||
| + | |||
| + | == LAB | ||
| + | === Objectives | ||
| + | * improving the student' | ||
| + | * improving the student' | ||
| + | === Scenario | ||
| + | * Implement a class representing an account exception, | ||
| + | * Implement a class representing a single bank account, | ||
| + | * This class should control access to the account number and account balance attributes by implementing the properties: | ||
| + | * it should be possible to read the account number only, not change it. In case someone tries to change the account number, raise an alarm by raising an exception; | ||
| + | * it should not be possible to set a negative balance. In case someone tries to set a negative balance, raise an alarm by raising an exception; | ||
| + | * when the bank operation (deposit or withdrawal) is above 100.000, then additional message should be printed on the standard output (screen) for auditing purposes; | ||
| + | * it should not be possible to delete an account as long as the balance is not zero; | ||
| + | * test your class behavior by: | ||
| + | * setting the balance to 1000; | ||
| + | * trying to set the balance to -200; | ||
| + | * trying to set a new value for the account number; | ||
| + | * trying to deposit 1.000.000; | ||
| + | * trying to delete the account attribute containing a non-zero balance. | ||
| + | |||
| + | === resposta | ||
| + | <code python> | ||
| + | # | ||
| + | # -*- coding: utf-8 -* | ||
| + | |||
| + | class AccountError(Exception): | ||
| + | pass | ||
| + | |||
| + | class Account(): | ||
| + | MAX_ALERT_AUDITING: | ||
| + | | ||
| + | def __init__(self, | ||
| + | self.__account = account | ||
| + | self.__balance = 0 | ||
| + | | ||
| + | @property | ||
| + | def account(self): | ||
| + | return self.__account | ||
| + | | ||
| + | @account.setter | ||
| + | def account(self, | ||
| + | raise AccountError(" | ||
| + | | ||
| + | @account.deleter | ||
| + | def account(self): | ||
| + | if self.__balance != 0: | ||
| + | raise AccountError(" | ||
| + | else: | ||
| + | self.__account = None | ||
| + | |||
| + | | ||
| + | @property | ||
| + | def balance(self): | ||
| + | return self.__balance | ||
| + | | ||
| + | @balance.setter | ||
| + | def balance(self, | ||
| + | if money < 0: | ||
| + | raise AccountError(" | ||
| + | if (abs(money) > Account.MAX_ALERT_AUDITING): | ||
| + | print(" | ||
| + | self.__balance = money | ||
| + | | ||
| + | @balance.deleter | ||
| + | def balance(self): | ||
| + | if self.__balance != 0: | ||
| + | raise AccountError(" | ||
| + | else: | ||
| + | self.__balance = None | ||
| + | | ||
| + | |||
| + | try: | ||
| + | A1 = Account(" | ||
| + | except AccountError as e: | ||
| + | print(" | ||
| + | | ||
| + | try: | ||
| + | A1.balance = 1000 | ||
| + | except AccountError as e: | ||
| + | print(" | ||
| + | | ||
| + | try: | ||
| + | A1.balance = -200 | ||
| + | except AccountError as e: | ||
| + | print(" | ||
| + | |||
| + | try: | ||
| + | A1.account = " | ||
| + | except AccountError as e: | ||
| + | print(" | ||
| + | | ||
| + | try: | ||
| + | A1.balance = 1000000 | ||
| + | except AccountError as e: | ||
| + | print(" | ||
| + | |||
| + | try: | ||
| + | del A1.account | ||
| + | except AccountError as e: | ||
| + | print(" | ||
| + | |||
| + | </ | ||
| + | | ||
| + | <code ; output> | ||
| + | ERROR: | ||
| + | ERROR: | ||
| + | Atenció! Moviment per ser auditat | ||
| + | ERROR: | ||
| + | </ | ||