4 ms·
> All people ever wanted was just a way to write in one line ´Point3D = struct(x, y, z)´ that translates into the exact equivalent of [... a regular class with
by raymondh 9y ago
> All people ever wanted was just a way to write in one line ´Point3D = struct(x, y, z)´ that translates into the exact equivalent of [... a regular class with just an x, y, and z attributes].
You're giving up a lot:
* The namedtuple is dramatically more space efficient (which will be important if you have a lot of points).
* The namedtuple has a nice self-describing __repr__() (which will can important when you're debugging):
>>> focal_point
Point3D(x=10.2, y=8.5, z=13.1)
* The namedtuple defines an equality relation (which is important if you ever want to recognize that two points are the same or need to find-out whether a given point is in a container):
>>> new_point in visited_points
True
* The namedtuple is hashable (this will be important if want to pass points into a function that uses functools.lru_cache() for example).
* The namedtuple is unpackable. This will important with loops like such as:
for x, y, z in points:
print(x*y - y*z)
* The namedtuple works with just about every existing tool in the language that expects regular tuples (such as old-style string formatting).
>>> 'Horizontal: %f Vertical: %f Depth: %f' % focal_point
'Horizontal: 10.200000 Vertical: 8.500000 Depth: 13.100000'
* The namedtuple has lexicographic ordering (which may be nice if you want to pprint() a set of points in a deterministic order).
* The nametuple has a _make() method for casting existing tuples without unpacking them . This is useful with the sqlite3 and csv modules:
EmployeeRecord = namedtuple('EmployeeRecord', 'name, age, title, department, paygrade')
import csv
for emp in map(EmployeeRecord._make, csv.reader(open("employees.csv", "rb"))):
print(emp.name, emp.title)
import sqlite3
conn = sqlite3.connect('/companydata')
cursor = conn.cursor()
cursor.execute('SELECT name, age, title, department, paygrade FROM employees')
for emp in map(EmployeeRecord._make, cursor.fetchall()):
print(emp.name, emp.title)
* The namedtuple is sliceable (useful for computing projections or partials to 2D points):
>>> focal_point[:2]
(10.2, 8.5)
* Many other little things as well: conversion to a dictionary with _asdict(), cloning with "newpoint = oldpoint._replace(y=0, z=1)", etc.
- Too 9y agoMaybe exact equivalent was an exaggeration, but anyway.. Half of those examples are just valid because x,y,z in particular have a de-facto ordering that everybody follows, for any other record type it's just confusing (what does slicing an EmployeeRecord mean??!). Csv module will produce named records based on the header so i don't see the use there. With sqllite3 you can set row_factory=Row which will produce results which already has access by name. If you for some reason need to convert to a dictionary, you could always use __dict__ Comparability, space, hashing and immutability are important though. Altough frankly, i can't take any immutability argument seriously since python has refused to add a constants for decades, it's just hypocrisy to one day have the philosophy that everything is dynamic and then then the next day say it's not.
- deleted 9y ago[deleted]
- eesmith 9y agoI agree. Change the class slightly and several of those methods become a big negative. As I've argued, I don't want to include API features "for free" if I don't really need them, because they will become a support headache for me in the future Here's what I mean. Let's start with the Point(x, y, z) class. If I want to tweak it a bit to add a pre-computed 'r' attribute, I can do the following in a normal class: class Point: __slots__ = ("x", "y", "z", "r") def __init__(self, x, y, z): self.x, self.y, self.z = x, y, z self.r = (x*x+y*y+z*z)**0.5 (If I want to enforce immutability I would use the attrs library.) I can't simply tweak a namedtuple to support that, because the "r" will leak in to the __init__, _as_dict(), __iter__, __repr__, and other methods. Nor can I write "for x, y, z in points" any more. Now, consider a point expressed in spherical coordinates (r, theta, phi). The two points (0, 0, 0) and (0, 45, 180) are identical, because both are at the origin. The two points (10, 0, 0) and (10, 360, 0) are also identical, because both are at (10, 0, 0) in Cartesian coordinates. (Angles expressed in degrees.) Either the __init__ needs to normalize the values to some standard range, or the __eq__ needs to check for degenerate cases. The default __eq__ and __hash__ for namedtuple are valid if all coordinates are in standard form, but that's incomplete. (Huh, and I see that the tuple behavior is that (nan,) == (nan,) even though nan != nan. This doesn't makes sense for some cases.) I disagree with the proposal that slicing is "useful for computing projections or partials to 2D points". In addition to the weirdness in slicing an Employee record, how do you project along the y axis using slicing? Are people really supposed to write [0::2]? Or [2:-1:-2] if they want the projection on the other side? That seems like a horrible API. Plus, the result is an anonymous tuple, when a Point2D makes more sense. For that matter, in spherical coordinates there is no meaning to point[::-1], so why do I want the API to support that in the first place? Next, consider a point expressed in homogeneous coordinates x, y, z, w. Slicing here is worthless as a way to give a projection. What does point4[2:] mean? In a real system of course, you project given a view point and orientation, and are not limited to a projection along the axis, which is all that a slice might be able to give you. Of course, in real system you store all of the points in an array because that "is dramatically more space efficient". Then there's the _make() method, which I'm struggling to understand why it's useful. It seems to save only a few characters over either of the following: for emp in (EmployeeRecord(*x) for x in csv.reader(file)): print(emp.name, emp.title) for row in csv.reader(file): emp = EmployeeRecord(*row) print(emp.name, emp.title) The latter is also, IMO, easier to understand and maintain for a wider number of people. On a deeper level, I can't see using this because most code would expect EmployeeRecord.age to be an integer. It's not easy to change the example code to, say, list those which are legally allowed to sell alcohol in the US: for emp in map(EmployeeRecord._make, csv.reader(open("employees.csv", "rb"))): if emp.age >= 18: print(emp.name, emp.title) Even changing it to "18" gives a problem because "100" < "18". At least the sqlite example will (likely) put the right types in the object, but again it's only saving a couple of keystrokes at most. Why learn a specialized API function for that? As for the "Many other little things", those appear to be solutions in search of a problem. As Too points out, if I needed an _asdict() I could often do obj.__dict__.copy(). In practice, I use __dict__ access in debugging - rarely - and don't recall ever using that in production code. Why is _asdict() so important that it should be part of my API? If it is so important, why don't more people include it with their own class definitions? I don't want to litter my API with functions that are useless, which is what namedtuple invites me to do by having me pretend that my data is "super" tuple-like.