Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Hint: You can adjust the default video playback speed in your account settings.
Hint: You can set your subtitle preferences in your account settings.
Sorry! Looks like there’s an issue with video playback 🙁 This might be due to a temporary outage or because of a configuration issue with your browser. Please refer to our video player troubleshooting guide for assistance.

Utilizing Composition

00:00 If you remember, composition is an object-oriented design concept that models a has a or part of relationship. Technically, we’ve already been using composition.

00:15 For example, our Employee class has two attributes, an .id and a .name, but these attributes are initialized with integers and strings, respectively—which is fine, but what’s more interesting is having attributes of some other type that we create. In this video, you are going to create a new class called Address, which models an address that an employee might want to store in the payroll system so it knows where to send checks to. To start, I’m going to create a new module and I’ll save it as contacts.py.

01:00 I want to create an Address class to model a street address. You might be thinking, “Why not just store that as a string?” The problem is that’s not complex enough for our needs.

01:14 What if we just want to get the street address, or the state, or the zip code? With so many possible street addresses, it would be hard to write an algorithm that would parse an address as a string and return just one part of that, especially when you consider that many streets share the same names as cities.

01:37 Instead, we will define each section of the address as a separate instance attribute. As usual, we’ll start with the .__init__() method.

01:49 This will take self, a street, a city, a state, a zip code, and optionally, a second street address called street2. This equal sign (=) allows us to assign a default value for this argument, which will be used if the user chooses not to pass in a second street address.

02:14 Basically, I’m saying, “Accept a second street address from the user, but if they don’t supply one, set it equal to an empty string.” Now, I’ll create instance attributes for each of these parameters.

02:30 This works, but I think we can take this one step further. Right now, if we ever wanted to print out the entire address of an Address object, we’d have to concatenate the streets, plus the city, plus the state, plus the zip code.

02:47 Let’s create a method in here that will return a nicely formatted string with all the information about the current Address object. We could call this method whatever we want, and then call it on the object like we would any method—but this presents an opportunity to practice a little bit of inheritance.

03:09 If you remember from an earlier video, I said that every custom class we create in Python 3 implicitly inherits from the object superclass, which itself defines magic methods that Python uses to manage the object. Well, one of those magic methods is called .__str__(), short for string, which is used to obtain a string representation of the current object. Because our Address class inherits from object, it’s got a .__str__() method too. In fact, let’s see what it spits out. Down here, I’m going to instantiate this class with some random address.

03:57 I’m going to leave the second street blank. Now, I’ll print the result of calling .__str__() on this object,

04:07 and actually, this is a little bit redundant. What’s special about .__str__() is that it’s used by other parts of the Python language, including the print() function.

04:18 What this means is that if I just pass this object into our print() function here, the print() function will print out the string representation of the object, which it will obtain by calling this .__str__() method under the hood.

04:36 I’ll run this and, well, as you can tell, the default string representation of a custom object in Python is pretty boring. We can modify it by providing a new implementation of .__str__(), effectively overriding the implementation inherited from the object superclass, which is currently super boring.

05:01 I’ll move back into the class here and I’ll create this .__str__() method. In order for everything to work, this method has to return a string.

05:12 It’s not supposed to just print something. So, let’s build a string. I’m going to start by creating a new variable called lines, which I will set to a new list containing the .street attribute.

05:27 Now, I want to append the second street address to the lines list, but only if it’s not empty.

05:36 We also need to append one more line, which will contain the .city, .state, and .zipcode in the form of an f-string.

05:47 Finally, I’ll return the newline character joined by our lines list. This will return a string where the end of each line—or the end of each element in the list—is separated by a newline character, which pushes the remaining text onto the next line.

06:07 Now, I will run this module again and we can see the string representation of the Address object is displayed in the output panel. All that’s left to do now is modify the rest of the program to use this new Address class. Before I do that though, I want to delete this object and print() call down here so it doesn’t start interfering with our main module.

06:36 Now, I’m going to move into the employees module and I’ll add a new instance attribute to the Employee class called .address.

06:47 There are a couple of things to notice here. One, notice how I didn’t import the address module. That’s because Python doesn’t require us to assign types to these instance attributes, so this class doesn’t need to know about the Address class.

07:05 This is called a loosely-coupled relationship. The composite class, Employee, doesn’t need to know about the component, the Address.

07:16 The other thing to notice here is that I’ve assigned the new attribute a value of None and I didn’t include a parameter for it in the .__init__() method.

07:26 What this means is that we won’t be assigning an Employee an Address when we create the Employee. Instead, we have to add this .address attribute after the object has initially been constructed.

