{ "info": { "author": "Rick Harris", "author_email": "rconradharris@gmail.com", "bugtrack_url": null, "classifiers": [ "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules" ], "description": "=======\nomitted\n=======\n\nA Do-Not-Care Value for Python\n\nLet's Talk About Kwargs\n=======================\n\nIt's easy to take for granted, but one of the most elegant aspects of Python\nis its handling of functional keyword arguments (kwargs). This one construct\nprovides:\n\n 1. A way to specify parameters without having to know its position in the\n argument list\n\n 2. Automatic checking of expected parameters\n\n 3. An elegant way to specify parameter defaults\n\n 4. A meaningful, self-documenting function signature\n\nMost of the time kwargs work flawlessly; but there are a few cases where\ntheir use becomes tricky.\n\nI'm going to talk about that problem, show one common anti-pattern that\nattempts to fix it, and then propose an alternate approach that retains all of\nthe benefits mentioned above at the cost of introducing a new concept to\nPython, the ``Omitted`` singleton.\n\nProblem, What Problem?\n======================\n\nLet's start off with an example. Suppose you have a function which takes a\nperson and allows you to update the person's name, age, both, or neither.\nNaively, you might write that function as something like::\n\n def update(person, name=None, age=None):\n person.name = name\n person.age = age\n\nNow let's call this function, but only update the person's age::\n\n > person = Person(name='Alice', age=32)\n > update(person, age=33)\n > person.age\n 33\n > person.name\n None\n\nNotice that the code correctly set the age but, unexpectedly from the caller's\npoint-of-view, also set the name to ``None``. Not what we intended.\n\nThe problem here is that ``None`` can't be used to both represent a valid value\nand at the same time we used to represent a do-not-care condition.\n\nRealizing we need a way to differentiate these two conditions, we modify the\ncode slightly::\n\n def update(person, **kwargs):\n for k, v in kwargs.items():\n setattr(person, k, v)\n\nHere we're using the presence or absence of the key in the kwargs dict to\nrepresent the do-not-care condition and the value of ``None`` is solely reserved\nfor actually setting the attribute to ``None`` (pretty reasonable, huh?).\n\nLet's call this new version of ``update`` again::\n\n > person = Person(name='Alice', age=32)\n > update(person, age=33)\n > person.age\n 33\n > person.name\n 'Alice'\n\nThe method behaved exactly as we expected. And from my experience, many\ndevelopers stop here because It Just Works. But it comes at a cost: we no\nlonger have expected kwarg checking (benefit #2), no elegant way of setting\ndefault parameters (benefit #3), and we've lost our meaningful function\nsignature (benefit #4). For these reasons, I consider this an anti-pattern and\npropose a different approach.\n\nNone More None\n==============\n\nTo retain the benefits of explicit kwargs, we need a way for the *value* of\nthe keyword to differentiate between the do-not-care condition and a valid\nvalue. As we've seen above, ``None`` isn't good enough since ``None`` is a\nperfectly reasonable value for many attributes.\n\nInstead we need to create a new value, a singleton like ``None`` that can\nrepresent this case which I call ``Omitted``. With it, our function becomes::\n\n Omitted = object()\n\n def update(person, name=Omitted, age=Omitted):\n if name is not Omitted:\n person.name = name\n\n if age is not Omitted:\n person.age = age\n\n\nWe end up with code that behaves exactly as we'd expect when called, but also\npreserves the benefits of explicit kwargs::\n\n update(person, age=33) # Just set the age to 33\n update(person, name=None) # Set the name to None\n update(person) # Doesn't update any attributes\n update(person, foo=bar) # Raises a TypeError because foo isn't an\n # expected kwargs (benefit #2)\n\n\nSo What's The Catch?\n====================\n\nSo, solving our default kwarg problem, we've gone ahead and published our\nlibrary containing our ``update`` function. Now suppose that someone comes along\nand wants to use our library. First of all, they'll be appreciative of our\nusage of explicit kwargs--once they get past the unusual looking ``Omitted``\ndefaults.\n\nHowever, when they go to use it, they might do something like::\n\n Omitted = object()\n\n def update_with_email(person, name=Omitted, age=Omitted):\n update(person, name=name, age=age)\n send_email(person)\n\n > person = Person(name='Alice', age=32)\n > update_with_email(age=33)\n > person.age\n 33\n > person.name\n \n\n\nAs you can see, ``person.name`` has ended up with the value of ``Omitted``. The\nproblem here is that caller's ``Omitted`` instance is different from the\nlibraries ``Omitted`` instance, so they don't compare as identical.\n\nWhat we'd like is a way to define a single global singleton that represents\nthis do-not-care condition across all Python packages, in the same way that\n``None`` is identical no matter where it's used.\n\n\nIntroducing...\n==============\n\nThis Python module aims to solve this problem by defining the one-and-only\n``Omitted`` instance, shareable between all packages on the system.\n\nTo be clear, just because a library uses ``Omitted``, it doesn't mean the\ncalling code must as well. Not passing the kwarg or setting it to ``None`` will\nbehave correctly without having to know that ``Omitted`` was used behind the\nscenes to make it work.\n\nThe only time a caller would need to import ``Omitted`` is if they wanted to\nproxy the do-not-care condition from the caller into the library. In that\ncase, you'd just do something like::\n\n from libperson import update\n from omitted import Omitted\n\n def update_with_email(person, name=Omitted, age=Omitted):\n update(person, name=name, age=age)\n send_email(person)\n\nWith that in mind, go ahead, import ``Omitted`` and let your code stop caring.\n", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/rconradharris/omitted", "keywords": null, "license": "MIT", "maintainer": null, "maintainer_email": null, "name": "omitted", "package_url": "https://pypi.org/project/omitted/", "platform": "any", "project_url": "https://pypi.org/project/omitted/", "project_urls": { "Download": "UNKNOWN", "Homepage": "https://github.com/rconradharris/omitted" }, "release_url": "https://pypi.org/project/omitted/0.1.3/", "requires_dist": null, "requires_python": null, "summary": "A Do-Not-Care Value for Python", "version": "0.1.3" }, "last_serial": 795706, "releases": { "0.1.2": [ { "comment_text": "", "digests": { "md5": "35c167e057147147e68ce059a9696650", "sha256": "d8be76afc03bd2c6d151298190976127db46d31eba4ce2434fec5d06ca37ec56" }, "downloads": -1, "filename": "omitted-0.1.2.tar.gz", "has_sig": false, "md5_digest": "35c167e057147147e68ce059a9696650", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 3738, "upload_time": "2012-11-11T03:04:30", "url": "https://files.pythonhosted.org/packages/a9/cd/7e02687bc4f70f81f1af1b4dd25ab5ef0111f6399414409080b68a678f47/omitted-0.1.2.tar.gz" } ], "0.1.3": [ { "comment_text": "", "digests": { "md5": "9cb4d088546620643839249ddb53d447", "sha256": "a7369279eedee31f3aad90c30fb78eed86a4e2608794a6bf00502d6db67cf0d7" }, "downloads": -1, "filename": "omitted-0.1.3.tar.gz", "has_sig": false, "md5_digest": "9cb4d088546620643839249ddb53d447", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 3793, "upload_time": "2012-12-27T22:43:40", "url": "https://files.pythonhosted.org/packages/b6/d7/2b8b450e4cb18b3c39b9c630b7969f644122ce016a668573ab08fdb086c6/omitted-0.1.3.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "9cb4d088546620643839249ddb53d447", "sha256": "a7369279eedee31f3aad90c30fb78eed86a4e2608794a6bf00502d6db67cf0d7" }, "downloads": -1, "filename": "omitted-0.1.3.tar.gz", "has_sig": false, "md5_digest": "9cb4d088546620643839249ddb53d447", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 3793, "upload_time": "2012-12-27T22:43:40", "url": "https://files.pythonhosted.org/packages/b6/d7/2b8b450e4cb18b3c39b9c630b7969f644122ce016a668573ab08fdb086c6/omitted-0.1.3.tar.gz" } ] }