Customizing Attribute Access

PythonPythonBeginner
Practice Now

This tutorial is from open-source community. Access the source code

Introduction

Objectives:

  • Learn about customizing attribute access
  • Delegation vs. inheritance

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("`Python`")) -.-> python/BasicConceptsGroup(["`Basic Concepts`"]) python(("`Python`")) -.-> python/ControlFlowGroup(["`Control Flow`"]) python(("`Python`")) -.-> python/DataStructuresGroup(["`Data Structures`"]) python(("`Python`")) -.-> python/FunctionsGroup(["`Functions`"]) python(("`Python`")) -.-> python/ModulesandPackagesGroup(["`Modules and Packages`"]) python(("`Python`")) -.-> python/ObjectOrientedProgrammingGroup(["`Object-Oriented Programming`"]) python(("`Python`")) -.-> python/ErrorandExceptionHandlingGroup(["`Error and Exception Handling`"]) python(("`Python`")) -.-> python/PythonStandardLibraryGroup(["`Python Standard Library`"]) python/BasicConceptsGroup -.-> python/variables_data_types("`Variables and Data Types`") python/ControlFlowGroup -.-> python/conditional_statements("`Conditional Statements`") python/DataStructuresGroup -.-> python/lists("`Lists`") python/DataStructuresGroup -.-> python/tuples("`Tuples`") python/DataStructuresGroup -.-> python/sets("`Sets`") python/FunctionsGroup -.-> python/function_definition("`Function Definition`") python/ModulesandPackagesGroup -.-> python/importing_modules("`Importing Modules`") python/ModulesandPackagesGroup -.-> python/using_packages("`Using Packages`") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("`Classes and Objects`") python/ObjectOrientedProgrammingGroup -.-> python/constructor("`Constructor`") python/ObjectOrientedProgrammingGroup -.-> python/polymorphism("`Polymorphism`") python/ObjectOrientedProgrammingGroup -.-> python/encapsulation("`Encapsulation`") python/ErrorandExceptionHandlingGroup -.-> python/raising_exceptions("`Raising Exceptions`") python/PythonStandardLibraryGroup -.-> python/data_collections("`Data Collections`") python/BasicConceptsGroup -.-> python/python_shell("`Python Shell`") python/FunctionsGroup -.-> python/build_in_functions("`Build-in Functions`") subgraph Lab Skills python/variables_data_types -.-> lab-132502{{"`Customizing Attribute Access`"}} python/conditional_statements -.-> lab-132502{{"`Customizing Attribute Access`"}} python/lists -.-> lab-132502{{"`Customizing Attribute Access`"}} python/tuples -.-> lab-132502{{"`Customizing Attribute Access`"}} python/sets -.-> lab-132502{{"`Customizing Attribute Access`"}} python/function_definition -.-> lab-132502{{"`Customizing Attribute Access`"}} python/importing_modules -.-> lab-132502{{"`Customizing Attribute Access`"}} python/using_packages -.-> lab-132502{{"`Customizing Attribute Access`"}} python/classes_objects -.-> lab-132502{{"`Customizing Attribute Access`"}} python/constructor -.-> lab-132502{{"`Customizing Attribute Access`"}} python/polymorphism -.-> lab-132502{{"`Customizing Attribute Access`"}} python/encapsulation -.-> lab-132502{{"`Customizing Attribute Access`"}} python/raising_exceptions -.-> lab-132502{{"`Customizing Attribute Access`"}} python/data_collections -.-> lab-132502{{"`Customizing Attribute Access`"}} python/python_shell -.-> lab-132502{{"`Customizing Attribute Access`"}} python/build_in_functions -.-> lab-132502{{"`Customizing Attribute Access`"}} end

Slots vs. setattr

In previous exercises, __slots__ was used to list the instance attributes on a class. The primary purpose of slots is to optimize the use of memory. A secondary effect is that it strictly limits the allowed attributes to those listed. A downside of slots is that it often interacts strangely with other parts of Python (for example, classes using slots can't be used with multiple inheritance). For that reason, you really shouldn't use slots except in special cases.

If you really wanted to limit the set of allowed attributes, an alternate way to do it would be to define a __setattr__() method. Try this experiment:

>>> class Stock:
        def __init__(self, name, shares, price):
            self.name = name
            self.shares = shares
            self.price = price
        def __setattr__(self, name, value):
            if name not in { 'name', 'shares', 'price' }:
                raise AttributeError('No attribute %s' % name)
            super().__setattr__(name, value)

>>> s = Stock('GOOG', 100, 490.1)
>>> s.name
'GOOG'
>>> s.shares = 75
>>> s.share = 50
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in __setattr__
AttributeError: No attribute share
>>>

In this example, there are no slots, but the __setattr__() method still restricts attributes to those in a predefined set. You'd probably need to think about how this approach might interact with inheritance (e.g., if subclasses wanted to add new attributes, they'd probably need to redefine __setattr__() to make it work).

Proxies

A proxy class is a class that wraps around an existing class and provides a similar interface. Define the following class which makes a read-only layer around an existing object:

>>> class Readonly:
        def __init__(self, obj):
            self.__dict__['_obj'] = obj
        def __setattr__(self, name, value):
            raise AttributeError("Can't set attribute")
        def __getattr__(self, name):
            return getattr(self._obj, name)

>>>

To use the class, you simply wrap it around an existing instance:

>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> p = Readonly(s)
>>> p.name
'GOOG'
>>> p.shares
100
>>> p.cost
49010.0
>>> p.shares = 50
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in __setattr__
AttributeError: Can't set attribute
>>>

Delegation as an alternative to inheritance

Delegation is sometimes used as an alternative to inheritance. The idea is almost the same as the proxy class you defined in part (b). Try defining the following class:

>>> class Spam:
        def a(self):
            print('Spam.a')
        def b(self):
            print('Spam.b')

>>>

Now, make a class that wraps around it and redefines some of the methods:

>>> class MySpam:
        def __init__(self):
            self._spam = Spam()
        def a(self):
            print('MySpam.a')
            self._spam.a()
        def c(self):
            print('MySpam.c')
        def __getattr__(self, name):
            return getattr(self._spam, name)

>>> s = MySpam()
>>> s.a()
MySpam.a
Spam.a
>>> s.b()
Spam.b
>>> s.c()
MySpam.c
>>>

Carefully notice that the resulting class looks very similar to inheritance. For example the a() method is doing something similar to the super() call. The method b() is picked up via the __getattr__() method which delegates to the internally held Spam instance.

Discussion

The __getattr__() method is commonly defined on classes that act as wrappers around other objects. However, you have to be aware that the process of wrapping another object in this manner often introduces other complexities. For example, the wrapper object might break type-checking if any other part of the application is using the isinstance() function.

Delegating methods through __getattr__() also doesn't work with special methods such as __getitem__(), __enter__(), and so forth. If a class makes extensive use of such methods, you'll have to provide similar functions in your wrapper class.

Summary

Congratulations! You have completed the Customizing Attribute Access lab. You can practice more labs in LabEx to improve your skills.

Other Python Tutorials you may like