07:41 The relationship between Employee and Address now looks like this. The Employee is composed of 0 or 1 Address objects, which defines every part of the street address.

07:59 The next thing we want to do is modify the PayrollSystem to print out the employee address when it’s calculating the payroll. I’ll move over to the hr module and inside the PayrollSystem’s .calculate_payroll() method, I want to do a check to see if the current employee being processed has an .address attribute. The code inside here will only be called if the .address instance attribute is not set to None.

08:32 If that’s the case, I’ll print "- Sent to:" and then the employee.address.

08:40 Now that we finished working on our classes, we can move back into the program module and give some of our employees an address. Before we can do that, we have to import contacts so that this module knows about the Address class.

08:58 I’ll add an Address object to the Manager first. Let’s move down to a new line and type manager.address = contacts.Address(). And then in the parentheses, we need to specify each part of the address as a string.

09:21 We’re utilizing the Address class’s .__init__() method to construct a new Address object, and then we assign that object to the Manager’s .address attribute after the Manager has already been created.

09:36 This means that we’re not using the Manager’s .__init__() method. We’re doing a manual assignment after the fact. What’s good about not using the .__init__() method is that we don’t have to give every Employee object an Address.

09:52 We just assign it to the employees that we want after they’ve been created. I’ll give the Secretary an address, too.

10:01 Now, fancy salary employees like the manager or the secretary will have their paychecks sent to them.

10:11 And it looks like that’s exactly what’s happening. You’ll notice that when an employee that has an address’s payroll is calculated, it says - Sent to: and then their address.

10:24 You’ve now learned how you can create a custom Address type that can be used by the Employee class. This Address class is completely independent of the Employee, and the Employee class utilizes Address objects without any knowledge of them.

10:43 This means that we can change the Address class without having any impact on the Employee class directly.

dwalsh on May 23, 2020

I freaked out with joy when you added a Concord NH address at time 9:34. I am from Concord NH!

dwalsh on May 27, 2020

I have a minor question. Would it technically be more Pythonic to make the argument street2=None as the if statement for street2 is going to just append empty str anyways? I tested it and the output is the same either way so it doesn’t matter but I’m just trying to also get a grasp about PEP.

Roy Telles on Aug. 14, 2020

I was able to look this info up regarding None and empty strings in Python:

In Python, best practices are to use None as a sentinel value when an exception isn’t warranted. Use an empty string when the result should be a string and would sensibly be empty, or as input to something that expects strings and you want to use a blank string, but not otherwise.

From this Quora article.

I would imagine this is because None is its own datatype and not the same as 0, False, or an empty string. And so declaring None as the default could propagate issues down the line.

Also in a REPL, we can check the available attributes of None compared to the empty string using dir():

>>> dir('')
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
>>> dir(None)
['__bool__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

Clearly we can do a lot more with the empty string than we can with None. This could lead to less exceptions being raised if we use the empty string as our program gets complex.

dbristow31 on Feb. 1, 2021

I tried running the code at the end of this video, and the Shell returned an AttributeError, ‘Salesperson’ object has no attribute ‘address.’ I thought that if I did not include ‘address’ as a parameter in the Employee init method, then I would not get this error.

dbristow31 on Feb. 1, 2021

Please disregard my previous comment. I was able to solve my problem.

Francois on March 1, 2021

Great explanation of this topic, thanks!

Thank you for the series!

Question -

What is the advantage or disadvantage if I chose to assign an address to an employee in the following way?

def __init__(self, id, name, address=None):
    self.id=id
    self.name=name
    self.address=address

and then use a, say, if statement to handle when a non-None address is given.

Thanks!

Bartosz Zaczyński RP Team on June 21, 2022

@YZ The advantage to having all instance attributes defined in the .__init__() method is that tools can introspect your objects to discover their properties even when there’s no associated value:

>>> class Employee:
...     def __init__(self, id, name, address=None):
...         self.id = id
...         self.name = name
...         self.address = address
...

>>> vars(Employee(1, "Joe"))
{'id': 1, 'name': 'Joe', 'address': None}

This also prevents you from having to remember to check whether an object has a given attribute or not:

>>> class Employee:
...     def __init__(self, id, name):
...         self.id = id
...         self.name = name
... 
>>> joe = Employee(1, "Joe")
>>> hasattr(joe, "address")
False
>>> joe.address
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    joe.address
AttributeError: 'Employee' object has no attribute 'address'

Attaching attributes dynamically to existing objects can be confusing and should be generally avoided. Other way to handle a missing value is the Null Object pattern.

Become a Member to join the conversation.