#!/usr/bin/env python # coding: utf-8 # # Extending Types by Subclassing # Allows you to customize or extend the behavior of built-in types with user-defined class # In[1]: class MyList(list): def __str__(self): print("Content: ") return list.__str__(self) ml = MyList([1, 2, 3]) print(ml) # --- # # The "New Style" Class Model # # - In Python3, all classes are "new style" # - In Python2, only explicitly inheritence from **object** would be regarded as "new style" # In[2]: class C(object): pass # ## New-Style Class Changes # ### 1. Attribute fetch for built-ins: instance skipped # - **`__getattr__`**, **`__getattribute__`** can no longer be called by other built-in operations for **`__X__`** # - The search for such names begins at classes, not instances # In[3]: class GetAttrTest(object): data = "spam" def __getattr__(self, attrname): print(attrname) return getattr(self.data, attrname) g = GetAttrTest() g.__add__ = lambda y: 88 + y g + 1 # Direct calls to built-in method names still work, but equivalent expression is not # In[4]: # Direct calls still work g.__add__(1) # ### The effect # # To code a proxy of an object whose interface may in part be invoked by built-in operations, new style classes require both **`--getattr__`** for normal names, as well as method redefinitions for all names accessed by built-in operations # ## 2. Classes and types merges # **type(I)** return the class an instance is made from, instead of generic instance type # In[5]: type(5) # - type checking is ususally not recommended in Python # - **isinstance** might be prefered, but still not recommened # In[6]: isinstance(5, (int, str)) # type and class hasve merged - type is a kind of object while object is a kind of type # In[7]: isinstance(type, object) # In[8]: isinstance(object, type) # ## 3. Automatic object root class # A small set of default operator overloading method (e.g. `__repr__`) # # ## 4. Inheritance search order: MRO and diamonds (Also mentioned in Ch31) # - Classic Class (Python2) -> DFLR # - New-style Class (Python3) -> MRO # - New-Style class inheritance works the same for most other inheritance tree structures # # ### Pros of MRO # - Avoids visiting the same superclass more than once when it's accessible from multiple subclasses (performance optimization) # - Without the new-style MRO, **object** would always override redefinitions in user-coded classes # In[9]: get_ipython().run_cell_magic('python2', '', '# To make this cell magic runs ok please ensure python2 can be run when you type python2 in terminal\n# Reference: http://stackoverflow.com/questions/30201431/ipython-cell-magics\n\n# DFLR: D -> B -> A -> C -> A\n\nclass A: attr =1\nclass B(A): pass\nclass C(A): attr = 2\nclass D(B, C): pass\n\nx = D()\nprint(x.attr) # x -> D -> B -> A\n') # In[10]: # MRO: D -> B -> C -> A class A: attr = 1 class B(A): pass class C(A): attr = 2 class D(B, C): pass x = D() print(x.attr) # x -> D -> B -> C # You can simply resolve this by using **`attr = C.attr`** in in class D # In[11]: get_ipython().run_cell_magic('python2', '', '\nclass A: attr =1\nclass B(A): pass\nclass C(A): attr = 2\nclass D(B, C): attr = C.attr\n\nx = D()\nprint(x.attr) # x -> D -> B -> A\n') # ### `__mro__` # In[12]: class A: attr = 1 class B(A): pass class C(A): attr = 2 class D(B, C): pass D.__mro__ # ### How MRO works # 1. List all the classes using the classic class's DFLR and include a class multiple times if it's vistied more than once # 2. Scan the resulting list for duplicate classes, remove all but the last occurrence of the duplicates in the list # ## 5. Inhertitance algorithm # Will be mentioned in Ch40 # # ## 6. New advanced tools # # ### slot: Attribute Declarations # - **slots should be used only in applications that clearly warrant the added complexity** # #### Basic Example # In[13]: class limiter(object): __slots__ = ["age", "name", "job"] x = limiter() x.age # In[14]: x.age = 40 # must be assign first print(x.age) # In[15]: x.ape = 1000 # - To save space, instead of allocating dictionary for each instance, Python reserves just enough space in each instance to hold a value for each slot attribute, along with inherited attributes in the common class to manage slot access # - Best reserved for rare cases where there are large number of instances in memory-critical applications # - In Python3.3, non-slots attribute space requirements have been reduced with a key-sharing dictionay model, where the **`__dict__`** used for objects' attributes may share part of their internal storage, including that of their keys. This may lessen some of the value of **`__slots__`** as an optimization tool # - Classes with **`__slots__`** do not have **`__dict__`** by defualt # In[16]: class C(object): __slots__ = ["a"] c = C() c.__dict__ # In[17]: dir(c) # - However, **`__dict__`** can still be included in **`__slots__`** # In[18]: class C(object): __slots__ = ["a", "b", "__dict__"] d = 3 def __init__(self): self.d = 5 self.a = 1 c = C() c.e = 5 c.__dict__ # Without an attribute namespace dict, it's not possible to assign new names to instances that are not in slots list # In[19]: class C(object): __slots__ = ["a", "b"] def __init__(self): self.d = 4 c = C() # ### Slot Usage Rules # # - Slots in subs are pointless when absent in supers # - **`__dict__`** created for the superclass will always be accessible # - Subclass still manages its slots, but doesn't compute their values in anyway, and doesn't avoid a dict # - Slots in supers are pointless when absent in subs # - Subclasses will produce and instacne **`__dict__`** # - Redefintion renders super slots pointless # - A class defines the same slot name as a superclass, its redefinition hides the slot in superclass # - Slots prevent class-level defaults # - Class attributes of the same name to provide default cannot be uesd # - Slots and **`__dict__`** # - Slots preclude **`__dict__`**, unless it's listed explicitly # # ``` # slots essentially require both universal and careful deployment to be effective # slots are not generally recommended, except in pathologicall cases where their space reduction is significant # ``` # ### Properties: Attribute Accessors # - Similar to proerties in languages like Java and C# # - proerties inercept accesss and compute values arbitrarily # # # #### Basics # - A property is a type of object assigned to a class attribute name # - By calling **property** and passing in up to three accessor methods (handlers for get, set and delete) # - If any of them is None or omitted, then that operation is not supported # - The result property object is typically assigned to a name at the top level of a class # - After thus assigned, accesses to the class property name are automatically routed to one of the accessor methods # # In[20]: class Proper(object): def getage(self): return 40 age = property(getage, None, None, None) # (get, set, del, docs) p = Proper() p.age # ### `__getattribute__` and Descriptors # # - **`__getattribute__`** is available for new-style classes only and used to intercept **all** attribute # - It's prone to loops much like **`__setattr__`** # - Python supports the notion of attribute descriptors - classes with **`__get__`** and **`__set__`** # In[21]: class AgeDesc(object): def __get__(self, instance, owner): return 30 def __set__(self, instance, value): instance._age = value class D(object): age = AgeDesc() d = D() d.age # In[22]: d.age = 42 d._age # --- # # Static and Class Methods # # ## Static Methods # ### Static Methods in Python2 and Python3 # # - To call a method without instance # - In Python2, **staticmethod** is a must # - In Python3, **staticmethod** is needed only if it would be called through an instance # In[23]: # Works both Python2 and Python3 class C(object): @staticmethod def print_one(): print(1) c = C() c.print_one() # In[24]: # Python3 Only class C(object): def print_one(): print(1) C.print_one() # Look at the code above. # Since **print_one** does not operate any class or instance data, it may be a good idea to make it static # ## Class Method # - Methods of a class that are passed a class object in their first argument instead of an instance, regardless of whether they are called through an instance or a class # # - Receive the lowest class of the call's subject # In[25]: class C(object): counter = 0 @classmethod def cmethod(cls): cls.counter += 1 print(cls.counter) c1 = C() c1.cmethod() c2 = C() c2.cmethod() # ## Static Method VS Class Method # # - static method: process data local to a class # - class method: process data that may differ for each class in hierarchy # - code that needs to manage per-class instance counters, for example, might be best off leveragin class method # ## Why using static/class method instead of simple function? # - Localized the function name in class scope # - Move the function closer to where it's used # - Allows subclasses to customize # --- # # Decorators and Metaclasses (Detail in Ch39, Ch40) # - Decorators # - General tool for adding logic that manages both functions and classes, or later calls to them # - e.g. log, count calls, check its argument types # # ## Function decorator # - Augment function definitions # - Wrap class method in an extra layer of logic implemented as another function, usually called metafunction # - Similar to delegation design pattern but designed to augment a specific funtcion # - A sort of runtime declaration # ### What the function decorators do # In[26]: # Decorator version class C: @staticmethod def meth(): pass # In[27]: class C: def meth(): pass meth = staticmethod(meth) # ### User-Defined Function Decotator # Use **`__call__`** # In[28]: class tracer: def __init__(self, func): self.calls = 0 self.func = func def __call__(self, *args): self.calls += 1 print("Call {} to {}".format(self.calls, self.func.__name__)) return self.func(*args) @tracer def spam(a, b, c): # Same as spam=tracer(spam) return a + b + c print(spam(1, 2, 3)) print(spam(3, 4, 5)) # #### Explanation # - The **spam** function is run through the **tracer** decorator, when the original **spam** is called it actually triggers the **`__call__`** in the class # # - **`*name`** argument is used to pack and unpack the passed-in arguments, because of this, this decorator can be used to wrap any function with any number of positional arguments # ## Class dectorator and metaclass # - Augment class definitions # - Add supoort for management of whole objects and their interface (metaclass) # - Manage an object's entire interface by intercepting construction calls # - metaclass generally redeines the **`__new__`** or **`__init__`** of the **type** class that normally intercepts this call # - meta class need not be a class at all # ### What the class decorators do # In[29]: def decorator(a_class): pass @decorator class C(object): pass # In[30]: def decoraor(a_class): pass class C(object): pass C = decoraor(C) # --- # # Super # # Skip # # --- # # Class Gotachas # # ## Class Are Mutable Objects (Side Effect) # - Chaning class attributes can have side effects # In[3]: class X(object): a = 1 X.a = 2 x2 = X() print(x2.a) # ## Multiple Inheritance: Order Matters # - Python always searches superclasses from left to right # - As a rule of thumb, multiple inheritance works best when mix-in classes are as self-contained as possible # ## Other Issue # - Choose per-instance or class storage wisely # - You usually want to call superclass constructors