Skip to content

Commit dae5078

Browse files
committed
Metaprogramming complete
1 parent ea74e3f commit dae5078

File tree

7 files changed

+363
-44
lines changed

7 files changed

+363
-44
lines changed

10_class/03-staticmethod.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
__doc__ = """Let's emulate decorator @staticmethod using metaprogramming
2+
Python @staticmethod is a decorator that is used to define a static method for a function in a class.
3+
It's quite useless in general, because you very rarely need to call a static method and not a regular function.
4+
5+
Decorator @classmethod is much more useful, because it allows you to call a method of a class,
6+
instead of a method of an instance of a class.
7+
Let's emulate it as well
8+
9+
We may use @classmethod to create a factory method, which is a method that returns an instance of a class.
10+
"""
11+
12+
13+
# noinspection PyPep8Naming, PyShadowingBuiltins
14+
class staticmethod(object):
15+
"""
16+
Python @staticmethod is a decorator that is used to define a static method for a function in a class.
17+
It is just a regular function that is defined inside a class.
18+
"""
19+
20+
def __init__(self, function):
21+
self.function = function
22+
23+
def __get__(self, obj, cls=None):
24+
return self.function
25+
26+
27+
# noinspection PyPep8Naming,PyShadowingBuiltins
28+
class classmethod(object):
29+
def __init__(self, function):
30+
self.function = function
31+
32+
def __get__(self, obj, cls=None):
33+
if cls is None:
34+
cls = type(obj)
35+
36+
def new_function(*args, **kwargs):
37+
return self.function(cls, *args, **kwargs)
38+
39+
return new_function
40+
41+
42+
# Example 1: Static Method
43+
class UseStatic:
44+
def __init__(self):
45+
pass
46+
47+
@staticmethod
48+
def foo():
49+
print("foo")
50+
51+
52+
UseStatic.foo()
53+
54+
55+
# Example 2: Class Method
56+
class UseClassMethod:
57+
def __init__(self):
58+
pass
59+
60+
@classmethod
61+
def foo(cls):
62+
print(f"cls is {cls}")
63+
64+
65+
UseClassMethod.foo()

10_class/04-call.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
__doc__ = """In order to make instance callable, we need to implement __call__ method.
2+
Class `type` use `__call__` method for creating new instances of the class.
3+
4+
Let's emulate it using metaprogramming (in reality its implementation is written in C).
5+
6+
This feature can be used in many powerful design patterns, e.g. Singleton, Factory, etc.
7+
Let's implement Singleton using the same technique
8+
9+
Method `__new__` is static by definition, so we need to pass cls explicitly
10+
It reminds of a constructor of the class, but actually it's not.
11+
`__new__` is a static method that takes the class as a first parameter,
12+
it's not necessarily to return a new instance of the class.
13+
"""
14+
15+
16+
# Example 1. Create a class using `type` metaclass
17+
# noinspection PyPep8Naming, PyShadowingBuiltins, PyArgumentList
18+
class type(object):
19+
20+
def __call__(self, *args, **kwargs):
21+
"""
22+
:param args: arguments to be passed to the constructor of the class
23+
:param kwargs: keyword arguments to be passed to the constructor of the class
24+
:return: instance of the class
25+
"""
26+
# Create a new instance of the class
27+
obj = self.__new__(self, *args, **kwargs)
28+
# Initialize the instance of the class
29+
obj.__init__(*args, **kwargs)
30+
# Return the instance of the class
31+
return obj
32+
33+
34+
class UseType:
35+
"""
36+
We don't mention metaclass explicitly, because our `type` shadows the built-in `type`
37+
"""
38+
def __init__(self, a, b):
39+
self.a = a
40+
self.b = b
41+
42+
def __repr__(self):
43+
return f"UseType(a={self.a}, b={self.b})"
44+
45+
46+
c = UseType(1, 2)
47+
print(c)
48+
49+
50+
# Example 2. Create a Singleton class
51+
class Singleton:
52+
"""
53+
Singleton is a design pattern that restricts the instantiation of a class to one object.
54+
It is useful when exactly one object is needed to coordinate actions across the system.
55+
"""
56+
_instance = None
57+
58+
def __new__(cls, *args, **kwargs):
59+
"""
60+
:param args: arguments to be passed to the constructor of the class
61+
:param kwargs: keyword arguments to be passed to the constructor of the class
62+
:return: instance of the class
63+
"""
64+
if cls._instance is None:
65+
cls._instance = object.__new__(cls)
66+
cls._instance.__init__(*args, **kwargs)
67+
return cls._instance
68+
69+
def __init__(self, a, b):
70+
self.a = a
71+
self.b = b
72+
73+
def print_params(self):
74+
print(f"a == {self.a}, b == {self.b}")
75+
76+
77+
singleton = Singleton(1, 2)
78+
print(singleton)
79+
singleton.print_params()
80+
81+
singleton2 = Singleton(3, 4)
82+
print(singleton2)
83+
singleton2.print_params()
84+
85+
86+
assert singleton is singleton2

