Python Objects: Understanding (the basics of) the Python Object Model#
Everything is an Object#
In Python everything is an object:
Functions are objects, classes are objects, instances are objects, types are objects, modules are objects, you name it: everything's an object.
The type of an object can be identified using the built-in type()
function:
Even code is an object:
>>> source = "lambda: 'Hello!'"
>>> code = compile(source, '', 'eval')
>>> type(code)
<class 'code'>
>>> isinstance(code, object)
True
>>> f = eval(code)
>>> f()
'Hello!'
>>>
An object is an entity encompassing the "data" and its acceptable operations. The Python data model documentation (https://docs.python.org/dev/reference/datamodel.html) describes:
"Every object has an identity, a type and a value. An object’s identity never changes once it has been created; you may think of it as the object’s address in memory. The ‘is’ operator compares the identity of two objects; the id() function returns an integer representing its identity."
Often, Python objects are created and named immediately with an assignment. Assigments introduce a name to refer to the object but don't create the object:1
>>> a = 1.2 # new name a for new float object
>>> b = 1.2 # new name b for new float object
>>> c = a # new name c for existing float object
>>> type(a)
<class 'float'>
>>> type(b)
<class 'float'>
>>> type(c)
<class 'float'>
>>> id(a) # (1)
140026583519544 # (2) different identity than (1) ==> different object
>>> id(b)
140026583519592
>>> id(c) # (3) same identity as (1) ==> same object
140026583519544
>>> a is b
False
>>> a is c
True
>>>
But note that an object needn't have a name:
In this example while the list
does have a name ("l") none of its items has
one: Neither the 1st integer item nor the 2nd float item nor the 3rd anonymous
function item.
Python objects can be created e.g. by
- instantiating built-in types
- instantiating user-defined types (classes)
- defining classes
- defining functions
- defining anonymous functions
You can create an instance of the most basic type object
like this:
Objects are First-Class#
This means that all (named) objects are equal in the sense that they can be treated equally: a function object can be an argument to another function, a module object can be a list item, a class can be a dictionary value (or even a dictionary key), ...
This allows for writing powerful constructs:
>> data = [42, 5.0, " some examples are more intelligent than others "]
>>> dispatcher = {
... str: lambda x: x.strip(), # we want strings stripped
... int: lambda x: str(x**2), # we want ints squared
... float: lambda x: str(-x) # we want inverted floats
... }
>>> print('|'.join(
... dispatcher[type(d)](d) for d in data)
... )
1764|-5.0|some examples are more intelligent than others
>>>
More characteristics of objects#
- The identity of an object never changes after creation (see above).
- The type of an object never changes after creation.
- The type of the object defines the allowed values and the acceptable operations.
- An objects is mutable if its value can be changed, immutable otherwise.
Object Attribute Access#
You can access an object's attributes with the '.'-operator:
>>> text = "I'm a string!"
>>> # Object attribute access - in this case, access a method attribute.
>>> text.upper
<built-in method upper of str object at 0x7f141886e730>
>>> # Objects have an internal __class__ attribute.
>>> text.__class__
<class 'str'>
>>> # A class object has an internal __name__ attribute.
>>> text.__class__.__name__
'str'
>>>
You can "query" an object if it has a certain attribute using hasattr(obj,
"attribute name")
, at runtime:
>>> text = "I'm also string!"
>>> hasattr(text, 'upper')
True
>>> hasattr(text, '__class__')
True
>>> hasattr(text.__class__, '__name__')
True
>>> hasattr(text, 'something_else')
False
In the same vein, you can dynamically access object attributes at runtime with
the getattr
built-in function:
>>> getattr(text, 'upper')
<built-in method upper of str object at 0x7f1416b0cb20>
>>> getattr(text, '__class__')
<class 'str'>
>>> getattr(text, 'something_else')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'something_else'
>>> getattr(text, 'something_else', "default value")
'default value'
>>>
This is very powerful.
Immutable and Mutable Objects#
The mutability/immutability of objects is determined by their types:
- immutable types:
str
,int
,float
,complex
,tuple
,frozenset
- mutable types:
list
,dict
,set
, user-defined objects (classes)
Examples: immutable types#
Immutable type int
:
>>> a = 1
>>> id(a)
140026581642624
>>> a = 2 # assign name 'a' to a new object
>>> id(a)
140026581642656
>>> a += 1
>>> a
3
>>> # a is now a name for another new object: immutable object 2 hasn't
>>> # changed in-place
>>> id(a)
140357021310400
Immutable type float
:
>>> f1 = 1.2
>>> f2 = 1.2
>>> f3 = f1 # (1) f3 is another name for the object named f1
>>> id(f1)
140026583519544
>>> id(f2)
140026583519592
>>> id(f3)
140026583519544
>>> f1 = 1.3 # (2) the name f1 is now given to another object
>>> f1
1.3
>>> f2
1.2
>>> f3 # (3) f3 is unaffected: it is still a name for the original object (1)
1.2
>>> id(f1)
140026583519352
>>> id(f2)
140026583519592
>>> id(f3)
140026583519544
>>>
Examples: mutable types#
Mutable type list
:
>>> l1 = [1, 2, 3]
>>> l2 = l1 # (1) l2 is another name for the object named l1
>>> id(l1)
140026582652168
>>> id(l2)
140026582652168
>>> l1[0] = 9 # (2) change a value of of l1
>>> l1
[9, 2, 3]
>>> l2 # (3) the change (2) also effects the value of l2
[9, 2, 3]
>>> id(l1)
140026582652168
>>> id(l2)
140026582652168
>>>
>>> l1 = [1, 2, 3]
>>> l2 = l1
>>> l1 += [4] # extend mutable object
>>> l1
[1, 2, 3, 4]
>>> l2
[1, 2, 3, 4]
>>> l1 is l2
True
>>>
Mutable type dict
:
>>> d1 = {1: 'one', 2: 'two'}
>>> d2 = d1
>>> d2[1] = 'three' # create some confusion
>>> d2
{1: 'three', 2: 'two'}
>>> d1
{1: 'three', 2: 'two'}
>>>
Object Lifetime and Object Reference#
Every object that is created must be destroyed when it is no longer needed, otherwise we run out of memory eventually. In Python objects aren't explicitly destroyed but may be garbage-collected by the interpreter when they become unreachable, i.e. they aren't referenced any more (by name or by other objects).
CPython implementation detail: CPython uses reference counting. Every object carries the "in-use" information in a "reference count" which records the number of references to this object. Remember, a variable is a name referencing an object. An assignment of a name to an object establishes this reference - during this step the reference count of the object is increased.
Conversely the object's reference count is decreased when the variable is
deleted (explicit using del
or implicit by running out of scope) or
re-assigned to another object.
If the reference count of an objects is 0
the object is automatically
destroyed by the garbage collector of the Python interpreter (not necessarily
immediately, so don't rely on it!).
You can watch these mechanisms using the getrefcount()
-function of the Python
standard library module sys
:
>>> import sys
>>> l1 = ['cpython', 'does', 'refcounting'] # create named new list
>>> sys.getrefcount(l1)
2
>>> id(l1)
39141600
>>> l2 = l1 # new name for existing object
>>> id(l2) # yes, it's the same object indeed
39141600
>>> sys.getrefcount(l1) # ==> refcount increased
3
>>> sys.getrefcount(l2)
3
>>> l3 = l1 # yet another new name for (=reference to) existing object
>>> sys.getrefcount(l1)
4
>>> del l3 # delete name l3
>>> sys.getrefcount(l1) # ==> refcount of list object decreased
3
>>> l2 = object() # assign name l2 to another object
>>> sys.getrefcount(l1) # ==> refcount of list object decreased
2
>>>
Remark:
As you may have noted the reference count is higher than you might expect.
This is due to the fact that the sys.getrefcount()
-function call also
increases the object's reference count, as it needs to hold a reference to the
object, too, while it is running.
Introspection#
Python objects are highly introspectable. That means that you can find out pretty much anything about their properties and capabailities (names, types, attributes, methods, annotations, ...) at runtime.
A method to list an object's attribute is the dir()
built-in:
>>> dir(3)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__',
'__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__',
'__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__',
'__getnewargs__', '__gt__', '__hash__', '__index__', '__init__',
'__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__',
'__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__',
'__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__',
'__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__',
'__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__',
'__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__',
'__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__',
'as_integer_ratio', 'bit_length', 'conjugate', 'denominator', 'from_bytes',
'imag', 'numerator', 'real', 'to_bytes']
>>>
>>> class IntervalDefaults:
... min_val = -10
... max_val = 10
...
>>> dir(IntervalDefaults)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__', 'max_val', 'min_val']
>>>
Or you can peek into an object's __dict__
, since Python objects' inner
workings are predominantly implemented with the help of dictionaries:
>>> IntervalDefaults.__dict__ # nowadays, a read-only dict on classes
mappingproxy({'__module__': '__main__', 'min_val': -10, 'max_val': 10,
'__dict__': <attribute '__dict__' of 'IntervalDefaults' objects>,
'__weakref__': <attribute '__weakref__' of 'IntervalDefaults' objects>,
'__doc__': None})
>>>
To check if an object is callable there's the callable
built-in:
The standard library inspect module offers extensive introspection support:
>>> def accelerate(car, target_speed, thrust=100):
... ...
...
>>> import inspect
>>> inspect.signature(accelerate)
<Signature (car, target_speed, thrust=100)>
>>> inspect.signature(accelerate).parameters
mappingproxy(OrderedDict([('car', <Parameter "car">), ('target_speed',
<Parameter "target_speed">), ('thrust', <Parameter "thrust=100">)]))
>>> inspect.signature(accelerate).parameters['car'].default
<class 'inspect._empty'>
>>>
Lesson: Object Introspection
Write a program that lets the user enter a text.
Try to convert the user's input text to data in this order:
- Try to convert the text to
int
. - If that fails, try to convert to
float
. - If that fails, simply use the text data.
By introspection, find out about the converted object's callable methods, apart from those whose name starts with a leading underscore ('_').
Generate and print a menu for the user to select which method should be applied to the (converted) object. Invoke the selected method and print the method's return value and the object.
Optional: Some methods might require an argument and thus can not simply be called without. Find out if the method needs arguments in the introspection step and sort those out for the selection menu generation.
Optional 2: Experiment with additional type converters for the input
data, e.g. also accept list
and tuple
. Can you simple use
the list
or tuple
constructors for type conversion here?
Try to execute the sort
method of a list object.
Use the input()
built-in function to read user input interactively.
Remember you can try
an operation and catch a resulting exception if
it fails.
Optional: inspect.signature
can provide you with information about
a callable's parameters.
Optional 2: An easy way to parse text input to lists or tuples is by
using json.loads
.
The program output could look something like this:
python3 object_introspection.py
Your input please: 42
value = 42 [<class 'int'>]
==========================================
Please select the method you want to call:
1 - as_integer_ratio
2 - bit_count
3 - bit_length
4 - conjugate
5 - from_bytes
6 - to_bytes
==========================================
Please enter your choice: 1
You selected 'as_integer_ratio'
Result:
(42).as_integer_ratio() --> (42, 1) [<class 'tuple'>]
value = 42
Really take a peek now?
# object_introspection.property
import json
import sys
def ask_user(prompt='Your input please: ', types=(int, float, json.loads)):
"""Get user input and convert to target data type with the 1st usable
`types` converter.
Returns the converted data or the string input if no converter is able to
convert the text input data.
"""
text = input(prompt)
# str is the default.
value = text
for typ in types:
try:
value = typ(text)
break
except Exception:
# Try the next type...
pass
print(f"value = {value} [{type(value)}]")
return value
def get_object_methods(obj):
"""Return a list of all `obj` methods that do not start with a leading
underscore.
"""
methods = [
value for name in dir(obj)
if not name.startswith('_') and callable(value := getattr(obj, name))
]
return methods
def select_method(methods):
print("==========================================")
print("Please select the method you want to call:")
for (i, method) in enumerate(methods):
print(f" {i+1} - {method.__name__}")
print("==========================================")
while (input_text := input("Please enter your choice: ")):
try:
choice = int(input_text)
break
except ValueError:
continue
selected_method = methods[choice-1]
print(f"You selected '{selected_method.__name__}'")
return selected_method
def run_method(method):
result = method()
print("Result:")
print(
f" ({method.__self__}).{method.__name__}() --> "
f"{result} [{type(result)}]"
)
print(f" value = {method.__self__}")
def main():
obj = ask_user()
methods = get_object_methods(obj)
selected_method = select_method(methods)
try:
run_method(selected_method)
except Exception as exc:
print(f"Oops! Ran into exception: {exc}.")
return 1
if __name__ == "__main__":
sys.exit(main())
-
Note though that
a = 1.2; b = 1.2; a is b
would actually yieldTrue
! This is due to identical float constants on the same line being stored as one float constant, thus labeling the single float object with the namesa
andb
(trycompile('a = 1.2; b = 1.2; a is b', '', 'exec').co_consts
). ↩