The World’s Simplest Python Class

We’ve begun studying class statement syntax in detail in this chapter, but I’d again like to remind you that the basic inheritance model that classes produce is very simple—all it really involves is searching for attributes in trees of linked objects. In fact, we can create a class with nothing in it at all. The following statement makes a class with no attributes attached, an empty namespace object:

>>> class rec: pass              # Empty namespace object

We need the no-operation pass placeholder statement (discussed in Chapter 13) here because we don’t have any methods to code. After we make the class by running this statement interactively, we can start attaching attributes to the class by assigning names to it completely outside of the original class statement:

>>> rec.name = 'Bob'             # Just objects with attributes
>>> rec.age  = 40

And, after we’ve created these attributes by assignment, we can fetch them with the usual syntax. When used this way, a class is roughly similar to a “struct” in C, or a “record” in Pascal. It’s basically an object with field names attached to it (as we’ll see ahead, doing similar with dictionary keys requires extra characters):

>>> print(rec.name)              # Like a C struct or a record
Bob

Notice that this works even though there are no instances of the class yet; classes are objects in their own right, even without instances. In fact, they are just self-contained namespaces; as long as we have a reference to a class, we can set or change its attributes anytime we wish. Watch what happens when we do create two instances, though:

>>> x = rec()                    # Instances inherit class names
>>> y = rec()

These instances begin their lives as completely empty namespace objects. Because they remember the class from which they were made, though, they will obtain the attributes we attached to the class by inheritance:

>>> x.name, y.name               # name is stored on the class only
('Bob', 'Bob')

Really, these instances have no attributes of their own; they simply fetch the name attribute from the class object where it is stored. If we do assign an attribute to an instance, though, it creates (or changes) the attribute in that object, and no other—crucially, attribute references kick off inheritance searches, but attribute assignments affect only the objects in which the assignments are made. Here, this means that x gets its own name, but y still inherits the name attached to the class above it:

>>> x.name = 'Sue'               # But assignment changes x only
>>> rec.name, x.name, y.name
('Bob', 'Sue', 'Bob')

In fact, as we’ll explore in more detail in Chapter 29, the attributes of a namespace object are usually implemented as dictionaries, and class inheritance trees are (generally speaking) just dictionaries with links to other dictionaries. If you know where to look, you can see this explicitly.

For example, the __dict__ attribute is the namespace dictionary for most class-based objects. Some classes may also (or instead) define attributes in __slots__, an advanced and seldom-used feature that we’ll note in Chapter 28, but largely postpone until Chapter 31 and Chapter 32. Normally, __dict__ literally is an instance’s attribute namespace.

To illustrate, the following was run in Python 3.3; the order of names and set of __X__ internal names present can vary from release to release, and we filter out built-ins with a generator expression as we’ve done before, but the names we assigned are present in all:

>>> list(rec.__dict__.keys())
['age', '__module__', '__qualname__', '__weakref__', 'name', '__dict__', '__doc__']

>>> list(name for name in rec.__dict__ if not name.startswith('__'))
['age', 'name']
>>> list(x.__dict__.keys())
['name']
>>> list(y.__dict__.keys())           # list() not required in Python 2.X
[]

Here, the class’s namespace dictionary shows the name and age attributes we assigned to it, x has its own name, and y is still empty. Because of this model, an attribute can often be fetched by either dictionary indexing or attribute notation, but only if it’s present on the object in question—attribute notation kicks off inheritance search, but indexing looks in the single object only (as we’ll see later, both have valid roles):

>>> x.name, x.__dict__['name']        # Attributes present here are dict keys
('Sue', 'Sue')
>>> x.age                             # But attribute fetch checks classes too
40
>>> x.__dict__['age']                 # Indexing dict does not do inheritance
KeyError: 'age'

To facilitate inheritance search on attribute fetches, each instance has a link to its class that Python creates for us—it’s called __class__, if you want to inspect it:

>>> x.__class__                       # Instance to class link
<class '__main__.rec'>

Classes also have a __bases__ attribute, which is a tuple of references to their superclass objects—in this example just the implied object root class in Python 3.X we’ll explore later (you’ll get an empty tuple in 2.X instead):

>>> rec.__bases__                     # Class to superclasses link, () in 2.X
(<class 'object'>,)

These two attributes are how class trees are literally represented in memory by Python. Internal details like these are not required knowledge—class trees are implied by the code you run, and their search is normally automatic—but they can often help demystify the model.

The main point to take away from this look under the hood is that Python’s class model is extremely dynamic. Classes and instances are just namespace objects, with attributes created on the fly by assignment. Those assignments usually happen within the class statements you code, but they can occur anywhere you have a reference to one of the objects in the tree.

Even methods, normally created by a def nested in a class, can be created completely independently of any class object. The following, for example, defines a simple function outside of any class that takes one argument:

>>> def uppername(obj):
        return obj.name.upper()       # Still needs a self argument (obj)

There is nothing about a class here yet—it’s a simple function, and it can be called as such at this point, provided we pass in an object obj with a name attribute, whose value in turn has an upper method—our class instances happen to fit the expected interface, and kick off string uppercase conversion:

>>> uppername(x)                      # Call as a simple function
'SUE'

If we assign this simple function to an attribute of our class, though, it becomes a method, callable through any instance, as well as through the class name itself as long as we pass in an instance manually—a technique we’ll leverage further in the next chapter:[54]

>>> rec.method = uppername            # Now it's a class's method!

>>> x.method()                        # Run  method to process x
'SUE'