10_class/05-meta.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
__doc__ = """In Python we can use meta-programming to create classes and functions dynamically.
2+
By default, Python uses `type` metaclass to create classes.
3+
It can be changed by specifying `metaclass` keyword argument in the class definition.
4+
5+
Usually metaclass is derived from `type` metaclass, but technically it's not necessary.
6+
Metaclass must provide `__call__` method, which is called when the class is instantiated.
7+
On this place can be any callable, e.g. function, class, lambda, etc.
8+
9+
Method `__prepare__` is called before `__new__` and `__init__`
10+
It must return a mapping object that will be used as a namespace for the class.
11+
12+
Useful metaclasses:
13+
* `abc.ABCMeta` - abstract base classes
14+
* Iterable - `collections.abc.Iterable` throws an exception if `__iter__` is not implemented
15+
"""
16+
17+
import abc
18+
19+
20+
class Base:
21+
pass
22+
23+
24+
# noinspection PyUnresolvedReferences
25+
class UseMetaLambda(Base, var=92, metaclass=lambda *args, **kwargs: print(f"meta({args}, {kwargs})")):
26+
"""
27+
Pass lambda object as a metaclass
28+
It's enough as it's just another callable
29+
It prints: class name, base classes, keyword arguments, etc.
30+
"""
31+
32+
def __init__(self, a, b):
33+
self.a = a
34+
self.b = b
35+
36+
def print_me(self):
37+
print(f"a == {self.a}, b == {self.b}")
38+
39+
40+
# Don't create instance of this class, it won't work
41+
# print() as a metaclass is not a good idea, because it returns None
42+
# However class declaration itself is enough to call metaclass
43+
44+
45+
# noinspection PyUnresolvedReferences
46+
class Meta(type):
47+
"""
48+
Typical metaclass implementing `__call__`, `__new__` and `__prepare__` methods
49+
"""
50+
51+
def __new__(mcs, name, bases, namespace, **kwargs):
52+
"""
53+
:param mcs: metaclass
54+
:param name: name of the class
55+
:param bases: base classes
56+
:param namespace: namespace of the class
57+
:param kwargs: keyword arguments
58+
:return: new instance of the class
59+
"""
60+
print(f"Meta.__new__({mcs}, {name}, {bases}, {namespace}, {kwargs})")
61+
return super().__new__(mcs, name, bases, namespace)
62+
63+
def __init__(cls, name, bases, namespace, **kwargs):
64+
"""
65+
`__init__` can be compared to "decorator" of metaclass, because
66+
it's called after the class is created, but before it's returned as a result
67+
in order to modify the behavior
68+
:param cls: class
69+
:param name: name of the class
70+
:param bases: base classes
71+
:param namespace: namespace of the class
72+
:param kwargs: keyword arguments
73+
"""
74+
print(f"Meta.__init__({cls}, {name}, {bases}, {namespace}, {kwargs})")
75+
super().__init__(name, bases, namespace)
76+
77+
@classmethod
78+
def __prepare__(mcs, name, bases, **kwargs):
79+
"""
80+
Before Python 3.7. it specified the namespace of the class as an OrderedDict.
81+
After 3.7 dict is ordered by default, so it's not necessary anymore.
82+
:param mcs: metaclass
83+
:param name: name of the class
84+
:param bases: base classes
85+
:param kwargs: keyword arguments
86+
:return: mapping object that will be used as a namespace for the class
87+
"""
88+
print(f"Meta.__prepare__({mcs}, {name}, {bases}, {kwargs})")
89+
return super().__prepare__(name, bases)
90+
91+
def __call__(cls, *args, **kwargs):
92+
"""
93+
:param cls: class
94+
:param args: arguments to be passed to the constructor of the class
95+
:param kwargs: keyword arguments to be passed to the constructor of the class
96+
:return: instance of the class
97+
"""
98+
print(f"Meta.__call__({cls}, {args}, {kwargs})")
99+
obj = cls.__new__(cls, *args, **kwargs)
100+
101+
102+
# noinspection PyUnresolvedReferences
103+
class Iterable(metaclass=abc.ABCMeta):
104+
"""
105+
Metaclass is used to check that the class implements `__iter__` method
106+
You also can check method signature using `inspect.signature` module
107+
"""
108+
@abc.abstractmethod
109+
def __iter__(self):
110+
raise NotImplementedError()

