Skip to content

Instantly share code, notes, and snippets.

@theavey
Created July 27, 2019 20:04
Show Gist options
  • Save theavey/ba30c6c1843e2f3d9aeaab400bbd6163 to your computer and use it in GitHub Desktop.
Save theavey/ba30c6c1843e2f3d9aeaab400bbd6163 to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# These are \"class objects\""
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"<class 'str'>\n",
"<class 'type'>\n",
"<class 'pandas.core.frame.DataFrame'>\n",
"<class 'type'>\n"
]
}
],
"source": [
"S = str # capitalized because it's a class\n",
"print(S)\n",
"print(type(S))\n",
"\n",
"DF = pd.DataFrame\n",
"print(DF)\n",
"print(type(DF))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# These are \"instance objects\" of different classes"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"<class 'str'>\n",
"Empty DataFrame\n",
"Columns: []\n",
"Index: []\n",
"<class 'pandas.core.frame.DataFrame'>\n"
]
}
],
"source": [
"s = str()\n",
"print(s) # blank string is just ''\n",
"print(type(s))\n",
"\n",
"df = pd.DataFrame()\n",
"print(df)\n",
"print(type(df))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Test them\n",
"\n",
"For these, we can easily just create instances and test their properties"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"hasattr(df, 'plot')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"and test if it's \"callable\""
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"hasattr(getattr(df, 'plot'), '__call__')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Not always so easy\n",
"But there are classes where instantiating them could be:\n",
"\n",
" 1. hard to know what arguments to instantiate it with generally\n",
" 2. slow or hard to test in a test environment (for example, it requires a database connection or authentication\n",
" 3. not free: maybe it's a class of data that is paid for every time it's requested\n",
" \n",
"Therefore, we want to be able to test the attributes of an uninstantiated class objects"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"One option for doing this would be to use a mixin class (just a parent class that contains a few methods or attributes, but is not a full parent class in the sense that it doesn't define most methods like an `__init__`, for example)\n",
"\n",
"I'll be using `classmethod` for this. If that is unfamiliar, see [the appendix](#appendix)."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"class MixinClassForTesting(object):\n",
" \n",
" # the `cls` is an aribitrary name, could just as easily use `self`\n",
" # but I'll use `cls` because it reminds us it's a class object,\n",
" # not an instance object\n",
" @classmethod\n",
" def _get_methods(cls):\n",
" methods = list()\n",
" for attr_name in dir(cls):\n",
" attr = getattr(cls, attr_name)\n",
" if hasattr(attr, '__call__'):\n",
" methods.append(attr_name)\n",
" return methods\n",
" \n",
" needed_methods = ['method1', 'method2']\n",
" \n",
" @classmethod\n",
" def has_needed_methods(cls):\n",
" methods_of_class = cls._get_methods()\n",
" for needed_method in cls.needed_methods:\n",
" if needed_method not in methods_of_class:\n",
" return False\n",
" return True\n",
"\n",
" \n",
"class GoodClass(MixinClassForTesting):\n",
" \n",
" def __init__(self, *args, **kwargs):\n",
" # let's assume this is either hard, slow, or expensive\n",
" self.args = args\n",
" self.kwargs = kwargs\n",
" \n",
" def method1(self):\n",
" return 'I have one good method'\n",
" \n",
" def method2(self):\n",
" return 'I have another good method'\n",
" \n",
" def method3(self):\n",
" return 'no one cares about this one'\n",
" \n",
" def __repr__(self):\n",
" return '<instance of GoodClass>'\n",
"\n",
" \n",
"class BadClass(MixinClassForTesting):\n",
" \n",
" def __init__(self, *args, **kwargs):\n",
" # let's assume this is either hard, slow, or expensive\n",
" self.args = args\n",
" self.kwargs = kwargs\n",
" \n",
" def method1(self):\n",
" return 'well it have this one'\n",
" \n",
" def method3(self):\n",
" return 'and it has this one, too'\n",
" \n",
" def __repr__(self):\n",
" return '<instance of BadClass>'"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The tests work here. Nice"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"True\n",
"False\n"
]
}
],
"source": [
"print(GoodClass.has_needed_methods())\n",
"print(BadClass.has_needed_methods())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Unfortunately, even if we create an instance of the class, the testing methods and such are still there. \n",
"We only wanted them for testing, so we don't really want them there for the actual instances."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"<instance of GoodClass>\n",
"['method1', 'method2']\n",
"True\n"
]
}
],
"source": [
"good_class = GoodClass()\n",
"print(good_class)\n",
"print(good_class.needed_methods)\n",
"print(good_class.has_needed_methods())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# An alternative approach"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
"class MetaClassForTesting(type):\n",
" \n",
" # I'll still use `cls` here for the same reason, even though\n",
" # this is not a `classmethod`\n",
" def _get_methods(cls):\n",
" methods = list()\n",
" for attr_name in dir(cls):\n",
" attr = getattr(cls, attr_name)\n",
" if hasattr(attr, '__call__'):\n",
" methods.append(attr_name)\n",
" return methods\n",
" \n",
" needed_methods = ['method1', 'method2']\n",
" \n",
" def has_needed_methods(cls):\n",
" methods_of_class = cls._get_methods()\n",
" for needed_method in cls.needed_methods:\n",
" if needed_method not in methods_of_class:\n",
" return False\n",
" return True\n",
"\n",
" \n",
"class GoodClass2(metaclass=MetaClassForTesting):\n",
" \n",
" def __init__(self, *args, **kwargs):\n",
" # let's assume this is either hard, slow, or expensive\n",
" self.args = args\n",
" self.kwargs = kwargs\n",
" \n",
" def method1(self):\n",
" return 'I have one good method'\n",
" \n",
" def method2(self):\n",
" return 'I have another good method'\n",
" \n",
" def method3(self):\n",
" return 'no one cares about this one'\n",
" \n",
" def __repr__(self):\n",
" return '<instance of GoodClass>'\n",
"\n",
" \n",
"class BadClass2(metaclass=MetaClassForTesting):\n",
" \n",
" def __init__(self, *args, **kwargs):\n",
" # let's assume this is either hard, slow, or expensive\n",
" self.args = args\n",
" self.kwargs = kwargs\n",
" \n",
" def method1(self):\n",
" return 'well it have this one'\n",
" \n",
" def method3(self):\n",
" return 'and it has this one, too'\n",
" \n",
" def __repr__(self):\n",
" return '<instance of BadClass>'"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The tests work here. Nice"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"True\n",
"False\n"
]
}
],
"source": [
"print(GoodClass2.has_needed_methods())\n",
"print(BadClass2.has_needed_methods())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"But now with this implementation, all of this testing framework is no longer present for an instances of the class"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"<instance of GoodClass>\n"
]
},
{
"ename": "AttributeError",
"evalue": "'GoodClass2' object has no attribute 'needed_methods'",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m<ipython-input-11-adb7456a9de2>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0mgood_class\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mGoodClass2\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgood_class\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgood_class\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mneeded_methods\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;31mAttributeError\u001b[0m: 'GoodClass2' object has no attribute 'needed_methods'"
]
}
],
"source": [
"good_class = GoodClass2()\n",
"print(good_class)\n",
"print(good_class.needed_methods)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---------\n",
"<a id='appendix'></a>\n",
"# Appendix: `classmethod`s and `staticmethod`s\n",
"\n",
"`classmethod` and `staticmethod` are both functions/decorators that somewhat change how methods of a class behave.\n",
"\n",
"`staticmethod` is mostly used for a function that is defined in a class just for convenience/namespacing: \n",
"* they will often be used for or in an instance of the class. \n",
"* What makes them different is that they cannot access any of the attributes of the instance. \n",
"* Because of that, they can be used without creating an instance of the class.\n",
"\n",
"`classmethod` is mostly used for creating a particular instance of a class. \n",
"* By convention, they generally return an instance of the class, but I don't know of anything that enforces this. \n",
"* These can also be used without creating an instance of the class.\n",
"* One simple example of this is `datetime.date.today()`. This returns an instance of `date` for today's date."
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"class Pizza:\n",
" \n",
" def __init__(self, *ingredients):\n",
" self.ingredients = ingredients\n",
" self.amount = 1.0\n",
" \n",
" # normal methods of the class\n",
" def eat_half(self):\n",
" self.amount *= 0.5\n",
" \n",
" # a staticmethod: related to this, but doesn't require a \n",
" # pizza to exist to be used\n",
" @staticmethod\n",
" def order_wine(variety='red'):\n",
" # Note, `self` is not given here; it has no real idea that it's\n",
" # a method of a class instead of just a random function\n",
" return f'open drizly on your phone and search for \"{variety} wine\"; hit \"buy\"'\n",
" \n",
" # a classmethod: instead of `self`, the first argument given to it\n",
" # is the class, not an instance of the class\n",
" @classmethod\n",
" def pepperoni_pizza(cls, *other_ingredients):\n",
" # `cls` is the class itself, not an instance of the class\n",
" print(f'has an \"amount\": {hasattr(cls, \"amount\")}')\n",
" # Pizza.amount is only created when it's instantiated, so it can't\n",
" # be used here\n",
" ingredients = ['red sauce', 'mozzarella', 'pepperoni'] + list(other_ingredients)\n",
" return cls(*ingredients)\n",
" \n",
" def __repr__(self):\n",
" s = f'<Pizza instance with {len(self.ingredients)} ingredients and {self.amount:%} left>'\n",
" return s"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"<class '__main__.Pizza'>\n",
"<class 'type'>\n",
"open drizly on your phone and search for \"white wine\"; hit \"buy\"\n",
"has an \"amount\": False\n",
"<Pizza instance with 3 ingredients and 100.000000% left>\n"
]
}
],
"source": [
"P = Pizza\n",
"print(P)\n",
"print(type(P))\n",
"print(P.order_wine('white'))\n",
"print(P.pepperoni_pizza())"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"ename": "TypeError",
"evalue": "eat_half() missing 1 required positional argument: 'self'",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m<ipython-input-14-6b290e2b0679>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# this won't work:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mP\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0meat_half\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;31mTypeError\u001b[0m: eat_half() missing 1 required positional argument: 'self'"
]
}
],
"source": [
"# this won't work:\n",
"P.eat_half()"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"<Pizza instance with 4 ingredients and 100.000000% left>\n",
"<class '__main__.Pizza'>\n",
"<Pizza instance with 4 ingredients and 50.000000% left>\n"
]
}
],
"source": [
"p = Pizza('olive oil', 'mozzarella', 'basil', 'red pepper')\n",
"print(p)\n",
"print(type(p))\n",
"# now works\n",
"p.eat_half()\n",
"print(p)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"But these still work as well\n",
"\n",
"Note, `pepperoni_pizza` returns a totally new Pizza with fewer ingredients and none of it eaten."
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"open drizly on your phone and search for \"white wine\"; hit \"buy\"\n",
"has an \"amount\": False\n",
"<Pizza instance with 3 ingredients and 100.000000% left>\n"
]
}
],
"source": [
"print(p.order_wine('white'))\n",
"print(p.pepperoni_pizza())"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
@theavey
Copy link
Author

theavey commented Jul 29, 2019

clarify: what's not always so easy?
what is a metaclass? how does it get called?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment