Skip to content

Commit

Permalink
feat: discuss metaclasses a bit
Browse files Browse the repository at this point in the history
  • Loading branch information
henryiii committed Jan 7, 2022
1 parent fcc6728 commit ebc1a18
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 7 deletions.
62 changes: 58 additions & 4 deletions notebooks/1.2 Classes.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,60 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"### The Type\n",
"\n",
"### The Type"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"All objects are \"based\" on a class (AKA type). You can call `type(object)` or `object.__class__` to get the class of an object. Types can \"inherit\" from other types, which just means they build on the other. The type is generally the \"shared\" part for objects, while the object itself holds the \"unique\" part that makes an instance special. Types tend to have methods, and instances tend to have member data, but since everything is an object, and functions really are just callable objects, the distinction between a variable and a function is a bit fuzzy. And that's okay.\n",
"\n",
"Think of it this way: I am an instance of a Person. A Person inherits from a LivingThing. So I am an object, a Person is my class, and a Person inherits from LivingThing. You could have more levels of inheritance, like Mammal, if you want; in \"real life\", we tend to use this a lot, since it helps us structure our thinking. It helps structure our programs, too!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### The metaclass and how to avoid using it"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Metaclasses may sound scary, but it's really just a feature of the Python syntax. When you write `class X`, normally this uses `type` to make the new class for you - `type` is the metaclass. You can customize this by setting a different metaclass (usually subclasses of `type`!). And, most importantly, subclasses inherit metaclasses too, so users are not exposed to metaclasses. This is how `abc.ABC` works - it's really just a simple do-nothing class with `abc.ABCMeta` as the metaclass - and abstract base classes are implemented through a metaclass. The metaclass will check and make sure there are no remaining abstract methods when you are creating an instance of the class.\n",
"\n",
"In modern Python, almost everything you would have done with a metaclass is now possible via special overloads or normal features - classes now always preserve order (thanks to dictionaries becoming ordered), `__init_subclass__` lets you customize what happens when users subclass your class, and `__prepare__` lets you modify the preparation of a new class namespace.\n",
"\n",
"Let's do something evil just to show how it works:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def collect(a, b, c, **kwargs):\n",
" return a, b, c, kwargs\n",
"\n",
"\n",
"class Example(metaclass=collect):\n",
" a = 1\n",
"\n",
"\n",
"Example"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"First, notice that `Example` is _not_ a class - it's not an instance of type, it's an \"instance\" of collect (which is just a function, so it's the function return value). The first argument was the class name (very powerful feature of class definitions - they always have a name!); the second is the list of bases (we didn't inherit from anything, so it's empty), and the third is the new namespace (just a dict). The `__module__` and the `__qualname__` were sef for free for us, otherwise, it's just the things we defined inside the class body. Finally, it can take keyword arguments - Python supports arbitrary keyword arguments when defining a class (except for `metaclass=`, which is taken for setting the metaclass), though that's a very advanced usage. `__prepare__` can handle these keyword arguments normally."
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down Expand Up @@ -115,9 +162,9 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"### Accessors\n",
"### Descriptors\n",
"\n",
"When you find a object, what do you actually return? Python has an accessors system. If the object has a `__get__` method (or `__set__` if you are setting something), then that is called with the instance as the argument and the result is returned. This is how methods work - (all) functions have a `__get__`, so when they are inside a class, they get called with `self` first; they return a \"bound\" method, one that will always include the instance in the call. This is how properties and staticmethod/classmethod work too, they customize `__get__`."
"When you find a object, what do you actually return? Python has an descriptor system that is called when you access a class member. If the object has a `__get__` method (or `__set__` if you are setting something), then that is called with the instance as the argument and the result is returned. This is how methods work - (all) functions have a `__get__`, so when they are inside a class, they get called with `self` first; they return a \"bound\" method, one that will always include the instance in the call. This is how properties and staticmethod/classmethod work too, they customize `__get__`. There is also a `__delete__`. While it's technically part of class creation, `__set_name__` is also very useful for descriptors. "
]
},
{
Expand Down Expand Up @@ -355,6 +402,13 @@
"v.mag()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If you use dataclasses, you even get automatic features from future Python versions, like pattern matching support, for free!"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
115 changes: 114 additions & 1 deletion notebooks/2.2 Generators.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"# Generators (Iterators)\n",
"# Generators and Iterators\n",
"\n",
"Let's change topics just for a moment (we'll get to context managers in a moment, which we are building toward). Let's look into iterators, which are a form of generator. I'm sure you've seen one:"
]
Expand Down Expand Up @@ -64,6 +64,8 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## Defining iterators\n",
"\n",
"You could implement `__iter__` and `__next__` yourself, but Python has a built in syntax shortcut for making iterators (and generators, which have a `__send__` too): "
]
},
Expand All @@ -80,6 +82,13 @@
" yield 3"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
Expand Down Expand Up @@ -172,6 +181,110 @@
"source": [
"But normally, you call them inplace, such as `list(range(4))`, so it is not often missed if they can't be restarted."
]
},
{
"cell_type": "markdown",
"metadata": {
"tags": []
},
"source": [
"## Factoring iterators and generators"
]
},
{
"cell_type": "markdown",
"metadata": {
"tags": []
},
"source": [
"You can also factor out genators, just like you can factor out functions:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def middle_two():\n",
" yield 1\n",
" yield 2\n",
"\n",
"\n",
"def range4_factored():\n",
" yield 0\n",
" yield from middle_two()\n",
" yield 3"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"list(range4_factored())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You might be tempted to place a loop inside the generator with a yield (`for item in middle_two(): yield item`), but `yield from` is simpler and also works correctly with generators."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## General generators\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A generator that only returns values is called an iterator, and that's mostly what you directly see. Generators that are not iterators support two-way communication. You rarely need these, and there really isn't a nice syntax method for sending information to a generator, but here is one, just as an example:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def generator():\n",
" received = yield 1\n",
" print(\"Received\", received)\n",
" received = yield 2\n",
" print(\"Received\", received)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Prepare generator\n",
"active = iter(generator())\n",
"print(\"Running first send\")\n",
"print(f\"{active.send(None) = }\")\n",
"print(\"Running second send\")\n",
"print(f\"{active.send(10) = }\")\n",
"try:\n",
" active.send(20)\n",
"except StopIteration:\n",
" print(\"Done\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`next(active)` is the same thing as `active.send(None)` - the first send must always be `None`, since it hasn't reached the first `=` sign yet. The final send does not need to be None, though I didn't accept anything for the second yield above, so I just used `next` to end it."
]
}
],
"metadata": {
Expand Down
4 changes: 2 additions & 2 deletions notebooks/save_and_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ class AutoMagics(Magics):
@cell_magic
def save_and_run(self, line, cell, local_ns=None):
commands = line.split()
filepath = Path("tmp.py")
filepath.write_text(cell)
filename = "tmp.py"
Path(filename).write_text(cell)

subprocess.run(
[sys.executable, "-m", *commands, filename],
Expand Down

0 comments on commit ebc1a18

Please sign in to comment.