10_class/06-strict.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
__doc__ = """Python is a dynamically typed language, which means that the type of a variable is determined at runtime.
2+
However, it also features descriptor protocol, which allows to implement strict typing of class attributes.
3+
Here's an example of a class that enforces strict typing of class attributes.
4+
5+
Starting from Python 3.6 descriptors can be implemented using `__set_name__` method
6+
which is used for setting the name of the attribute in the class.
7+
8+
Rules of using meta-programming:
9+
* Metaclass is always one
10+
* Metaclass can be derived from other metaclass
11+
* Real metaclass is the most derived metaclass
12+
* Metaclass often inherits from `type` metaclass, however you can completely redefine its behavior yourself
13+
* You can use base `abc.ABCMeta` metaclass to create abstract classes
14+
15+
Metaclass can be conflicting, if different classes in the inheritance hierarchy
16+
have different metaclasses.
17+
"""
18+
19+
20+
# Example 1: Strict Integer using descriptor protocol
21+
class Integer:
22+
def __init__(self, value=0):
23+
self.value = value
24+
25+
def __get__(self, instance, owner):
26+
return self.value
27+
28+
def __set__(self, instance, value):
29+
if not isinstance(value, int):
30+
raise TypeError(f"{value} is not an integer")
31+
self.value = value
32+
33+
def __delete__(self, instance):
34+
del self.value
35+
36+
def __set_name__(self, owner, name):
37+
"""
38+
Note: `type.__new__` calls `__set_name__` on the descriptor
39+
"""
40+
self.name = name
41+
42+
def __str__(self):
43+
return str(self.value)
44+
45+
46+
x = Integer(10)
47+
print(f"x == {x}")
48+
49+
x = 42
50+
print(f"x == {x}")
51+
52+
y = Integer()
53+
x = y
54+
print(f"x == {x}")
55+
56+
# TODO: useful metaclasses, last 10 minutes of the video

10_class/07-hooks.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
__doc__ = """Python hooks are used to implement custom behavior on some events.
2+
It can replace metaclasses in most cases, as just another way of doing the same thing.
3+
4+
__init_subclass__ hook is called when a class is subclassed in the MRO order
5+
6+
As soon as you inherited from a class with __init_subclass__ hook,
7+
it will be called on the subclass automatically
8+
E.g. let's create class to check the coding style of the code to be PEP8 compliant (e.g. `variable_name`)
9+
"""
10+
11+
12+
class CodeStyleChecker:
13+
def __init_subclass__(cls, **kwargs):
14+
print(f"CodeStyleChecker.__init_subclass__({cls}, {kwargs})")
15+
super().__init_subclass__(**kwargs)
16+
17+
# check we use only lower case letters and underscore
18+
for name in dir(cls):
19+
if name.startswith("__"):
20+
continue
21+
if not name.islower():
22+
raise ValueError(f"Invalid name: {name}")
23+
24+
def __init__(self, **kwargs):
25+
print(f"CodeStyleChecker.__init__({self}, {kwargs})")
26+
super().__init__(**kwargs)
27+
28+
29+
class MyClass(CodeStyleChecker):
30+
"""
31+
Defining methods like `def IncorrectName(self)` will raise an exception
32+
"""
33+
def __init__(self, a, b, **kwargs):
34+
super().__init__(**kwargs)
35+
self.a = a
36+
self.b = b
37+
38+
def print_me(self):
39+
print(f"a == {self.a}, b == {self.b}")
40+
41+
def __str__(self):
42+
return f"MyClass(a={self.a}, b={self.b})"
43+
44+
45+
c = MyClass(10, 20)
46+
print(f"c == {c}")

0 commit comments

Comments
 (0)