- Treating types as objects
- Using types
- Creating user-defined classes
- Understanding duck typing
By now, you’ve learned the basic Python types and also how to create your own data types using classes. For many languages, that would be pretty much it, as far as data types are concerned. But Python is dynamically typed, meaning that types of things are determined at runtime, not at compile time. This is one of the reasons Python is so easy to use. It also makes it possible, and sometimes necessary, to compute with the types of objects (and not just the objects themselves).
Fire up a Python session, and try out the following:
>>> type(['hello', 'goodbye'])
This is the first time you’ve seen the built-in type function in Python. It can be applied to any Python object and returns the type of that object. In this example, it tells us that 5 is an int (integer) and that ['hello', 'goodbye'] is a list, something you probably already knew.
Of greater interest is that Python returns objects in response to the calls to type; <class 'int'> and <class 'list'> are the screen representations of the returned objects. What sort of object is returned by a call of type(5)? We have any easy way of finding out—just use type on that result:
>>> type_result = type(5)
The object returned by type is an object whose type happens to be <class 'type'>—we can call it a type object. A type object is another kind of Python object whose only outstanding feature is the confusion its name sometime causes. Saying a type object is of type <class 'type'> has about the same degree of clarity as the old Abbot and Costello “Who’s on First?” comedy routine.
Now that you know that data types can be represented as Python type objects, what can you do with them? You can compare them, because any two Python objects can be compared:
>>> type("Hello") == type("Goodbye")
>>> type("Hello") == type(5)
The types of "Hello" and "Goodbye" are the same (they’re both strings), but the types of "Hello" and 5 are different. Among other things, you can use this to provide type checking in your function and method definitions.
The most common reason to be interested in the types of objects, particularly instances of user-defined classes, is to find out whether a particular object is an instance of a class. After determining that an object is of a particular type, the code can treat it appropriately. An example makes things much clearer. To start, let’s define a couple of empty classes, so as to set up a simple inheritance hierarchy:
>>> class A:
>>> class B(A):
>>> b = B()
As expected, applying the type function to b tells us that b is an instance of the class B that’s defined in our current __main__ namespace:
We can also obtain exactly the same information by accessing the instance’s special __class__ attribute:
We’ll be working with that class quite a bit to extract further information, so let’s store it somewhere:
>>> b_class = b.__class__
Now, to emphasize that everything in Python is an object, let’s prove that the class we obtained from b is the class we defined under the name B:
>>> b_class == B
In this example, we didn’t need to store the class of b—we already had it—but I want to make clear that a class is just another Python object and can be stored or passed around like any Python object.
Given the class of b, we can find the name of that class using its __name__ attribute:
And we can find out what classes it inherits from by accessing its __bases__ attribute, which contains a tuple of all of its base classes:
Used together, __class__, __bases__, and __name__ allow a full analysis of the class inheritance structure associated with any instance.
But two built-in functions provide a more user-friendly way of obtaining most of the information we usually need: isinstance and issubclass. The isinstance function is what you should use to determine whether, for example, a class passed into a function or method is of the expected type:
For class instances, check against the class . e is an instance of class D because E inherits from D . But d isn’t an instance of class E . For other types, you can use an example . A class is considered a subclass of itself .
Using type, isinstance, and issubclass makes it fairly easy to make code correctly determine an object’s or class’s inheritance hierarchy. Although this is easy, Python also has a feature that makes using objects even easier: duck typing. Duck typing, as in “if it walks like a duck and quacks like a duck, it probably is a duck,” refers to Python’s way of determining whether an object is the required type for an operation, focusing on an object’s interface rather than its type. If an operation needs an iterator, for example, the object used doesn’t need to be a subclass of any particular iterator or of any iterator at all. All that matters is that the object used as an iterator is able to yield a series of objects in the expected way.
This is in contrast to a language like Java, where stricter rules of inheritance are enforced. In short, duck typing means that in Python, you don’t need to (and probably shouldn’t) worry about type-checking function or method arguments and the like. Instead, you should rely on readable and documented code combined with thorough testing to make sure an object “quacks like a duck” as needed.
Duck typing can increase the flexibility of well-written code and, combined with the more advanced OO features discussed in chapter 20, gives you the ability to create classes and objects to cover almost any situation.
With what we’ve covered here, you have all the tools necessary to provide type checking in the situations where it’s necessary for your applications. On the other hand, by taking advantage of duck typing, you can write more flexible code that doesn’t need to be as concerned with type checking. As you’ll see in the next chapter, Python’s use of duck typing and its flexibility in defining special method attributes make it possible to construct and combine classes in almost any way imaginable.