>>> y.method()                        # Same, but pass y to self
'BOB'

>>> rec.method(x)                     # Can call through instance or class
'SUE'

Normally, classes are filled out by class statements, and instance attributes are created by assignments to self attributes in method functions. The point again, though, is that they don’t have to be; OOP in Python really is mostly about looking up attributes in linked namespace objects.

Records Revisited: Classes Versus Dictionaries

Although the simple classes of the prior section are meant to illustrate class model basics, the techniques they employ can also be used for real work. For example, Chapter 8 and Chapter 9 showed how to use dictionaries, tuples, and lists to record properties of entities in our programs, generically called records. It turns out that classes can often serve better in this role—they package information like dictionaries, but can also bundle processing logic in the form of methods. For reference, here is an example for tuple- and dictionary-based records we used earlier in the book (using one of many dictionary coding techniques):

>>> rec = ('Bob', 40.5, ['dev', 'mgr'])     # Tuple-based record
>>> print(rec[0])
Bob

>>> rec = {}
>>> rec['name'] = 'Bob'                     # Dictionary-based record
>>> rec['age']  = 40.5                      # Or {...}, dict(n=v), etc.
>>> rec['jobs'] = ['dev', 'mgr']
>>>
>>> print(rec['name'])
Bob

This code emulates tools like records in other languages. As we just saw, though, there are also multiple ways to do the same with classes. Perhaps the simplest is this—trading keys for attributes:

>>> class rec: pass

>>> rec.name = 'Bob'                        # Class-based record
>>> rec.age  = 40.5
>>> rec.jobs = ['dev', 'mgr']
>>>
>>> print(rec.name)
Bob

This code has substantially less syntax than the dictionary equivalent. It uses an empty class statement to generate an empty namespace object. Once we make the empty class, we fill it out by assigning class attributes over time, as before.

This works, but a new class statement will be required for each distinct record we will need. Perhaps more typically, we can instead generate instances of an empty class to represent each distinct entity:

>>> class rec: pass

>>> pers1 = rec()                           # Instance-based records
>>> pers1.name = 'Bob'
>>> pers1.jobs = ['dev', 'mgr']
>>> pers1.age  = 40.5
>>>
>>> pers2 = rec()
>>> pers2.name = 'Sue'
>>> pers2.jobs = ['dev', 'cto']
>>>
>>> pers1.name, pers2.name
('Bob', 'Sue')

Here, we make two records from the same class. Instances start out life empty, just like classes. We then fill in the records by assigning to attributes. This time, though, there are two separate objects, and hence two separate name attributes. In fact, instances of the same class don’t even have to have the same set of attribute names; in this example, one has a unique age name. Instances really are distinct namespaces, so each has a distinct attribute dictionary. Although they are normally filled out consistently by a class’s methods, they are more flexible than you might expect.

Finally, we might instead code a more full-blown class to implement the record and its processing—something that data-oriented dictionaries do not directly support:

>>> class Person:
        def __init__(self, name, jobs, age=None):      # class = data + logic
            self.name = name
            self.jobs = jobs
            self.age  = age
        def info(self):
            return (self.name, self.jobs)

>>> rec1 = Person('Bob', ['dev', 'mgr'], 40.5)         # Construction calls
>>> rec2 = Person('Sue', ['dev', 'cto'])
>>>
>>> rec1.jobs, rec2.info()                             # Attributes + methods
(['dev', 'mgr'], ('Sue', ['dev', 'cto']))

This scheme also makes multiple instances, but the class is not empty this time: we’ve added logic (methods) to initialize instances at construction time and collect attributes into a tuple on request. The constructor imposes some consistency on instances here by always setting the name, job, and age attributes, even though the latter can be omitted when an object is made. Together, the class’s methods and instance attributes create a package, which combines both data and logic.

We could further extend this code by adding logic to compute salaries, parse names, and so on. Ultimately, we might link the class into a larger hierarchy to inherit and customize an existing set of methods via the automatic attribute search of classes, or perhaps even store instances of the class in a file with Python object pickling to make them persistent. In fact, we will—in the next chapter, we’ll expand on this analogy between classes and records with a more realistic running example that demonstrates class basics in action.

To be fair to other tools, in this form, the two class construction calls above more closely resemble dictionaries made all at once, but still seem less cluttered and provide extra processing methods. In fact, the class’s construction calls more closely resemble Chapter 9’s named tuples—which makes sense, given that named tuples really are classes with extra logic to map attributes to tuple offsets:

>>> rec = dict(name='Bob', age=40.5, jobs=['dev', 'mgr'])        # Dictionaries

>>> rec = {'name': 'Bob', 'age': 40.5, 'jobs': ['dev', 'mgr']}

>>> rec = Rec('Bob', 40.5, ['dev', 'mgr'])                       # Named tuples

In the end, although types like dictionaries and tuples are flexible, classes allow us to add behavior to objects in ways that built-in types and simple functions do not directly support. Although we can store functions in dictionaries, too, using them to process implied instances is nowhere near as natural and structured as it is in classes. To see this more clearly, let’s move ahead to the next chapter.



[54] In fact, this is one of the reasons the self argument must always be explicit in Python methods—because methods can be created as simple functions independent of a class, they need to make the implied instance argument explicit. They can be called as either functions or methods, and Python can neither guess nor assume that a simple function might eventually become a class’s method. The main reason for the explicit self argument, though, is to make the meanings of names more obvious: names not referenced through self are simple variables mapped to scopes, while names referenced through self with attribute notation are obviously instance attributes.