Python Object

What is an object in Python?

Table of Contents

What is an Object


Object definition

The Python glossary defines an object as "Any data with state (attributes or value) and defined behavior (methods). Also the ultimate base class of any new-style class".

Everything we work with in Python is an object. If we use strings, integers, bytes, lists, dicts, etc. in our programs, we are using objects of a certain type. Classes and functions, user-defined or built-in, are also objects. When we have a file with the .py extension and import it, a module object is created. Built-in annotation types, exceptions, constants (False, True, None, NotImplemented, Ellipsis) are objects.


What is not an object?

1). Keywords, except False, True, and None. The list of keywords depends on the Python version.

import keyword

print(keyword.kwlist)
# ['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await',
# 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except',
# 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is',
# 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 
# 'try', 'while', 'with', 'yield']

2). Delimiters such as (, ), [, ], {, }, ,(comma), :, !, ., ;, @, =, ->, +=, -=, *=, /=, //=, %=, @=, &=, |=, ^=, >>=, <<=, **=.

3). Operators such as +, -, *, **, /, //, %, @, <<, >>, &, |, ^, ~, :=, <, >, <=, >=, ==, !=.

4). Other characters that have special meaning: ', ", #, \.

5) Literal prefixes and suffixes. They have special meaning only as part of literals.

- String literal prefixes: r, u, R, U, f, F, fr, Fr, fR, FR, rf, rF, Rf, RF.

- Bytes literal prefixes: b, B, br, Br, bR, BR, rb, rB, Rb, RB.

- Binary, octal, hexadecimal literal prefixes: 0b, 0B, 0o, 0O, 0x, 0X (Integer literals).

- Exponent in floating-point literals: e, E.

- Imaginary literal suffixes: j, J.

6) Escape sequences in string and bytes literals.


Characteristics of the Object


Type

Each object has a type. We can check this using type(object).

print(type("Python"))
# <class 'str'>

print(type(True))
# <class 'bool'>

print(type(print))
# <class 'builtin_function_or_method'>

print(type(list))
# <class 'type'>

print(type([]))
# <class 'list'>

import json
print(type(json))
# <class 'module'>

def foo():
    pass

print(type(foo))
# <class 'function'>

class A:
    pass

print(type(A))
# <class 'type'>

Reference Count

Every object in Python has a reference count, that keeps track of the number of references that point to that object. When the reference count of an object drops to zero, it is deallocated.

There are also immortal objects. For example, True and None are immortal in CPython. An immortal object always exists as long as the interpreter is running.

# -1 because count includes a temporary reference in getrefcount(obj)

import sys

def foo():
    pass

print(sys.getrefcount(foo) - 1)
# 1

foo1 = foo # additional reference

print(sys.getrefcount(foo) - 1)
# 2

For some objects, the returned value may not reflect the actual number of references to the object.


Size

Size of an object in bytes (vary by implementation and version of Python).

Python containers such as list, dict, set, etc. contain references to other objects (container elements). Using sys.getsizeof does not take into account the memory consumption of the objects referenced in the container.

import sys

print(sys.getsizeof(list))
# 424

print(sys.getsizeof([]))
# 56

id()

All objects have an integer id that is unique and constant for those objects throughout their lifetime. In the CPython implementation, this is the address of the object in memory.

obj = []
obj1 = []

print(id(obj) == id(obj1))
# False

Immutable objects can be reused to optimize memory usage.

obj = 12
obj1 = 12

# same id
print(id(obj) == id(obj1))
# True

print(obj is obj1)
# True

Equality and Hash

Almost all objects can be compared for equality, and they can have hash values.

The default behavior for equality comparison is based on the identity of the objects (x is y implies that x == y).

class A:
    pass

class B:
    pass

a = A()
a1 = A()
b = B()

# same object, same identity
print(a is a, a == a)
# True True

# different objects of same type (A)
print(a is a1, a == a1)
# False False

# different objects of different types (A and B)
print(a is b, a == b)
# False False

Built-in types customize their comparison behavior to provide value-based equality within the same type (numbers, strings, binary sequences, lists, tuples, ranges, dicts, sets, etc.). User-defined classes can also customize their comparison.

Numeric types can be compared with other numeric types.

from decimal import Decimal

obj = 1 # int
obj1 = 1.0 # float
obj2 = Decimal(1)

# different objects of different types
print(obj is obj1, obj is obj2, obj1 is obj2)
# False False False

