Skip to content

Commit

Permalink
Merge pull request #241 from Jongy/methods
Browse files Browse the repository at this point in the history
Add section on methods equality and identity
  • Loading branch information
satwikkansal authored Nov 8, 2020
2 parents 542c602 + 4993c4b commit 3ffa87f
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 45 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Following are the wonderful people (in no specific order) who have contributed t
| Ghost account | N/A | [#96](https://github.com/satwikkansal/wtfpython/issues/96)
| koddo | [koddo](https://github.com/koddo) | [#80](https://github.com/satwikkansal/wtfpython/issues/80), [#73](https://github.com/satwikkansal/wtfpython/issues/73) |
| jab | [jab](https://github.com/jab) | [#77](https://github.com/satwikkansal/wtfpython/issues/77) |
| Jongy | [Jongy](https://github.com/Jongy) | [#208](https://github.com/satwikkansal/wtfpython/issues/208), [#210](https://github.com/satwikkansal/wtfpython/issues/210) |
| Jongy | [Jongy](https://github.com/Jongy) | [#208](https://github.com/satwikkansal/wtfpython/issues/208), [#210](https://github.com/satwikkansal/wtfpython/issues/210), [#233](https://github.com/satwikkansal/wtfpython/issues/233) |
| Diptangsu Goswami | [diptangsu](https://github.com/diptangsu) | [#193](https://github.com/satwikkansal/wtfpython/issues/193) |

---
Expand Down
146 changes: 102 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ So, here we go...
+ [▶ The sticky output function](#-the-sticky-output-function)
+ [▶ The chicken-egg problem *](#-the-chicken-egg-problem-)
+ [▶ Subclass relationships](#-subclass-relationships)
+ [▶ Methods equality and identity](#-methods-equality-and-identity)
+ [▶ All-true-ation *](#-all-true-ation-)
+ [▶ The surprising comma](#-the-surprising-comma)
+ [▶ Strings and the backslashes](#-strings-and-the-backslashes)
+ [▶ not knot!](#-not-knot)
+ [▶ Half triple-quoted strings](#-half-triple-quoted-strings)
+ [▶ What's wrong with booleans?](#-whats-wrong-with-booleans)
+ [▶ Class attributes and instance attributes](#-class-attributes-and-instance-attributes)
+ [▶ Non-reflexive class method *](#-non-reflexive-class-method-)
+ [▶ yielding None](#-yielding-none)
+ [▶ Yielding from... return! *](#-yielding-from-return-)
+ [▶ Nan-reflexivity *](#-nan-reflexivity-)
Expand Down Expand Up @@ -1122,6 +1122,107 @@ The Subclass relationships were expected to be transitive, right? (i.e., if `A`

---

### ▶ Methods equality and identity
<!-- Example ID: 94802911-48fe-4242-defa-728ae893fa32 --->

1.
```py
class SomeClass:
def method(self):
pass

@classmethod
def classm(cls):
pass

@staticmethod
def staticm():
pass
```

**Output:**
```py
>>> print(SomeClass.method is SomeClass.method)
True
>>> print(SomeClass.classm is SomeClass.classm)
False
>>> print(SomeClass.classm == SomeClass.classm)
True
>>> print(SomeClass.staticm is SomeClass.staticm)
True
```

Accessing `classm` twice, we get an equal object, but not the *same* one? Let's see what happens
with instances of `SomeClass`:

2.
```py
o1 = SomeClass()
o2 = SomeClass()
```

**Output:**
```py
>>> print(o1.method == o2.method)
False
>>> print(o1.method == o1.method)
True
>>> print(o1.method is o1.method)
False
>>> print(o1.classm is o1.classm)
False
>>> print(o1.classm == o1.classm == o2.classm == SomeClass.classm)
True
>>> print(o1.staticm is o1.staticm is o2.staticm is SomeClass.staticm)
True
```

Accessing` classm` or `method` twice, creates equal but not *same* objects for the same instance of `SomeClass`.

#### 💡 Explanation
* Functions are [descriptors](https://docs.python.org/3/howto/descriptor.html). Whenever a function is accessed as an
attribute, the descriptor is invoked, creating a method object which "binds" the function with the object owning the
attribute. If called, the method calls the function, implicitly passing the bound object as the first argument
(this is how we get `self` as the first argument, despite not passing it explicitly).
```py
>>> o1.method
<bound method SomeClass.method of <__main__.SomeClass object at ...>>
```
* Accessing the attribute multiple times creates a method object every time! Therefore `o1.method is o1.method` is
never truthy. Accessing functions as class attributes (as opposed to instance) does not create methods, however; so
`SomeClass.method is SomeClass.method` is truthy.
```py
>>> SomeClass.method
<function SomeClass.method at ...>
```
* `classmethod` transforms functions into class methods. Class methods are descriptors that, when accessed, create
a method object which binds the *class* (type) of the object, instead of the object itself.
```py
>>> o1.classm
<bound method SomeClass.classm of <class '__main__.SomeClass'>>
```
* Unlike functions, `classmethod`s will create a method also when accessed as class attributes (in which case they
bind the class, not to the type of it). So `SomeClass.classm is SomeClass.classm` is falsy.
```py
>>> SomeClass.classm
<bound method SomeClass.classm of <class '__main__.SomeClass'>>
```
* A method object compares equal when both the functions are equal, and the bound objects are the same. So
`o1.method == o1.method` is truthy, although not the same object in memory.
* `staticmethod` transforms functions into a "no-op" descriptor, which returns the function as-is. No method
objects are ever created, so comparison with `is` is truthy.
```py
>>> o1.staticm
<function SomeClass.staticm at ...>
>>> SomeClass.staticm
<function SomeClass.staticm at ...>
```
* Having to create new "method" objects every time Python calls instance methods and having to modify the arguments
every time in order to insert `self` affected performance badly.
CPython 3.7 [solved it](https://bugs.python.org/issue26110) by introducing new opcodes that deal with calling methods
without creating the temporary method objects. This is used only when the accessed function is actually called, so the
snippets here are not affected, and still generate methods :)

### ▶ All-true-ation *

<!-- Example ID: dfe6d845-e452-48fe-a2da-0ed3869a8042 -->
Expand Down Expand Up @@ -1451,49 +1552,6 @@ True

---

### ▶ Non-reflexive class method *

<!-- Example ID: 3649771a-f733-413c-8060-3f9f167b83fd -->

```py
class SomeClass:
def instance_method(self):
pass

@classmethod
def class_method(cls):
pass
```

**Output:**

```py
>>> SomeClass.instance_method is SomeClass.instance_method
True
>>> SomeClass.class_method is SomeClass.class_method
False
>>> id(SomeClass.class_method) == id(SomeClass.class_method)
True
```

#### 💡 Explanation:

- The reason `SomeClass.class_method is SomeClass.class_method` is `False` is due to the `@classmethod` decorator.

```py
>>> SomeClass.instance_method
<function __main__.SomeClass.instance_method(self)>
>>> SomeClass.class_method
<bound method SomeClass.class_method of <class '__main__.SomeClass'>
```

A new bound method every time `SomeClass.class_method` is accessed.

- `id(SomeClass.class_method) == id(SomeClass.class_method)` returned `True` because the second allocation of memory for `class_method` happened at the same location of first deallocation (See Deep Down, we're all the same example for more detailed explanation).

---


### ▶ yielding None
<!-- Example ID: 5a40c241-2c30-40d0-8ba9-cf7e097b3b53 --->
```py
Expand Down

0 comments on commit 3ffa87f

Please sign in to comment.