Skip to content

Commit

Permalink
feat: property addition
Browse files Browse the repository at this point in the history
Signed-off-by: Henry Schreiner <[email protected]>
  • Loading branch information
henryiii committed Sep 29, 2022
1 parent 93c7f26 commit fcc88dc
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 13 deletions.
6 changes: 3 additions & 3 deletions _toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ parts:
numbered: true
chapters:
- file: notebooks/1.1 Inspection
- file: notebooks/1.1b Intro to Classes
- file: notebooks/1.2 Intro to Classes
- file: notebooks/1.3 Logging
- file: notebooks/1.4 Debugging
- file: notebooks/1.5 Profiling
Expand All @@ -32,5 +32,5 @@ parts:
- caption: Advanced
numbered: true
chapters:
- file: notebooks/1.1 Memory Model
- file: notebooks/1.2 Classes
- file: notebooks/4.1 Memory Model
- file: notebooks/4.2 Classes
File renamed without changes.
File renamed without changes.
181 changes: 171 additions & 10 deletions notebooks/1.2 Classes.ipynb → notebooks/4.2 Classes.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
{
"cell_type": "markdown",
"metadata": {
"jp-MarkdownHeadingCollapsed": true,
"tags": []
},
"source": [
Expand Down Expand Up @@ -196,7 +195,6 @@
{
"cell_type": "markdown",
"metadata": {
"jp-MarkdownHeadingCollapsed": true,
"tags": []
},
"source": [
Expand All @@ -207,7 +205,33 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"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. "
"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. There is also a `__delete__`. While it's technically part of class creation, `__set_name__` is also very useful for descriptors.\n",
"\n",
"Why have this when you already have `__getattribute__`, `__setattr__`, and `__delattr__`? Decriptors are defined on member of the class, rather than the class itself!"
]
},
{
"cell_type": "markdown",
"metadata": {
"jp-MarkdownHeadingCollapsed": true,
"tags": []
},
"source": [
"#### Common descriptors"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Descriptors are [at work in several areas](https://www.youtube.com/watch?v=mMbVs17Vmo4) of the core Python sytnax. Ever wondered why `Class.x(instance)` and `instance.x()` were the same? A function object has a `__get__` that can tell if it's running from a class or an instance. Functions inside clases are not actually special, all functions simply implement descriptors! "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If you look at any function, you'll see it has a `__get__`:"
]
},
{
Expand All @@ -216,7 +240,14 @@
"metadata": {},
"outputs": [],
"source": [
"F.__call__"
"f.__get__"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's just make a little descriptor that prints out what it sees when it gets used."
]
},
{
Expand All @@ -225,7 +256,21 @@
"metadata": {},
"outputs": [],
"source": [
"F.__call__(None, 2)"
"class CheckDescriptor:\n",
" def __get__(self, obj, cls):\n",
" print(f\"{obj = }\\n{cls = }\")\n",
" \n",
"class HasDescriptor:\n",
" d = CheckDescriptor()\n",
" \n",
"inst = HasDescriptor()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If we access the attribute from the class, the descriptor recieves `None` for the instance, and the class."
]
},
{
Expand All @@ -234,7 +279,14 @@
"metadata": {},
"outputs": [],
"source": [
"f_inst.__call__"
"HasDescriptor.d"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And if you trigger the descriptor from an instance, you instead get that instance as the first argument:"
]
},
{
Expand All @@ -243,14 +295,21 @@
"metadata": {},
"outputs": [],
"source": [
"f_inst.__call__(2)"
"inst.d"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now you can see how bound methods could be implemented via `__get__` on functions."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We could to this by hand if we wanted to:"
"Let's try to make a little descriptor that returns it's own name, but only when accessed from an instance."
]
},
{
Expand All @@ -259,7 +318,107 @@
"metadata": {},
"outputs": [],
"source": [
"F.__call__.__get__(f_inst)"
"class SelfName:\n",
" def __set_name__(self, owner, name):\n",
" self.name = name\n",
" def __get__(self, obj, cls):\n",
" return self if obj is None else self.name"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class Holder:\n",
" x = SelfName()\n",
" y = SelfName()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"Holder.x"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"Holder().x"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"One common use for getting the name is to make a instance variable that you can store data in."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Descriptors are also how `@classmethod` and `@staticmethod` are implemented. They have different descriptors over a usual function. There are several other descriptors in Python, including the features that make `__slots__`, `__dict__`, and `super()` work. But there's one more major usage of descriptors you are likely to see a lot and even use: `@property`."
]
},
{
"cell_type": "markdown",
"metadata": {
"jp-MarkdownHeadingCollapsed": true,
"tags": []
},
"source": [
"#### Property"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A property is an easy way to make something that looks like a member but actually calls methods when it is accessed, set, or even deleted. Here's how you'd make this:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class Example:\n",
" @property\n",
" def x(self):\n",
" return 42\n",
" @x.setter\n",
" def x(self, value):\n",
" if value != 42:\n",
" raise ValueError(\"Must be 42!\")\n",
" @x.deleter\n",
" def x(self):\n",
" raise ValueError(\"You can't delete 42!\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"ex = Example()\n",
"ex.x"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Feel free to play with this - ex.x can't be deleted or set to anything other than 42! In practice, the functions are optional - most properties don't have a deleter, and many of them don't have a setter. See [the Python docs](https://docs.python.org/3/howto/descriptor.html#properties) for a great overview of descriptors and properties, including how you could implement the decorator yourself using descriptors."
]
},
{
Expand Down Expand Up @@ -414,7 +573,9 @@
},
{
"cell_type": "markdown",
"metadata": {},
"metadata": {
"tags": []
},
"source": [
"### Data + functions"
]
Expand Down

0 comments on commit fcc88dc

Please sign in to comment.