# same value
print(obj == obj1, obj == obj2, obj1 == obj2)
# True True True

Containers of the same type can be compared by value.

obj = [1, 2, 3]
obj1 = [1, 2, 3]

print(obj is obj1) # different objects
# False

print(obj == obj1) # same value
# True

Truth Value Testing

Any object can be tested for truthiness.

By default, an object is considered true unless its class defines a __bool__ method or a __len__ method.

def foo():
    pass

class A:
    pass

a = A()

print(bool(foo))
# True
print(bool(A))
# True
print(bool(a))
# True

Truth value testing of built-in types and PSL classes can differ from default truth value testing.

Built-in objects evaluated as false:
- constants None and False;
- zero of any numeric type: 0, 0.0, 0j, Decimal(0), Fraction(0, 1);
- empty sequence and collection: '', (), [], {}, set(), frozenset(), bytes(), bytearray(), range(0).
list1 = []
list2 = [1, 2, 3]

print(bool(list1))
# False
print(bool(list2))
# True

if list2:
    print(list2[0])
# 1

An object with an overridden __bool__ or __len__ methods.

class A:
    def __init__(self, value):
        self.value = value
         
    def __bool__(self):
        return bool(self.value)

a = A("")

print(bool(a)) # "" is empty sequence
# False

if a:
    print("a")
else:
    print("not a")
# not a
class A:
    def __init__(self, value):
        self.value = value
         
    def __len__(self):
        return len(self.value)

a = A("")

print(bool(a)) # len("") == 0
# False

if a:
    print("a")
else:
    print("not a")
# not a

Mutable or Immutable

Objects can be mutable or immutable.

Built-in mutable types have methods for adding, setting, or removing values associated with them. They support item assignment, except for unordered types such as set. Changes to the value do not change the identity of the mutable object.

d = {"a": "A"}
print(d)
# {'a': 'A'}

print(id(d))
# 1538355629632

d["b"] = "B"
print(d)
# {'a': 'A', 'b': 'B'}

d.pop("a")
print(d)
# {'b': 'B'}

print(id(d))
# 1538355629632

Built-in immutable types do not have methods for changing the values associated with them. They may have methods that manipulate the value, but these methods return a new object. If we need a new value, we get a new object with a new id().

s = "Python"
print(id(s))
# 140725582104824

s1 = s.upper()
print(s1)
# PYTHON
print(id(s1))
# 2651742964400

s2 = s.encode()
print(s2)
# b'Python'
print(id(s2))
# 2651787375024

Object Attributes

Object attributes can be accessed through the dot notation: object.attr_name. This is called an attribute reference.

We can check a list of attributes using dir(object). In most cases, dir(class_object) and dir(instance_object) show the same result.

# attributes of str type
print(dir(str))
# ['__add__', '__class__', ..., 'upper', 'zfill']

# attributes of instance of class str
print(dir("Python"))
# ['__add__', '__class__', ..., 'upper', 'zfill']

Object attributes are also Python objects with type and attributes.

# __doc__ attribute of the upper() method

print(type(str.upper.__doc__), str.upper.__doc__)
# <class 'str'> Return a copy of the string converted to uppercase.

Unlike built-in classes and their instances, in user-defined classes and their instances we can have arbitrary values and functions. We can get, set, or delete these attributes. In this case, the attributes of the class and instance may differ.

class A:
     
    class_attr = "some shared value"
     
    def __init__(self, value):
        # value can be unique for each instance
        self.instance_attr = value

    def method(self):
        pass

a = A(12)

# attributes of A type
print(dir(A))
# ['__class__', ..., '__init__', ..., 'class_attr', 'method']

# attributes of instance of class A
print(dir(a))
#  ['__class__', ..., '__init__', ..., 'class_attr', 'instance_attr', 'method']

References:

  1. Glossary: object
  2. Docs: Data model, Objects, values and types
  3. Docs: C-api PyObject and PyVarObject
  4. Docs: Type Object Structures
  5. GitHub: object.h
  6. Glossary: reference count
  7. Docs: sys.getrefcount
  8. PEP 683: Immortal Objects, Using a Fixed Refcount
  9. What’s New In Python 3.12: Immortal Objects
  10. Docs: sys.getsizeof
  11. Docs: Truth Value Testing
  12. Docs: Comparisons
  13. Docs: object.__attr__
  14. Docs: Built-in Types

Popular posts from this blog