Python __get__ Method

Complete guide to Python's __get__ method covering descriptors, attribute access, and property implementation.

Python __get__ Method

Python get Method

Last modified April 8, 2025

This comprehensive guide explores Python’s get method, the special method at the heart of descriptors. We’ll cover basic usage, property implementation, method binding, and practical examples.

Basic Definitions

The get method is part of Python’s descriptor protocol. It is called when accessing an attribute that is a descriptor. Descriptors enable custom attribute access.

Key characteristics: it takes three parameters (self, instance, and owner), returns the attribute value, and is used to implement properties, methods, and other attribute access patterns.

Basic Descriptor Implementation

Here’s a simple descriptor showing get in action. It logs attribute access while maintaining normal behavior.

basic_get.py

class LoggedAccess: def init(self, value): self.value = value

def __get__(self, instance, owner):
    print(f"Accessing {self.value} from {instance}")
    return self.value

class MyClass: attr = LoggedAccess(42)

obj = MyClass() print(obj.attr) # Prints access message and returns 42

This example demonstrates the basic descriptor pattern. When obj.attr is accessed, Python calls LoggedAccess.get with the instance and owner class.

The instance parameter is None when accessed through the class rather than an instance. The owner parameter is the class where the descriptor is defined.

Implementing a Property

The property built-in is implemented using descriptors. Here’s how to create a property-like descriptor with get.

property_descriptor.py

class MyProperty: def init(self, getter): self.getter = getter

def __get__(self, instance, owner):
    if instance is None:
        return self
    return self.getter(instance)

class Circle: def init(self, radius): self.radius = radius

@MyProperty
def diameter(self):
    return self.radius * 2

circle = Circle(5) print(circle.diameter) # 10

This custom property descriptor stores the getter function and calls it when the attribute is accessed. The instance is None check handles class access.

The @MyProperty decorator works similarly to @property, showing how Python’s property system is built on descriptors.

Method Binding with get

Python uses get to implement method binding. Here’s how to simulate method binding behavior.

method_binding.py

class Method: def init(self, func): self.func = func

def __get__(self, instance, owner):
    if instance is None:
        return self
    from functools import partial
    return partial(self.func, instance)

class MyClass: def init(self, value): self.value = value

@Method
def show(self):
    print(f"Value: {self.value}")

obj = MyClass(10) obj.show() # Value: 10

This example shows how Python binds methods to instances. When a method is accessed, get returns a partial function with the instance bound as first argument.

The partial function from functools creates a new function with the instance pre-bound, simulating Python’s method binding.

Cached Property Descriptor

Here’s a practical descriptor that caches computed properties after first access, optimizing performance for expensive calculations.

cached_property.py

class CachedProperty: def init(self, func): self.func = func self.cache_name = f"cache{func.name}"

def __get__(self, instance, owner):
    if instance is None:
        return self
    if not hasattr(instance, self.cache_name):
        setattr(instance, self.cache_name, self.func(instance))
    return getattr(instance, self.cache_name)

class Data: def init(self, data): self.data = data

@CachedProperty
def processed_data(self):
    print("Processing data...")
    return [x * 2 for x in self.data]

d = Data([1, 2, 3]) print(d.processed_data) # Processes and prints print(d.processed_data) # Returns cached value

This descriptor stores computed values in the instance’s namespace. Subsequent accesses return the cached value instead of recalculating.

The cache name is generated dynamically to avoid collisions. This pattern is useful for expensive computations that don’t change after first access.

Type Checking Descriptor

Descriptors can enforce type checking on attribute access. This example ensures an attribute always has a specific type.

type_checking.py

class Typed: def init(self, name, expected_type): self.name = name self.expected_type = expected_type

def __get__(self, instance, owner):
    if instance is None:
        return self
    return instance.__dict__[self.name]

def __set__(self, instance, value):
    if not isinstance(value, self.expected_type):
        raise TypeError(f"Expected {self.expected_type}")
    instance.__dict__[self.name] = value

class Person: name = Typed(“name”, str) age = Typed(“age”, int)

def __init__(self, name, age):
    self.name = name
    self.age = age

p = Person(“Alice”, 30)

p.age = “thirty” # Raises TypeError

This descriptor stores values in the instance’s dict while enforcing type constraints. The get method retrieves the stored value.

The descriptor maintains the actual data in the instance’s dictionary while controlling access through the descriptor protocol.

Best Practices

  • Handle instance=None: Always check for class-level access

  • Store instance data properly: Use instance.dict to avoid infinite recursion

  • Document descriptor behavior: Clearly explain expected behavior

  • Consider performance: Descriptors add overhead to attribute access

  • Use existing tools when possible: Prefer @property for simple cases

Source References

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.

ad ad