
{"id":4987,"date":"2024-07-13T21:28:11","date_gmt":"2024-07-14T01:28:11","guid":{"rendered":"https:\/\/ikriv.com\/blog\/?p=4987"},"modified":"2024-07-13T22:09:11","modified_gmt":"2024-07-14T02:09:11","slug":"python-dynamic-methods-and-object-copying-a-tale-of-one-bug","status":"publish","type":"post","link":"https:\/\/ikriv.com\/blog\/?p=4987","title":{"rendered":"Python: dynamic methods and object copying, a tale of one bug"},"content":{"rendered":"<h1>Executive Summary<\/h1>\n<p>Avoid using callable instance attributes that call back into the object that contains them. If you must, make the object uncopyable by overriding <code>__copy__<\/code> and <code>__deepcopy__<\/code> and raising an error.<\/p>\n<p>Reason: in Python,<code>foo.method<\/code> is permanently bound to <code>foo<\/code>. If <code>bar<\/code> is another instance of the same class, and we do <code>bar.dynamic_method = foo.method<\/code>, <code>bar.dynamic_method()<\/code> operates on <code>foo<\/code>, not on <code>bar<\/code>. This may cause interesting bugs.<\/p>\n<h1>Simple Example<\/h1>\n<p>Suppose we have a class that prints introductions in different languages. We also add functionality to introduce in default language, as follows:<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\r\nclass Foo:\r\n    def __init__(self, name, default_language):\r\n        self.name = name\r\n        self.introduce = (self.introduce_in_english \r\n            if default_language == &quot;English&quot; \r\n            else self.introduce_in_french)\r\n\r\n    def introduce_in_english(self):\r\n        print(f&quot;I am {self.name}&quot;)\r\n\r\n    def introduce_in_french(self):\r\n        print(f&quot;Je m&#039;apelle {self.name}&quot;)\r\n        \r\nfoo = Foo(&quot;FOO&quot;, &quot;English&quot;)\r\nfoo.introduce()               # I am FOO\r\nfoo.introduce_in_french()     # Je m&#039;apelle FOO\r\n\r\nbar = Foo(&quot;BAR&quot;, &quot;French&quot;)\r\nbar.introduce = foo.introduce # (BAD IDEA)\r\nfoo.introduce_in_english()    # I am BAR\r\nbar.introduce_in_french()     # Je m&#039;apelle BAR\r\nbar.introduce()               # I am FOO &lt;--- BUG!\r\n<\/pre>\n<p>If we use <code>copy()<\/code>, the copying happens implicitly, without the user realizing the averse effects. Given the same definition of `Foo` as above:<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\r\nfrom copy import copy\r\nfoo = Foo(&quot;FOO&quot;, &quot;English&quot;)\r\nbar = copy(foo)\r\nbar.name = &quot;BAR&quot;\r\nbar.introduce()               # I am FOO &lt;--- BUG!\r\n<\/pre>\n<p><b>Fun fact<\/b>: if we use <code>deepcopy()<\/code> instead of <code>copy()<\/code>, the bug disappears. The explanation is rather subtle, we address it in the next post.<\/p>\n<h2>Solution for the simple example<\/h2>\n<p>The solution is to avoid having callable attributes that are bound to the instance that contains them. Other callables are fine. This may lead to somewhat longer code, but it is free of the copying bug. For example, we can do:<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\r\ndef introduce_in_english(name):\r\n    print(f&quot;I am {name}&quot;)\r\n    \r\ndef introduce_in_french(name):\r\n    print(f&quot;Je m&#039;apelle {name}&quot;)\r\n\r\nclass Foo:\r\n    def __init__(self, name, default_language):\r\n        self.name = name\r\n        self.greeter = (introduce_in_english \r\n            if default_language == &quot;English&quot; \r\n            else introduce_in_french)\r\n        \r\n    def introduce(self):\r\n        self.greeter(self.name)\r\n\r\n    def introduce_in_english(self):\r\n        introduce_in_english(self.name)  # call global function\r\n \r\n    def introduce_in_french(self):\r\n        introduce_in_french(self.name)   # call global function\r\n<\/pre>\n<p>We replace dynamic methods with dynamic free-standing functions, and it avoids the bug. There are lots of other ways to accomplish the same thing, but the key point is to avoid storing a method, or another callable that is bound to the object itself, in an object attribute.<\/p>\n<h1>Real Life Example: Decorators<\/h1>\n<p>The actual bug we encountered in production was about applying decorators to a method. I can&#8217;t copy real production code for intellectual property protection reasons, but conceptually it went something like this:<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\r\nfrom copy import deepcopy\r\n \r\nclass RangeValidator:\r\n    def __init__(self, max_value, decorator=None):\r\n        self.max_value = max_value\r\n        if decorator:\r\n            self.validate = decorator(self.validate)\r\n             \r\n    def set_max_value(self, value):\r\n        self.max_value = value\r\n        return self\r\n             \r\n    def validate(self, number):\r\n        return number &lt; self.max_value\r\n         \r\ndef negate(validator):\r\n    def inner(number):\r\n        return not validator(number)\r\n    return inner\r\n    \r\ndef double(validator):\r\n    return deepcopy(validator).set_max_value(2*validator.max_value)\r\n     \r\nat_least_100 = RangeValidator(100, negate)              # Rejects anything that is under 100\r\nat_least_200 = double(at_least_100)                     # Supposed to reject anything under 200, but doesn&#039;t\r\nprint(at_least_100.validate(150))                       # True\r\nprint(at_least_200.validate(150))                       # Also True &lt;-- BUG\r\nprint(at_least_200.max_value)                           # 200\r\n<\/pre>\n<p>The source of the bug is similar to the simple case, with two differences:<\/p>\n<ul>\n<li>We create an instance attribute <code>validate<\/code> that hides the original method.<\/li>\n<li>The attribute is bound to the containing object indirectly. It references function <code>inner()<\/code>, that captures the original <code>validate()<\/code> method bound to <code>at_least_100<\/code>.<\/li>\n<\/ul>\n<p>It all works well until we copy this attribute to another instance, e.g. <code>at_least_200<\/code>.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-5007\" src=\"https:\/\/ikriv.com\/blog\/wp-content\/uploads\/2024\/07\/Python-Circular-Reference.png\" alt=\"\" width=\"946\" height=\"298\" \/><\/p>\n<p>Note that in this case we use <code>deepcopy()<\/code>, but it does not help: the bug is still there. See the next post to know why.<\/p>\n<h2>Solution 1: Use Free Function for Validation, Reapply the Decorator on Changes<\/h2>\n<p>Again, multiple solutions are possible, but the main point is to avoid callables stored in attribute that point to the containing instance, directly or indirectly.<\/p>\n<p>We can store the decorator and recreate the validator when max value changes. Usage of the class remains the same:<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\r\nfrom functools import partial\r\n\r\ndef validate_range(number, max_value):\r\n    return number &lt; max_value\r\n\r\nclass RangeValidator:\r\n    def __init__(self, max_value, decorator=None):\r\n        self.decorator = decorator\r\n        self.set_max_value(max_value)\r\n             \r\n    def set_max_value(self, value):\r\n        self.max_value = value\r\n        # one-argument function that does not reference self\r\n        self.validator = partial(validate_range, max_value=value)  \r\n        if self.decorator:\r\n            # apply decorator to the function\r\n            self.validator = self.decorator(self.validator) \r\n        return self\r\n        \r\n    def validate(self, number):\r\n        return self.validator(number)\r\n<\/pre>\n<h2>Solution 2: Decorator Class<\/h2>\n<p>We can go full object-oriented and build a real decorator pattern. The drawback of this solution is that the decorator must expose the properties of the inner validator to make things work.<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\r\nfrom copy import deepcopy\r\n\r\nclass RangeValidator:\r\n    def __init__(self, max_value):\r\n        self.max_value = max_value\r\n             \r\n    def validate(self, number):\r\n        return number &lt; self.max_value\r\n\r\n\r\nclass Negate:\r\n    def __init__(self, validator):\r\n        self.validator = validator\r\n        \r\n    def validate(self, number):\r\n        return not self.validator.validate(number)\r\n\r\n    @property\r\n    def max_value(self):\r\n        return self.validator.max_value\r\n        \r\n    @max_value.setter\r\n    def max_value(self, value):\r\n        self.validator.max_value = value\r\n        \r\n\r\ndef double(validator):\r\n    copy = deepcopy(validator)\r\n    copy.max_value = 2*validator.max_value\r\n    return copy\r\n     \r\nat_least_100 = Negate(RangeValidator(100))\r\nat_least_200 = double(at_least_100)\r\nprint(at_least_100.validate(150))              # True\r\nprint(at_least_200.validate(150))              # False &lt;-- THE BUG IS GONE\r\nprint(at_least_200.max_value)                  # 200\r\n<\/pre>\n<h2>Solution 3: Functions all the way down<\/h2>\n<p>Keep in mind that the problem arises when we copy an object that has self-referencing callables as attributes. Functions in Python can have attributes, and they cannot be copied, so if we use functions instead of classes, it will work great. We still have self-referencing attributes, but copy is no longer a problem. To be exact, one can call <code>copy()<\/code> or <code>deepcopy()<\/code> on a function, but it just returns the original object.<\/p>\n<p>The resulting code, however, looks like a Javascript wannabe, and may be difficult to read for traditional Python programmers.<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\r\ndef RangeValidator(max_value, decorator=None):\r\n    def this():\r\n        this.max_value = max_value\r\n        validate = lambda number: number &lt; this.max_value # don&#039;t use just max_value here!\r\n        this.validate = decorator(validate) if decorator else validate\r\n        this.copy = lambda: RangeValidator(this.max_value, decorator)\r\n    this()  # execute &quot;constructor&quot;\r\n    return this\r\n\r\ndef negate(validator):\r\n    return lambda number: not validator(number)\r\n\r\ndef double(validator):\r\n    copy = validator.copy()\r\n    copy.max_value = 2*validator.max_value\r\n    return copy\r\n\r\nat_least_100 = RangeValidator(100, negate)\r\nat_least_200 = double(at_least_100)\r\nprint(at_least_100.validate(50))                    # False\r\nprint(at_least_100.validate(150))                   # True\r\nprint(at_least_100.validate(250))                   # True\r\nprint(at_least_200.validate(50))                    # False\r\nprint(at_least_200.validate(150))                   # False\r\nprint(at_least_200.validate(250))                   # True\r\n<\/pre>\n<h1>Conclusion<\/h1>\n<p>Attributes that implicitly reference the containing object don&#8217;t work well when the object is mutable and copying is allowed. This may lead to very subtle bugs and is better avoided, even if the result is more verbose code.<\/p>\n<p>See the next post for some exciting details on how dynamic methods and (deep)copying work under the hood, and fascinating interactions between them.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Executive Summary Avoid using callable instance attributes that call back into the object that contains them. If you must, make the object uncopyable by overriding __copy__ and __deepcopy__ and raising <a href=\"https:\/\/ikriv.com\/blog\/?p=4987\" class=\"more-link\">[&hellip;]<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"Layout":"","footnotes":""},"categories":[4],"tags":[],"class_list":["entry","author-ikriv","post-4987","post","type-post","status-publish","format-standard","category-hack"],"_links":{"self":[{"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/4987","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=4987"}],"version-history":[{"count":45,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/4987\/revisions"}],"predecessor-version":[{"id":5036,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/4987\/revisions\/5036"}],"wp:attachment":[{"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=4987"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=4987"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=4987"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}