OOP is about code Re-Use
About looking up attributes in trees
2x python features that can be used to enforce constraints from derived class to base class:
object -> (list, dict, tuple, set)
automatically installs this class under the object class.
class MyClass(object): as it is done automatically in python 3.SUMMARY:
type.3.3.3.1. Metaclasses
constructed using type().body is executed in a new namespaceclass name is bound locally to the result of type(name, bases, namespace).class creation process can be customized by passing the metaclass keyword argument in the class definition line, or by inheriting from an existing class that included such an argument.MyClass and MySubclass are instances of Meta:class Meta(type):
pass
class MyClass(metaclass=Meta):
pass
class MySubclass(MyClass):
pass
When a class definition is executed, the following steps occur:
type() is used;metaclass is given and it is not an instance of type(), then it is used directly as the metaclass;type() is given as the explicit metaclass, or bases are defined, then the most derived metaclass is used.metaclass (if any) and the metaclasses (i.e. type(cls)) of all specified base classes.metaclasses.none of the candidate metaclasses meets that criterion, then the class definition will fail with TypeError.__prepare__ attribute, it is called as namespace = metaclass.__prepare__(name, bases, **kwds) (where the additional keyword arguments, if any, come from the class definition).__prepare__ method should be implemented as a classmethod.__prepare__ is passed in to __new__, but when the final class object is created the namespace is copied into a new dict.__prepare__ attribute, then the class namespace is initialised as an empty ordered mapping.PEP 3115 - Metaclasses in Python 3000
__prepare__ namespace hookexec(body, globals(), namespace).exec() is that lexical scoping allows the class body (including any methods) to reference names from the current and outer scopes when the class definition occurs inside a function.__class__ reference described in the next section.Once the class namespace has been populated by executing the class body, the class object is created by calling metaclass(name, bases, namespace, **kwds) (the additional keywords passed here are the same as those passed to __prepare__).
This class object is the one that will be referenced by the zero-argument form of super().
class is an implicit closure reference created by the compiler if any methods in a class body refer to either __class__ or super.
This allows the zero argument form of super() to correctly identify the class being defined based on lexical scoping, while the class or instance that was used to make the current call is identified based on the first argument passed to the method.
CPython implementation detail:
__class__ cell is passed to the metaclass as a __classcell__ entry in the class namespace.type.__new__ call in order for the class to be initialised correctly.RuntimeError in Python 3.8.When using the default metaclass type, or any metaclass that ultimately calls type.__new__, the following additional customization steps are invoked after creating the class object:
type.__new__ method collects all of the attributes in the class namespace that define a __set_name__() method;__set_name__ methods are called with the class being defined and the assigned name of that particular attribute;__init_subclass__() hook is called on the immediate parent of the new class in its method resolution order.type.__new__, the object provided as the namespace parameter is copied to a new ordered mapping and the original object is discarded.
__dict__ attribute of the class object.Links:
SUMMARY:
from abc import ABC class.from abc import ABC, abstractmethod
class AbstractClassName(ABC):
@abstractmethod
def abstract_method_name(self):
pass
def concrete_method(self):
pass
Their current form in AbstractBag is appropriate for unordered collections,
but most collection classes are likely to be linear rather than unordered.
Note that the classes trend from the more general to the more specific in character, as your eyes move down through the hierarchy.
Now when a new collection type, such as ListInterface, comes along, you can create an abstract class for it, place that under
AbstractCollection, and begin with some data and methods ready to hand. The concrete
implementations of lists would then go under the abstract list class.
To create the AbstractCollection class, you copy code as usual from another module, in this case, AbstractBag.
You can now perform the following steps:
- You then remove the isEmpty, __len__, count and __add__ methods from AbstractBag.
- The implementation of AbstractCollection and the modification of AbstractBag are left
SUMMARY:
subclass generally is a more specialized version of its superclass.superclass is also called the parent of its subclasses.subclass inherits all the methods and variables from its parent class, as well as any of its ancestor classes.subclass specializes the behavior of its superclass by modifying its methods or adding new methods.call a method in its superclass by using the superclass name as a prefix to the method.abstract class serves as a repository of data and methods that are common to a set of other classes.not abstract, they are called concrete classes.Abstract classes are not instantiated.Note also that AbstractSet, unlike AbstractBag, is not a subclass of AbstractCollection.
The reason for this is that AbstractSet does not introduce any new instance variables for data, but just defines additional methods peculiar to all sets.
You now explore the array-based implementation to clarify who is inheriting what from whom in this hierarchy.
Methods:
__and__()
__or__()
__sub__()
issubset
You can view a dictionary as a set of key/value pairs called entries.
However, a dictionary’s interface is rather different from that of a set.
As you know from using Python’s dict type, values are inserted or replaced at given keys using the subscript operator [].
The method pop removes a value at a given key, and the methods keys and values return iterators on a dictionary’s set of keys and collection of values, respectively.
The __iter__ method supports a for loop over a dictionary’s keys.
The method get allows you to access a value at a key or recover by returning a default value if the key is not present.
The common collection methods are also supported.
super() is single inheritance.class SendEmailGetSet:
def __init__(self):
self.init_email = ''
@property
def email_addr(self):
return self.init_email
@email_addr.setter
def email_addr(self, new_email_addr):
self.init_email = new_email_addr
if __name__ == "__main__":
e = SendEmailGetSet()
print('email: ', e.email_addr)
e.email_addr = 'my_email@gmail.com'
print('email: ', e.email_addr)
class SendEmailGet:
def __init__(self):
self._addr = 'your_email@gmail.com'
@property
def email_addr(self):
return self._addr
if __name__ == "__main__":
e = SendEmailGet()
email = e.email_addr
print('email: ', email)
DESCRIPTION:
Are created by calling a class object.
Has a namespace implemented as a dictionary which is the first place in which attribute references are searched.
attribute by that name, the search continues with the class attributes.__self__ attribute is the instance.See section Implementing Descriptors for another way in which attributes of a class retrieved via its instances may differ from the objects actually stored in the class’s __dict__.
If no class attribute is found, and the objects class has a __getattr__() method, that is called to satisfy the lookup.
Attribute assignments and deletions update the instance’s dictionary, never a class’s dictionary.
__setattr__() or __delattr__() method, this is called instead of updating the instance dictionary directly.Class instances can pretend to be numbers, sequences, or mappings if they have methods with certain special names.
See section Special method names.
Special attributes:
__dict__ is the attribute dictionary.__class__ is the instance’s class.from functools import wraps, partial
'''1. DEBUGS FUNCTIONS'''
def debug(func=None, *, prefix=''):
'''
https://youtu.be/sPiWg5jSoZI?list=PLYV45E82CBTECjpsWX5btprBVa9lWm1vZ&t=1201
- debugs functions by printing out nice error repr
'''
if func is None:
return partial(debug, prefix=prefix)
@wraps(func)
def wrapper(*args, **kwargs):
print(func.__qualname__)
return func(*args, **kwargs)
return wrapper
'''2. DEBUGS CLASSES (just methods, not @classmethods or @staticmethods)'''
def debugmethods(cls):
for key, val in vars(cls).items():
if callable(val):
setattr(cls, key, debug(val))
return cls
# https://youtu.be/sPiWg5jSoZI?list=PLYV45E82CBTECjpsWX5btprBVa9lWm1vZ&t=1387
@debugmethods
class Spam:
def a(self):
pass
def b(self):
pass
'''3. Meta Class that propogates through entire class hierarchy.
Can think of it almost as a genetic mutation.
https://youtu.be/sPiWg5jSoZI?list=PLYV45E82CBTECjpsWX5btprBVa9lWm1vZ&t=1589'''
class debugmeta(type):
def __new__(cls, clsname, bases, clsdict):
clsobj = super().__new__(cls, clsname, bases, clsdict)
clsobj = debugmethods(clsobj)
return clsobj
class Base(metaclass=debugmeta):
pass
class Ham(Base):
def a(self):
pass
def b(self):
pass
if __name__ == "__main__":
s = Spam()
s.a
x = Ham()
x.a
This produces the following output.
>>> import meta_programming_utube as mu
>>> q = mu.Ham
>>> q = mu.Ham()
>>> q.a
<bound method Ham.a of <meta_programming_utube.Ham object at 0x7f84952a3b50>>
>>> q.av
Traceback (most recent call last):
File "<input>", line 1, in <module>
q.av
AttributeError: 'Ham' object has no attribute 'av'
>>> q.be
Traceback (most recent call last):
File "<input>", line 1, in <module>
q.be
AttributeError: 'Ham' object has no attribute 'be'
>>>
__init__()__new__()), but before it is returned to the caller.
__init__() method, the derived class’s __init__() method, if any, must explicitly call it to ensure proper initialization of the base class part of the instance;
super().__init__([args...]).__new__() and __init__() work together in constructing objects (__new__() to create it, and __init__() to customize it), no non-None value may be returned by __init__();
__call__()DESCRIPTION:
Called to create a new instance of class cls. __new__() is a static method (special-cased so you need not declare it as such) that takes the class of which an instance was requested as its first argument.
__new__() should be the new object instance.
(usually an instance of cls).Typical implementations create a new instance of the class by invoking the superclass’s __new__() method using super().__new__(cls[, ...]) with appropriate arguments and then modifying the newly-created instance as necessary before returning it.
If __new__() is invoked during object construction and it returns an instance of cls, then the new instance’s __init__() method will be invoked like __init__(self[, ...]), where self is the new instance and the remaining arguments are the same as were passed to the object constructor.
If __new__() does not return an instance of cls, then the new instance’s __init__() method will not be invoked.
__new__() is intended mainly to allow subclasses of immutable types (like int, str, or tuple) to customize instance creation.
__del__()__del__() method to postpone destruction of the instance by creating a new reference to it. This is called object resurrection. It is implementation-dependent whether del() is called a second time when a resurrected object is about to be destroyed;
__del__() methods are called for objects that still exist when the interpreter exits.__str__()object.__repr__() in that there is no expectation that __str__() return a valid Python expression: a more convenient or concise representation can be used.object.__repr__().class StringEx:
'''
in: se = StringEx('red')
print(se)
out: color is: red
'''
def __init__(self, color: str):
self.color = color
def __str__(self):
return f'color is: {self.color}'
se = StringEx('red')
print(se)
# out: color is: red
__repr__()__repr__() but not __str__(), then __repr__() is also used when an “informal” string representation of instances of that class is required.class ReprEx:
'''
in: re = ReprEx('red')
print(re)
out: color is: red
'''
def __init__(self, color: str):
self.color = color
def __repr__(self):
return f'color is: {self.color}'
# re = ReprEx('red')
# print(re)
# out: color is: red
__bytes__()DESCRIPTION:
__format__()DESCRIPTION:
str.format() method, to produce a “formatted” string representation of an object.__format__(), however most classes will either delegate formatting to one of the built-in types, or use a similar formatting option syntax.must be a string object.__format__ method of object itself raises a TypeError if passed any non-empty string.object.__format__(x, '') is now equivalent to str(x) rather than format(str(x), '').__lt__(self, other)__le__(self, other)__eq__(self, other)__ne__(self, other)__gt__(self, other)__ge__(self, other)x<y calls x.__lt__(y), x<=y calls x.__le__(y), x==y calls x.__eq__(y), x!=y calls x.__ne__(y), x>y calls x.__gt__(y), and x>=y calls x.__ge__(y).bool() on the value to determine if the result is true or false.__eq__() by using is, returning NotImplemented in the case of a false comparison: True if x is y else NotImplemented.
__ne__(), by default it delegates to __eq__() and inverts the result unless it is NotImplemented.(x<y or x==y) does not imply x<=y.
functools.total_ordering().__hash__() for some important notes on creating hashable objects which support custom comparison operations and are usable as dictionary keys.__lt__() and __gt__() are each other’s reflection, __le__() and __ge__() are each other’s reflection, and __eq__() and __ne__() are their own reflection.__hash__(self)Summary:
__eq__() method it should not define a __hash__() operation either;
__eq__() but not __hash__(), its instances will not be usable as items in hashable collections.__eq__() method, it should not implement __hash__(), since the implementation of hashable collections requires that a key’s hash value is immutable (if the object’s hash value changes, it will be in the wrong hash bucket).Description:
hash() and for operations on members of hashed collections including set, frozenset, and dict.__hash__() should return an integer.Example:def __hash__(self):
return hash((self.name, self.nick, self.color))
__eq__() and __hash__() methods by default;
x.__hash__() returns an appropriate value such that x == y implies both that x is y and hash(x) == hash(y).__eq__() and does not define __hash__() will have its __hash__() implicitly set to None. When the __hash__() method of a class is None, instances of the class will raise an appropriate TypeError when a program attempts to retrieve their hash value, and will also be correctly identified as unhashable when checking isinstance(obj, collections.abc.Hashable).__eq__() needs to retain the implementation of __hash__() from a parent class, the interpreter must be told this explicitly by setting __hash__ = <ParentClass>.__hash__.__eq__() wishes to suppress hash support, it should include __hash__ = None in the class definition.__hash__() that explicitly raises a TypeError would be incorrectly identified as hashable by an isinstance(obj, collections.abc.Hashable) call.Note:
__hash__() values of str and bytes objects are “salted” with an unpredictable random value.O(n2) complexity.
PYTHONHASHSEED.object.__bool__(self)built-in operation bool();
__len__() is called, if it is defined, and the object is considered true if its result is nonzero.__len__() nor __bool__(), all its instances are considered true.object.__getattr__(self, name)SUMMARY
. name syntax.DESCRIPTION:
AttributeError
__getattribute__() raises an AttributeError because name is not an instance attribute or an attribute in the class tree for self;
__get__() of a name property raises AttributeError).return the (computed) attribute value or raise an AttributeError exception.Note:
__getattr__() is not called.__getattr__() and __setattr__()).__getattr__() would have no way to access other attributes of the instance.__getattribute__() method below for a way to actually get total control over attribute access.object.__getattribute__(self, name)__getattr__(), the latter will not be called unless __getattribute__() either calls it explicitly or raises an AttributeError.object.__getattribute__(self, name).object.__getattr__ with arguments obj and name.object.__setattr__(self, name, value)__setattr__() wants to assign to an instance attribute, it should call the base class method with the same name, for example, object.__setattr__(self, name, value).For certain sensitive attribute assignments, raises an auditing event object.__setattr__ with arguments obj, name, value.
object.__delattr__(self, name)__setattr__() but for attribute deletion instead of assignment.object.__delattr__ with arguments obj and name.object.__dir__(self)dir() is called on the object.sequence must be returned.dir() converts the returned sequence to a list and sorts it.__dict__.object.__get__(self, instance, owner=None)AttributeError exception.__get__() is callable with one or two arguments.__getattribute__() implementation always passes in both arguments whether they are required or not.object.__set__(self, instance, value)__set__() or __delete__() changes the kind of descriptor to a “data descriptor”.
object.__delete__(self, instance)delete the attribute on an instance instance of the owner class.__objclass__ is interpreted by the inspect module as specifying the class where this object was defined
dynamic class attributes).callables, it may indicate that an instance of the given type (or a subclass) is expected or required as the first positional argumentobject.__slots__stringiterablesequence of strings
__slots__ reserves space for the declared variables
__dict__ and __weakref__ for each instance.Notes:
__slots__, the __dict__ and __weakref__ attribute of the instances will always be accessible.__dict__ variable, instances cannot be assigned new variables not listed in the __slots__ definition.dynamic assignment of new variables is desired, then add __dict__ to the sequence of strings in the __slots__ declaration.__weakref__ variable for each instance, classes defining __slots__ do not support weak references to its instances.__weakref__ to the sequence of strings in the __slots__ declaration.__slots__ are implemented at the class level by creating descriptors for each variable name.__slots__;
__slots__ declaration is not limited to the class where it is defined.__slots__ declared in parents are available in child classes.
__dict__ and __weakref__ unless they also define __slots__
only contain names of any additional slots).class defines a slot also defined in a base class, the instance variable defined by the base class slot is inaccessible (except by retrieving its descriptor directly from the base class).
Nonempty __slots__ does not work for classes derived from variable-length built-in types such as int, bytes and tuple.__slots__.__slots__, the dictionary keys will be used as the slot names.
inspect.getdoc() and displayed in the output of help().__class__ assignment works only if both classes have the same __slots__.raise TypeError.__slots__ then a descriptor is created for each of the iterator’s values.
__slots__ attribute will be an empty iterator.__init_subclass__() is called on that class.subclasses.class decorators, but where class decorators only affect the specific class they’re applied to, __init_subclass__ solely applies to future subclasses of the class defining the method.classmethod object.__init_subclass__(cls)cls is then the new subclass.normal instance method, this method is implicitly converted to a class method.__init_subclass__.__init_subclass__, one should take out the needed keyword arguments and pass the others over to the base class, as in:class Philosopher:
def __init_subclass__(cls, /, default_name, **kwargs):
super().__init_subclass__(**kwargs)
cls.default_name = default_name
class AustralianPhilosopher(Philosopher, default_name="Bruce"):
pass
object.__init_subclass__ does nothing, but raises an error if it is called with any arguments.metaclass is consumed by the rest of the type machinery, and is never passed to __init_subclass__ implementations.metaclass (rather than the explicit hint) can be accessed as type(cls).object.__set_name__(self, owner, name)class A:
x = C() # Automatically calls: x.__set_name__(A, 'x')
__set_name__() will not be called automatically.__set_name__() can be called directly:class A:
pass
c = C()
A.x = c # The hook is not called
c.__set_name__(A, 'x') # Manually invoke the hook
object.__call__(self[, args...])https://stackoverflow.com/questions/3369640/when-is-using-call-a-good-idea
SUMMARY:
Called under the hood whenever you use something with parentheses func(10).
However, __call__ has quite a bit of competition in the Python world:
A regular named method, whose behavior can sometimes be a lot more easily deduced from the name. Can convert to a bound method, which can be called like a function.
A closure, obtained by returning a function that's defined in a nested block.
A lambda, which is a limited but quick way of making a closure.
Generators and coroutines, whose bodies hold accumulated state much like a functor can.
I'd say the time to use call is when you're not better served by one of the options above. Check the following criteria, perhaps:
Your object has state.
There is a clear "primary" behavior for your class that's kind of silly to name. E.g. if you find yourself writing run() or doStuff() or go() or the ever-popular and ever-redundant doRun(), you may have a candidate.
Your object has state that exceeds what would be expected of a generator function.
Your object wraps, emulates, or abstracts the concept of a function.
Your object has other auxilliary methods that conceptually belong with your primary behavior.
One example I like is UI command objects. Designed so that their primary task is to execute the comnand, but with extra methods to control their display as a menu item, for example, this seems to me to be the sort of thing you'd still want a callable object for.
DESCRIPTION:
“called” as a function;
x(arg1, arg2, ...) roughly translates to type(x).__call__(x, arg1, ...).container objects.mappings (like dictionaries), but can represent other containers as well.sequence or to emulate a mapping;
sequence, the allowable keys should be integers k for which 0 <= k < N where N is the length of the sequence, or slice objects, which define a range of items.keys(), values(), items(), get(), clear(), setdefault(), pop(), popitem(), copy(), and update() behaving similar to those for Python’s standard dictionary objects.collections.abc module provides a MutableMapping abstract base class to help create those methods from a base set of __getitem__(), __setitem__(), __delitem__(), and keys().append(), count(), index(), extend(), insert(), pop(), remove(), reverse() and sort(), like Python standard list objects.sequence types should implement addition (meaning concatenation) and multiplication (meaning repetition) by defining the methods __add__(), __radd__(), __iadd__(), __mul__(), __rmul__() and __imul__() described below;
numerical operators.mappings and sequences implement the __contains__() method to allow efficient use of the in operator;
keys;values.__iter__() method to allow efficient iteration through the container;
mappings, __iter__() should iterate through the object’s keys;sequences, it should iterate through the values.object.__len__(self)len().integer >= 0.__bool__() method and whose __len__() method returns zero is considered to be false in a Boolean context.sys.maxsize some features (such as len()) may raise OverflowError.raising OverflowError by truth value testing, an object must define a __bool__() method.object.__length_hint__(self)operator.length_hint().integer >= 0.NotImplemented, which is treated the same as if the __length_hint__ method didn’t exist at all.object.__getitem__(self, key)SUMMARY:
["item"] syntax.__getitem__ or __iter__.__iter__ first, which returns an object that supports the iteration protocol with a __next__ method:
__iter__ is found by inheritance search, Python falls back on the __getitem__ indexing method,DESCRIPTION:
self[key].integers and slice objects.
__getitem__() method.TypeError may be raised;
IndexError should be raised.key is missing (not in the container), KeyError should be raised.IndexError will be raised for illegal indexes to allow proper detection of the end of the sequence.__class_getitem__() may be called instead of __getitem__().
__class_getitem__ vs __getitem__ for more details.object.__setitem__(self, key, value)SUMMARY:
dictDESCRIPTION:
self[key].__getitem__().__getitem__() method.object.__delitem__(self, key)self[key].__getitem__().__getitem__() method.object.__missing__(self, key)dict.__getitem__() to implement self[key] for dict subclasses when key is not in the dictionary.object.__iter__(self)object.__reversed__(self)new iterator object that iterates over all the objects in the container in reverse order.__reversed__() method is not provided, the reversed() built-in will fall back to using the sequence protocol (__len__() and __getitem__()).__reversed__() if they can provide an implementation that is more efficient than the one provided by reversed().(in and not in) are normally implemented as an iteration through a container.
object.__contains__(self, item)membership test operators.
return true if item is in self, false otherwise.mapping objects, this should consider the keys of the mapping rather than the values or the key-item pairs.__contains__():
__iter__(),__getitem__()
object.add(self, other)
object.sub(self, other)
object.mul(self, other)
object.matmul(self, other)
object.truediv(self, other)
object.floordiv(self, other)
object.mod(self, other)
object.divmod(self, other)
object.pow(self, other[, modulo])
object.lshift(self, other)
object.rshift(self, other)
object.and(self, other)
object.xor(self, other)
object.__or__(self, other)__truediv__().
__pow__() should be defined to accept an optional third argument if the ternary version of the built-in pow() function is to be supported.object.radd(self, other)
object.rsub(self, other)
object.rmul(self, other)
object.rmatmul(self, other)
object.rtruediv(self, other)
object.rfloordiv(self, other)
object.rmod(self, other)
object.rdivmod(self, other)
object.rpow(self, other[, modulo])
object.rlshift(self, other)
object.rrshift(self, other)
object.rand(self, other)
object.rxor(self, other)
object.__ror__(self, other)pow() will not try calling __rpow__()
object.iadd(self, other)
object.isub(self, other)
object.imul(self, other)
object.imatmul(self, other)
object.itruediv(self, other)
object.ifloordiv(self, other)
object.imod(self, other)
object.ipow(self, other[, modulo])
object.ilshift(self, other)
object.irshift(self, other)
object.iand(self, other)
object.ixor(self, other)
object.__ior__(self, other)x is an instance of a class with an __iadd__() method,
x += y is equivalent to x = x.__iadd__(y).x.__add__(y) and y.__radd__(x) are considered, as with the evaluation of x + y.a_tuple[i] += [‘item’] raise an exception when the addition works?), but this behavior is in fact part of the data model.object.neg(self)
object.pos(self)
object.abs(self)
object.__invert__(self)object.__float__(self)complex(), int() and float().object.__index__(self)convert the numeric object to an integer object (such as in slicing, or in the built-in bin(), hex() and oct() functions).numeric object is an integer type.integer.__int__(), __float__() and __complex__() are not defined then corresponding built-in functions int(), float() and complex() fall back to __index__().object.round(self[, ndigits])
object.trunc(self)
object.floor(self)
object.__ceil__(self)round() and math functions trunc(), floor() and ceil().__round__() all these methods should return the value of the object truncated to an Integral (typically an int).int() falls back to __trunc__() if neither __int__() nor __index__() is defined.