From f13b98e6d56d8eac1225c198bac8c9e713ebeea7 Mon Sep 17 00:00:00 2001 From: Yonatan Goldschmidt Date: Tue, 3 Nov 2020 01:55:49 +0200 Subject: [PATCH 1/2] Add section on methods equality and identity Closes: #233 --- CONTRIBUTORS.md | 2 +- README.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 9dd1d2b..659a382 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -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) | --- diff --git a/README.md b/README.md index ea95470..f2a8691 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ 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) @@ -1122,6 +1123,84 @@ The Subclass relationships were expected to be transitive, right? (i.e., if `A` --- +### ▶ Methods equality and identity + + +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). +* Accessing the attribute multiple times creates multiple method objects! Therefore `o1.method is o2.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. +* `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. +* 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. +* 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. +* Having to create new "method" objects every time Python calls instance methods 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 * From 314de9a30ba31779a1faf0fd79c9030fefdb87d1 Mon Sep 17 00:00:00 2001 From: Yonatan Goldschmidt Date: Sun, 8 Nov 2020 01:41:59 +0200 Subject: [PATCH 2/2] Merge "Non-reflexive class method" into new example --- README.md | 71 ++++++++++++++++++++----------------------------------- 1 file changed, 25 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index f2a8691..7137d07 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,6 @@ So, here we go... + [▶ 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-) @@ -1185,18 +1184,41 @@ Accessing` classm` or `method` twice, creates equal but not *same* objects for t 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). -* Accessing the attribute multiple times creates multiple method objects! Therefore `o1.method is o2.method` is +```py +>>> o1.method +> +``` +* 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 + +``` * `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 +> +``` * 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 +> +``` * 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. -* Having to create new "method" objects every time Python calls instance methods affected performance badly. +```py +>>> o1.staticm + +>>> SomeClass.staticm + +``` +* 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 :) @@ -1530,49 +1552,6 @@ True --- -### ▶ Non-reflexive class method * - - - -```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 - - >>> SomeClass.class_method - - ``` - - 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 ```py