Skip to content

Conversation

stevendes
Copy link
Owner

Completed and ready for review!

@stevendes stevendes requested a review from glpuga January 17, 2020 17:48
Copy link

@AlejoAsd AlejoAsd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some questions and a number of comments to help make some things a little bit more clear.

self.assertEqual(__, getattr(fido, "_name"))
self.assertEqual('Fido', getattr(fido, "_name"))
# getattr(), setattr() and delattr() are a way of accessing attributes
# by method rather than through assignment operators

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you think of a situation where you would use getattr instead of accessing the attribute directly?

Put in another way, what does getattr let you do that accessing directly does not?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the main benefit is the fact that you can pass getattr a default value, so if for some reason the attribute it's not correctly taken, you can have other instead of going directly in a Exception.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is one very good reason.

The other big advantage is that you can get the properties of things using strings. This gives you a lot of flexibility because you don't need to know the name of the parameter beforehand. As an example, here's a simple function that gets you the values of all properties you request.

In [1]: def get_properties(object, properties): 
   ...:   """ 
   ...:   Gets the  
   ...:   :param object: Object from which to extract values. 
   ...:   :param properties: Properties to extract values for.
   ...:   """ 
   ...:   values = [] 
   ...:   for property in properties: 
   ...:     values.append(getattr(object, property, None)) 
   ...:  
   ...:   return values 
   ...:                                                                                                                                                                                                                                                                              

In [2]: class Test(object): 
   ...:   def __init__(self, a, b, c): 
   ...:     self.a = a 
   ...:     self.b = b 
   ...:     self.c = c 
   ...:                                                                                                                                                                                                                                                                              

In [3]: t = Test(1, 2, 3)                                                                                                                                                                                                                                                            

In [4]: get_properties(t, ('a', 'c', 'error'))                                                                                                                                                                                                                                                

In [5]: print(v)                                                                                                                                                                                                                                                                     
[1, 3, None]

self.assertEqual('Fido', fido.__dict__["_name"])
# Yes, this works here, but don't rely on the __dict__ object! Some
# class implementations use optimization which result in __dict__ not
# showing everything.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is not mentioned, something important to understand is that Python class instances are basically dicts. You can check the __dict__ property to see and access the contents of a class instance.

Any attribute that start with double underscores have their names mangled (changed) and will not be accessible directly. You can print the contents of __dict__ to see their new names.

# Define a Test class
>>> class Test(object):
...   def __init__(self, test):
...     self.__test = test
... 

# Get an instance of the Test class
>>> t = Test('test')

# __test is not accessible.
>>> t.__test
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Test' object has no attribute '__test'

# Not even when accessing directly through __dict__
>>> t.__dict__['__test']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: '__test'

# Checking __dict__ we can see that __test is stored as _Test__test instead
>>> t.__dict__
{'_Test__test': 'test'}

# If we try to access t._Test__test we get our value
>>> t._Test__test
'test'

class Dog(object):
"Dogs need regular walkies. Never, ever let them drive."

def test_instances_of_classes_can_be_created_adding_parentheses(self):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before I get deep into it, I want to set the terminology typically used when talking about classes. This is not Python specific, but it's important that we're using the same terminology so that the concepts are clearly understood.

In programming, you typically have variables and functions. There can be different types of variables (constants, static, etc.) and different types of functions (virtual, static, etc.), but the larger categories are variables and functions. Some programming languages make the distinction between functions and procedures based on if they return a value or not, respectively. We will not make this distinction but I thought I would mention it.

Classes use different words to refer to variables and functions. A variable that belongs to a class is called a property, and a function is called a method. Attribute is a way of generically saying "something that belongs or defines a class". Both properties and methods are considered attributes of a class.

Code Class
variable property
function/procedure method

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't' know this!


name = property(get_name, set_name)

#Comment: I'm not that sure on how property works

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

property(getter, setter) is one of Python's way of implementing getters and setters. If you're not familiar with the concept of getters and setters, you can find an explanation here. The example is in Java but the same concepts apply.

Getters and setters allow you to have control on how class properties are accessed and manipulated and are typically used to expose private properties in a controlled way. This allows you to even apply some logic before setting an attribute.

As an example, consider a class with a property that should always be a string.

In [1]: class Test(object):
   ...:   def __init__(self):
   ...:     # This value should ALWAYS contain a string
   ...:     self._string = ''
   ...: 
   ...:   def get_string(self):
   ...:     return self._string
   ...: 
   ...:   def set_string(self, value):
   ...:     """
   ...:     This setter makes sure that whatever value is set
   ...:     to `self.string` is converted to a string.
   ...:     """
   ...:     self._string = str(value)
   ...: 
   ...:   string = property(get_string, set_string)
   ...:   

In [2]: # Create an instance
   ...: t = Test()

