Partially Emulating collections.namedtuple
If you want to learn more about collections.namedtuple
, then check out Write Pythonic and Clean Code With namedtuple.
00:00
Partially Emulating collections.namedtuple
. As a final example of how to take advantage of .__new__()
in your code, you can push your Python skills and write a factory function that partially emulates collections.namedtuple
.
00:16
The namedtuple()
function allows you to create subclasses of tuple
with the additional feature of having named fields for accessing the items in the tuple.
00:27
Next, you’ll see code that implements a named_tuple_factory()
function that partially emulates this functionality by overriding the .__new__()
method of a nested class called NamedTuple
.
00:42
First, you import itemgetter
from the operators
module. This function allows you to retrieve items using their index in the containing sequence.
00:51
Next, named_tuple_factory()
is defined. This function takes a first argument called type_name
, which will hold the name of the tuple subclass that you want to create.
01:02
The *fields
argument allows you to pass an undefined number of field names as strings. Here, you define a local variable to hold the number of named fields provided by the user. Here, you define a nested class called NamedTuple
, which inherits from the built-in tuple
class.
01:20
This provides a .__slots__
class attribute. This attribute defines a tuple for holding instance attributes. This tuple saves memory by acting as a substitute for the instance’s dictionary, which would otherwise play a similar role.
01:34
Next you implement .__new__()
with cls
as its first argument. This implementation also takes the *args
argument to accept an undefined number of field values.
01:45
Then you define a conditional statement that checks if the number of items to store in the final tuple differs from the number of named fields. If that’s the case, then the conditional raises a TypeError
with an error message.
02:02
This sets the .__name__
attribute the current class to the value provided by type_name
. These lines define a for
loop that turns every name field into a property that uses itemgetter()
to return the item at the target index
.
02:16
The loop uses the built-in setattr()
function to perform this action. Note that the built-in enumerate()
function provides the appropriate index
value.
02:27
This line returns a new instance of the current class by calling super().__new__()
as usual. These lines define a .__repr__()
special method for the tuple subclass.
02:49
Finally, this line returns the newly created NamedTuple
class,
02:57
To try the named_tuple_factory()
out, start an interactive session in the directory containing the named_tuple.py
file and run the following code.
03:11
First, create a new Point
class by calling named_tuple_factory()
. The first argument in this call represents the name that the resulting class object will use.
03:21
The second and third arguments are the named fields available in the resulting class. Then you create a Point
object by calling the class constructor with the appropriate values for the x
and y
fields.
03:35 To access the value of each named field, you can use dot notation. You can also use indices to retrieve the values because your class is a tuple subclass.
03:50
Because tuples are an immutable data type in Python, you can’t assign new values to the point’s coordinates in place. If you try to do that, you get an AttributeError
.
04:02
Finally, calling dir()
with your point
instance as an argument reveals that your object inherits all of the attributes and methods that regular tuples have in Python.
04:13 Now that you’ve covered all the content in this course, in the next section, you’ll take a look back at what you’ve learned.
Bartosz Zaczyński RP Team on March 14, 2023
@Michal B While the common standard is to use *args
and **kwargs
, it’s mainly for generic arguments. In this case, it’s more descriptive to use the term *fields
since this function is specifically for creating a named tuple. So, yes, sometimes it makes more sense to use other argument names.
praghavan1973 on June 13, 2023
In the example program, what is the meaning of property(itemgetattr(index))
and why is it used to do setattr()
?
setattr(cls, field, property(itemgetter(index)))
I changed the code to:
setattr(cls, field, args[index])
and this seems to be working fine too.
Any reason why this straightforward approach was not done?
Leodanis Pozo Ramos RP Team on June 13, 2023
@praghavan1973 The reason behind the use of property() is that we wanted to have full control over the setter method. Your code works but if you forget to use __slots__ = ()
, then assignments will work and the tuple won’t be immutable any longer.
mochibeepboop on Sept. 18, 2023
If I may offer some feedback, this course (especially the second part) needs practical use cases. And it would be great if there was also a quiz. It feels really theoretical and I don’t know where I would even start applying these concepts on an actual project. Thank you :)
Bartosz Zaczyński RP Team on Sept. 18, 2023
@mochibeepboop Thank you for your feedback. We’re constantly striving to update and improve our courses. We’ll consider your suggestion when revamping the material. As to the quizzes, we’re slowly adding those to the published courses and tutorials, so you should expect one to be included in the future!
Oleg Vovkodav on March 18, 2024
Interesting tutorial, thank you.
Have a question though - what is the purpose of __slots__
class attribute here? It’s not used explicitly and when I was trying to print its content, it shows empty. So what is it for?
Bartosz Zaczyński RP Team on March 19, 2024
@Oleg Vovkodav When a class defines the .__slots__
attribute, it usually contains the allowed attribute names of its instances. This is both a memory-optimization and safety feature. When you define the slots as an empty tuple, you disable the ability to dynamically add or remove attributes from your objects:
>>> class Empty:
... __slots__ = ()
...
>>> empty = Empty()
>>> empty.custom_attribute = "value"
Traceback (most recent call last):
...
AttributeError: 'Empty' object has no attribute 'custom_attribute
Now, compare this to Python’s default behavior, which allows you to attach attributes after you’ve created an object:
>>> class Empty:
... pass
...
>>> empty = Empty()
>>> empty.custom_attribute = "value"
>>> dir(empty)
[..., 'custom_attribute']
In contrast, instances of a class with slots don’t have the .__dict__
attribute.
Become a Member to join the conversation.
Michal B on March 13, 2023
Hi All,
In 1:02, is the
in
instead of
?
I am aware that args and kwargs are arbitrary names, however if it’s the standard, then maybe we should be sticking to it? Unless ‘fields’ is something else?
Thanks!