#!/usr/bin/env python # coding: utf-8 # # Attribute Access # - property # - descriptor # - **`__getattr__`**, **`__setattr__`** # - **`__getattribute__`** # --- # # Properties # - Route a specific attributes's get, set and delete operations to functions provided # - Created with **property** built-in # - They are inherited by subclasses and instances # # ## Basics # ```python # attribute = property(fget, fset, fdel, doc) # ``` # # - None of these function are required # - The default of them are **None** # - which means this operation is not supported # - attempting it raises an **AttributeError** # # # - **property** call returns a property object, which we assign to the name of the attribute to be managed in the class scope # - require new-style **object** derivation # In[1]: class Person: def __init__(self, name): self._name = name def get_name(self): print("Fetch...") return self._name def set_name(self, value): print("Change...") self._name = value def del_name(self): print("Remove...") del self._name name = property(get_name, set_name, del_name, "name property docs") bob = Person("Bob Smith") bob.name bob.name = "Robert Smith" print(Person.name.__doc__) del bob.name # ### properties with decorators # In[2]: class Person: def __init__(self, name): self._name = name @property def name(self): "name property docs" print("Fetch...") return self._name @name.setter def name(self, value): print("Change...") self._name = value @name.deleter def name(self): print("Remove...") del self._name bob = Person("Bob Smith") bob.name bob.name = "Robert Smith" print(Person.name.__doc__) del bob.name # --- # # Descrptors # - Descriptors are created as independent classes and are assigned to class attributes just like method functions # - Like a property, a descriptor manages a single, specific attribute # - A property is a simplified way to create a specific type of descriptor that runs method functions on attribute access # - Descriptor is a more general tool # - state information # - inheritance # - composition # - Works only for new-style class (Both descriptor and its client class) # # ## Basics # ```python # class Descriptor: # 'docstring' # def __get__(self, instance, owner): # """ # owner: specify the class to which descriptor is attached # instance: the instance which the attribute was accessed (for instnce.attr) or Nonr (for class.attr) # """ # ... # return attr # # def __set__(self, instance, value): # ... # # def __delete__(self, instance): # ... # ``` # # Unlike property, omitting a **`__set__`** allows the descriptor attribute's name to be assigned # In[3]: # Really Read-Only class Descriptor: def __get__(*args): print("get") def __set__(*args): raise AttributeError("cannot set") class C: a = Descriptor() c = C() c.a c.a = 1 # - Assign an instance of the descriptor class to a class attribute to enable it to be inherited by all instances of the class # In[ ]: class Name: "name descriptor docs" def __get__(self, instance, owner): print("Fetch...") return instance._name def __set__(self, instance, value): print("Change...") instance._name = value def __delete__(self, instance): print("Remove...") del instance._name class Person: def __init__(self, name): self._name = name name = Name() # It must be assign like this instead of a self inatnce attribute bob = Person("Bob Smith") bob.name bob.name = "Robert Smith" del bob.name # - If the descriptor class is not userful outside the client class, it's perfectly reasonable to embed it inside its client as subclass # - By doing so it won't clash with any names outside the class # ## Using State Information in Descriptors # ### Descriptor state # - Data internal to the workings of the descriptor # - Data spans all instnaces # - Can vary per attribute apperance # # ### Instance state # - Date that created by the client class # - Can vary per client class instance # In[4]: class DescState: def __get__(self, instance, owner): print("Des Get") return self._des * 10 def __set__(self, instance, value): print("Des Set") self._des = value class C: def __init__(self): self._des = 0 self._ins = 0 des = DescState() class InstState: def __get__(self, instance, owner): print("Ins Get") return instance._ins def __set__(self, instance, value): print("Ins Set") instance._ins = value ins = InstState() c1 = C() c1.des = 1 c1.ins = 2 print(c1.des, c1.ins) print("-" * 30) c2 = C() c2.des = 3 c2.ins = 4 print(c2.des, c2.ins) print("-" * 30) print(c1.des, c1.ins) # --- # # `__getattr__` and `__getattribute__` # Well suited to general delegation-based coding patterns # # ## Methods Overview # - **`__getattr__`** # - For undefined attributes # # - **`__getattribute__`** # - For every attribute # - Need to avoid recursive loops (Can be done by routing to superclass) # - Only available for new-style classes # # - **`__setattr__`** # - For every attribute fetch # - Need to avoid recursive loops # # - **`__delattr__`** # # ```python # class C: # def __getattr__(self, name): # ''' # name: the string name of the attribute being accessed # ''' # ... # return attr # # def __getattribute__(self, name): # ... # return attr # # def __setattr__(self, name, value): # ''' # value: the object being assigned to the attributeb # ''' # # def __delattr__(self, name): # ``` # # ## Avoiding Loops # # - The following code will trigger infinite loop # ```python # class C: # def __getattribute__(self, name): # x = self.other # return x # ``` # # - Routing to superclass (object) # ```python # class C: # def __getattribute(self, name): # x = object.__getattribute__(self, 'other') # return x # ``` # # ## Intercept Built-in Operation Attributes # **`__getattr__`** and **`__getattribute__`** are ideal for delegation except ***method-name attributes implicityly fetched by bulit-in operation*** # These methods may even not be run at all # # - In Python2, **`__getattr__`** can run for such attributes while it cannot in Python3 # - **`__getattribute__`** cannot run for such attributes in both versions # In[5]: get_ipython().run_cell_magic('python2', '', "class GetAttr:\n eggs = 88\n def __init__(self):\n self.spam = 77\n \n def __getattr__(self, attr):\n print('getattr: ', attr)\n if attr == '__str__':\n return lambda *args: '[Getattr Str]'\n else:\n return lambda *args: None\n \ng = GetAttr()\ng[0]\n") # In[6]: class GetAttr: eggs = 88 def __init__(self): self.spam = 77 def __getattr__(self, attr): print("getattr: ", attr) if attr == "__str__": return lambda *args: "[Getattr Str]" else: return lambda *args: None g = GetAttr() g[0] # - Operator overloading methods implicityly run by built-in operations are never routed through either attribute interception method in Python3 # - If wrapped classes may contain operator overloading methods, those methods must be redefined in the wrapper class in order to delegate to the wrapped object