Blog Detail
“Everything in Python is an object” has been a common refrain among the Python community. What is an object? A simple definition is: An object is an instance of a class. For example, strings are instances of the str
class.
# Creating a string variable
>>> device_type = "switch"
# device_type is an instance of the str class
>>> type(device_type)
<class 'str'>
>>>
Classes themselves are instances of type
.
# Creating a class object normally
>>> class Cisco:
... vendor = "cisco"
...
>>> Cisco
<class '__main__.Cisco'>
>>> Cisco.vendor
'cisco'
>>> isinstance(Cisco, type)
True
# Creating a class object explicitly from type
>>> Arista = type("Arista", (), {"vendor": "arista"})
>>> Arista
<class '__main__.Arista'>
>>> Arista.vendor
'arista'
>>>
In addition to being an instance of a class, objects consist of attributes defining state and behavior. It is common to refer to state attributes as data attributes or variables, and behaviors are called methods. The Python builtin classes generally have many methods, but do not have data attributes. The str class provides methods for:
- Manipulating the string (capitalize, format, etc.)
- Finding information about the string (startswith, isnumeric, etc.)
- Segmenting the string (partition, split, etc.)
>>> example = "this is a string"
# Capitalize the first letter of each word
>>> example.title()
'This Is A String'
# Count the number of times the letter i appears
>>> example.count("i")
3
# Convert the string into a list of words
>>> example.split()
["this", "is", "a", "string"]
It is more common for custom classes to have data attributes that store information about the objects they imitate. For an object representing a switch, some example data attributes might be: modules, uptime, and reachability. Some example methods might be: reboot, and ping.
>>> switch = Arista(hostname="nyc-hq-rt1")
# Example of a data attribute
>>> switch.uptime
'4 years, 3 days'
# Example of a method attribute
>>> ping_stats = switch.ping("10.1.1.1", 5)
'pinging 10.1.1.1 times 5'
>>> ping_stats
{
'attempts': 5,
'success': 5,
'fail': 0,
'messages': ['pinged 10.1.1.1 in 0.21ms', 'pinged 10.1.1.1 in 0.14ms', ...]
}
>>>
Data Attributes
Data attributes are distinguished between class variables and instance variables. Class variables assign values that are the same for all objects of the class. Instance variables assign values for an instance of the class. Class variables are generally used for metadata fields, such as vendor
in the first example. Instance variables contain more interesting information that the object methods act upon. Using the previous example, the reachability
attribute might be used in a report to identify all switches that are unreachable. The Cisco class above is redefined to create instance variables upon initialization for hostname, connection status, and reachability information.
class Cisco:
# Class variable
vendor = "cisco"
def __init__(self, hostname):
# Instance variables
self.hostname = hostname
self.connected = False
ping_reachability = self.ping(hostname, 2)
if ping_reachability["success"]:
self.connect()
if self.connected:
self.reachability = "authorized"
else:
self.reachability = "unauthorized"
else:
self.reachability = "unreachable"
def connect(self):
...
self.connected = True if connected else False
def ping(self, destination, count):
...
return {
"attempts": count,
"success": success_count,
"fail": fail_count,
"messages": [message for message in ping_results],
}
Python uses dictionaries to store class and instance variables. Class variables share the same dictionary across all instances of the class, and instance variables are stored in a unique dictionary per instance. The class dict is stored in <class>.__dict__
, and this is referenced on an instance with <obj>.__class__.__dict__
. The instance dict is stored in <obj>.__dict__
. Here are some examples showing how these two dictionaries behave.
# View the class dict
>>> Cisco.__dict__
mappingproxy({'vendor': 'cisco', ...})
# Create two objects of the Cisco class
>>> rt1 = Cisco("nyc-hq-rt1")
>>> rt2 = Cisco("nyc-dc-rt1")
# Viewing the instance dict
>>> rt1.__dict__
{'hostname': 'nyc-hq-rt1', 'connected': True, 'reachability': 'authorized'}
# Reachability is an instance variable and can have different values across objects
>>> rt1.reachability
'authorized'
>>> rt2.reachability
'unreachable'
# The class dict is referenced within instances
>>> Cisco.__dict__ == rt1.__class__.__dict__
True
# Changing the vendor on the class is reflected on all instances
>>> Cisco.vendor = "CSCO"
>>> rt1.vendor
'CSCO'
>>> rt2.vendor
'CSCO'
>>>
Attribute Precedence
This distinction between class and instance variables is important for understanding Python’s attribute access behavior. The standard attribute lookup uses the following precedence until the attribute is found, or an Error is raised:
- Attributes defined on the instance
- Attributes defined on the class
- Attributes defined on inherited classes
Practically, this means that overriding a class attribute on an instance will only affect the single instance and not other instances.
>>> rt1 = Cisco("nyc-hq-rt1")
>>> rt2 = Cisco("nyc-dc-rt1")
# rt1 and rt2 both have a value of "cisco" for the vendor attribute
>>> rt1.vendor == rt2.vendor == "cisco"
True
# rt1's instance dict does not have an attribute for `vendor`
>>> rt1.__dict__
{'hostname': 'nyc-hq-rt1', 'connected': True, 'reachability': 'authorized'}
>>> rt1.vendor = "Cisco"
>>> rt1.vendor
'Cisco'
# rt1's instance dict was updated with a `vendor` attribute
>>> rt1.__dict__
{'hostname': 'nyc-hq-rt1', 'connected': True, 'reachability': 'authorized', 'vendor': 'Cisco'}
# rt2's vendor attribute remains unchanged
>>> rt2.vendor
'cisco'
Mutable Attributes
The above demonstrates the built-in safety provided when naming instance variables collides with class variables. However, this behavior might have surprising results when class variables point to mutable objects (such as lists). The Arista class is rewritten below to have a class variable defining the modules, with the value using a list for modular chassis.
class Arista:
vendor = "arista"
modules = ["7500E-48S", "DCS-7500E-SUP"]
>>> rt1 = Arista("nyc-hq-rt1")
>>> rt2 = Arista("nyc-dc-rt1")
>>> rt1.modules
["7500E-48S", "DCS-7500E-SUP"]
# Appending to rt2's `modules` attribute affects rt1
>>> rt2.modules.append("7500E-72S")
>>> rt1.modules
["7500E-48S", "DCS-7500E-SUP", "7500E-72S"]
# Using the `+=` operator on rt2's `modules` attributed affects rt1
>>> rt2.modules += ["7500E-36Q"]
>>> rt1.modules
["7500E-48S", "DCS-7500E-SUP", "7500E-72S", "7500E-36Q"]
# Both modifications to rt2's `modules` attribute affect the class attribute
>>> Arista.modules
["7500E-48S", "DCS-7500E-SUP", "7500E-72S", "7500E-36Q"]
# All references to `modules` are the same shared object
>>> Arista.modules is rt1.modulues is rt2.modules
True
>>>
Conclusion
Understanding how objects work is critical to working with Python. Fundamental to working with objects is managing and using data attributes. Data attributes can be created on the class, or on each instance of the class. Instance level attributes have a higher precedence than class level attributes. Finally, when creating attributes on a class, it is important to consider how they will be used, and that mutable attributes can lead to unexpected behavior.
-Jacob
Tags :
Contact Us to Learn More
Share details about yourself & someone from our team will reach out to you ASAP!