-
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,10 +10,10 @@ class Dog(object): | |
|
||
def test_instances_of_classes_can_be_created_adding_parentheses(self): | ||
fido = self.Dog() | ||
self.assertEqual(__, fido.__class__.__name__) | ||
self.assertEqual('Dog', fido.__class__.__name__) | ||
|
||
def test_classes_have_docstrings(self): | ||
self.assertMatch(__, self.Dog.__doc__) | ||
self.assertMatch("Dogs need regular walkies. Never, ever let them drive.", self.Dog.__doc__) | ||
|
||
# ------------------------------------------------------------------ | ||
|
||
|
@@ -26,24 +26,24 @@ def set_name(self, a_name): | |
|
||
def test_init_method_is_the_constructor(self): | ||
dog = self.Dog2() | ||
self.assertEqual(__, dog._name) | ||
self.assertEqual('Paul', dog._name) | ||
|
||
def test_private_attributes_are_not_really_private(self): | ||
dog = self.Dog2() | ||
dog.set_name("Fido") | ||
self.assertEqual(__, dog._name) | ||
self.assertEqual("Fido", dog._name) | ||
# The _ prefix in _name implies private ownership, but nothing is truly | ||
# private in Python. | ||
|
||
def test_you_can_also_access_the_value_out_using_getattr_and_dict(self): | ||
fido = self.Dog2() | ||
fido.set_name("Fido") | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Can you think of a situation where you would use Put in another way, what does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the main benefit is the fact that you can pass There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.__dict__["_name"]) | ||
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 commentThe 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 Any attribute that start with double underscores have their names mangled (changed) and will not be accessible directly. You can print the contents of # 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' |
||
|
@@ -62,12 +62,14 @@ def get_name(self): | |
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more.
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 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 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 # 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 commentThe reason will be displayed to describe this comment to others. Learn more. Great explanation! |
||
|
||
def test_that_name_can_be_read_as_a_property(self): | ||
fido = self.Dog3() | ||
fido.set_name("Fido") | ||
|
||
self.assertEqual(__, fido.get_name()) # access as method | ||
self.assertEqual(__, fido.name) # access as property | ||
self.assertEqual('Fido', fido.get_name()) # access as method | ||
self.assertEqual('Fido', fido.name) # access as property | ||
|
||
# ------------------------------------------------------------------ | ||
|
||
|
@@ -87,7 +89,7 @@ def test_creating_properties_with_decorators_is_slightly_easier(self): | |
fido = self.Dog4() | ||
|
||
fido.name = "Fido" | ||
self.assertEqual(__, fido.name) | ||
self.assertEqual('Fido', fido.name) | ||
|
||
# ------------------------------------------------------------------ | ||
|
||
|
@@ -101,19 +103,21 @@ def name(self): | |
|
||
def test_init_provides_initial_values_for_instance_variables(self): | ||
fido = self.Dog5("Fido") | ||
self.assertEqual(__, fido.name) | ||
self.assertEqual("Fido", fido.name) | ||
|
||
def test_args_must_match_init(self): | ||
self.assertRaises(___, self.Dog5) # Evaluates self.Dog5() | ||
self.assertRaises(TypeError, self.Dog5) # Evaluates self.Dog5() | ||
|
||
# THINK ABOUT IT: | ||
# Why is this so? | ||
|
||
#Class initialization expects 2 arguments and only gets 1 (self) | ||
|
||
def test_different_objects_have_different_instance_variables(self): | ||
fido = self.Dog5("Fido") | ||
rover = self.Dog5("Rover") | ||
|
||
self.assertEqual(____, rover.name == fido.name) | ||
self.assertEqual(False, rover.name == fido.name) | ||
|
||
# ------------------------------------------------------------------ | ||
|
||
|
@@ -125,36 +129,36 @@ def get_self(self): | |
return self | ||
|
||
def __str__(self): | ||
# | ||
# Implement this! | ||
# | ||
return __ | ||
name = str(self._name) | ||
return name | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
||
|
||
def __repr__(self): | ||
return "<Dog named '" + self._name + "'>" | ||
|
||
def test_inside_a_method_self_refers_to_the_containing_object(self): | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Is it clear why 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 commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's right! |
||
|
||
def test_str_provides_a_string_version_of_the_object(self): | ||
fido = self.Dog6("Fido") | ||
self.assertEqual("Fido", str(fido)) | ||
|
||
def test_str_is_used_explicitly_in_string_interpolation(self): | ||
fido = self.Dog6("Fido") | ||
self.assertEqual(__, "My dog is " + str(fido)) | ||
self.assertEqual("My dog is Fido", "My dog is " + str(fido)) | ||
|
||
def test_repr_provides_a_more_complete_string_version(self): | ||
fido = self.Dog6("Fido") | ||
self.assertEqual(__, repr(fido)) | ||
self.assertEqual("<Dog named 'Fido'>", repr(fido)) | ||
|
||
def test_all_objects_support_str_and_repr(self): | ||
seq = [1, 2, 3] | ||
|
||
self.assertEqual(__, str(seq)) | ||
self.assertEqual(__, repr(seq)) | ||
self.assertEqual('[1, 2, 3]', str(seq)) | ||
self.assertEqual('[1, 2, 3]', repr(seq)) | ||
|
||
self.assertEqual('STRING', str("STRING")) | ||
self.assertEqual("'STRING'", repr("STRING")) | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. This page goes into the differences between There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
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.
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!