In [3]: # Check the type and contents of string
   ...: print(t.string.__class__.__name__, t.string)
('str', '')

In [4]: # Try to set the value of string to a number
   ...: t.string = 1234

In [5]: # Check the type and contents of string
   ...: # Notice that it's a string and not a number
   ...: print(t.string.__class__.__name__, t.string)
('str', '1234')

Notice that we're getting and setting the value of the private property self._string, but we never access that property directly; all our calls were to t.value and not t._value. Getters and setters act as guards that control how private properties are accessed.

As another example, you can create a private attribute whose setter always raises an error. This would prevent someone from directly changing the value of a property, and force them to use some other class method to update the value.

In [1]: class Test(object):
   ...:   def __init__(self):
   ...:     # This value should not be accessed directly
   ...:     self._value = 0
   ...: 
   ...:   def get_value(self):
   ...:     return self._value
   ...: 
   ...:   def set_value(self, value):
   ...:     """
   ...:     The value should never be updated directly.
   ...:     """
   ...:     raise Exception('self._value cannot be accessed directly.')
   ...: 
   ...:   def add(self, amount):
   ...:     """
   ...:     Increases this instance's value by a certain amount.
   ...:     """
   ...:     self._value += amount
   ...: 
   ...:   def subtract(self, amount):
   ...:     """
   ...:     Decreases this instance's value by a certain amount.
   ...:     """
   ...:     self._value -= amount
   ...: 
   ...:   # Assign a getter and setter for self._value
   ...:   value = property(get_value, set_value)
   ...:   

In [2]: # Create an instance
   ...: t = Test()

In [3]: # Check the initial contents of value
   ...: print(t.value)
0

In [4]: # Try to assign a value directly
   ...: try:
   ...:   # This will fail
   ...:   t.value = 100
   ...: except Exception as e:
   ...:   print(str(e))
   ...:   
self._value cannot be accessed directly.

In [5]: # Increase the value by 10
   ...: t.add(10)
   ...: print(t.value)
   ...: 
10

In [6]: # Decrease the value by 5
   ...: t.subtract(5)
   ...: print(t.value)
   ...: 
5

Python doesn't really have the concept of private attributes. Prefixing an attribute with an underscore only suggests that it should not be accessed directly, but does not prevent someone from doing so. The guards we set in the previous example can easily be bypassed by accessing the private attribute t._value directly.

In [1]: class Test(object):
   ...:   def __init__(self):
   ...:     # This value should not be accessed directly
   ...:     self._value = 0
   ...: 
   ...:   def get_value(self):
   ...:     return self._value
   ...: 
   ...:   def set_value(self, value):
   ...:     """
   ...:     The value should never be updated directly.
   ...:     """
   ...:     raise Exception('self._value cannot be accessed directly.')
   ...: 
   ...:   def add(self, amount):
   ...:     """
   ...:     Increases this instance's value by a certain amount.
   ...:     """
   ...:     self._value += amount
   ...: 
   ...:   def subtract(self, amount):
   ...:     """
   ...:     Decreases this instance's value by a certain amount.
   ...:     """
   ...:     self._value -= amount
   ...: 
   ...:   # Assign a getter and setter for self._value
   ...:   value = property(get_value, set_value)
   ...:   

In [2]: # Create an instance
   ...: t = Test()

In [3]: # Access the private value directly
   ...: t._value = 1000
   ...: print(t.value)
   ...: 
1000

Coming back to property, it provides a way of generating an object that acts as a getter and setter for a property. property(<getter function>, <setter function>) returns an object that calls the getter function when the value is accessed, and the setter function when a value is assigned to it.

# Accessing a `property` object calls the getter function
`t.value` calls `t.get_value()`
# Assigning to a `property` object calls the setter function
`t.value = 100` calls `t.set_value(100)`

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great explanation!

#
return __
name = str(self._name)
return name

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct. You can also simply return the casted value.

def __str__(self):
  return str(self._name)

fido = self.Dog6("Fido")

self.assertEqual(__, fido.get_self()) # Not a string!
self.assertEqual(fido, fido.get_self()) # Not a string!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it clear why fido and fido.get_self() are equal?

Will the following pass?

fido1 = self.Dog6("Fido")
fido2 = self.Dog6("Fido")
self.assertEqual(fido1, fido2.get_self())

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fido is the object created and the method get_self returns self, which refers again to the object itself.

self.assertEqual(fido1, fido2.get_self()) would output an error because the fido2.get_self()" is reffering to the object fido2, which is different from the object fido1`, even if they are come from the same class.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right!


self.assertEqual(__, str("STRING"))
self.assertEqual(__, repr("STRING"))
#In the last one I'm not sure why the "" appears twice

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This page goes into the differences between str and repr. Give it a read and let me know if it clear things for you. If not, we can discuss about it.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand it, but we can discuss it next to see if what I understand it's correct, haha.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants