Locked learning resources

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

Unlock This Lesson

Locked learning resources

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

Unlock This Lesson

Customizing Copying in User-Defined Classes

00:00 It’ll be easier to use an IDE for this example, so I’m using VS Code. We start with this DataFile class and person.json, which is a JSON file that has some generic information about a user.

00:12 You can get these from the downloadable course resources. Let’s go over datafile.py. First, import the built-in json module. Then, define the class DataFile.

00:22 .__init__() has the parameters self and path. Within the instance, attribute self.file is created and stores the result of calling the built-in open() function, passing in the path, mode="r" and encoding="utf-8".

00:40 What this does is it opens the file at the location of path for reading, expecting text encoded in UTF-8. The __enter__() and __exit__() special methods allow this class to function as a context manager using Python’s with keyword. __enter__() returns self, the instance of the DataFile class to be used within the context block.

01:01 And __exit__() calls self.file.close(), which ensures that the file closes when either the context block ends or an exception is raised within the block.

01:11 The three parameters on __exit__() starting with exc, are related to exceptions raised within the context block, but it’s beyond the scope of this course to discuss their use.

01:21 Finally, the read_json method navigates to the start of the open file and parses its contents as JSON returning a valid Python object. So let’s give this class a spin and see how the copy module handles it.

01:34 Open the REPL in the integrated terminal. Import copy. To make the JSON easier to read, you’ll again import the pp function from pprint.

01:48 And import the DataFile class from datafile. Open a context manager with DataFile taking in the string person.json as datafile.

02:01 Create the copy: shallow_copy = copy. copy(datafile)

02:07 and use the pp function to print the results of calling datafile. read_json(). Exit the context manager, and the contents of person.json are printed nicely formatted as a Python dictionary.

02:22 What if you try using the shallow copy’s read_json() method now? shallow_copy.read_json()

02:29 ValueError: I/O operation on closed file.

02:33 Why does this error occur? Well, the way that the copy module works when copying an object, it actually creates a new instance of that object bypassing the __init__() function and just copies over the attributes of the source object, field for field.

02:49 When you shallow copied your DataFile instance, only the reference to the existing file object was copied and that file handle was closed when you exited the context manager. And the .__init__() function was never run either, so no new file handle could have been created on the copy, hence the I/O operation on a closed file.

03:08 And you might think this could be fixed with deep copying, but unfortunately that won’t work either. Neither copy nor deepcopy is supported on file objects.

03:17 Go ahead and try to copy the existing shallow copy file copy.copy(shallow_copy.file) TypeError: cannot pickle TextIOWrapper instances.

03:29 It looks weird, but this is the error you should expect when trying to copy a file. The reference to pickling is due to copying sharing some functionality with the pickle module, so the message isn’t particularly relevant to what we’re trying to do.

03:43 What’s important to know is that copying like this isn’t going to work. So what’s the workaround? How can you make instances of this class copyable? Go back to datafile.py and add a new special method, .__copy__(). This can go right after .__init__(). def __copy__ accepting the parameter self: return type(self) and in another pair of parentheses, self.file.name.

04:11 Looks complicated, but let’s break it down. This new method calls the built-in type() function passing in self, which returns the class DataFile, which is then called receiving self.file.name as the path argument, the file name of the file attribute of the instance passed to copy.

04:29 Ultimately creating a new instance of DataFile, which is then returned and becomes the return value of copy.copy. Open up the REPL again and try it out.

04:39 Exit, clear, and python.

04:45 Import copy. From pprint, import pp. From datafile, import DataFile.

04:55 And just like before, with DataFile("person.json") as datafile: shallow_copy = copy. copy(datafile)

05:08 and print(datafile. read_json()) using that pp function.

05:15 Everything is the same so far, seeing that same JSON dictionary. But try reading the JSON in the copy—pp( shallow_copy.read_json()), and it works.

05:28 The JSON is printed as a Python dict. Of course, because this instance is not being used as a context manager, you should remember to close the file and free up the resource: shallow_copy.file.close().

05:42 And just like that, you’re able to take the reins and decide exactly how Python copies your objects. Pretty cool, huh? Okay, all that’s left is to meet me in the summary and review what you’ve learned.

Become a Member to join the conversation.