-
Notifications
You must be signed in to change notification settings - Fork 0
Completed the about_classes koan #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: devel
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 dict
s. 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): |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)`
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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! |
There was a problem hiding this comment.
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())
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Completed and ready for review!