├── .gitignore ├── Extract Class.ipynb ├── Extract Closure.ipynb ├── Extract Variable.ipynb ├── LICENSE ├── README.md ├── requirements.txt └── slides_screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | lib 3 | pyvenv.cfg 4 | pip-selfcheck.json 5 | share 6 | include 7 | -------------------------------------------------------------------------------- /Extract Class.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Example 2: Extract Class, Move Field, Move Method\n", 8 | "\n", 9 | "[Extract class in the refactoring catalog.](http://refactoring.com/catalog/extractClass.html)\n", 10 | "\n", 11 | "[Move field in the refactoring catalog](http://refactoring.com/catalog/moveField.html).\n", 12 | "\n", 13 | "[Move method in the refactoring catalog](http://refactoring.com/catalog/moveMethod.html)." 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "---\n", 21 | "\n", 22 | "Say you're representing a family pet that has a name." 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 1, 28 | "metadata": { 29 | "collapsed": true 30 | }, 31 | "outputs": [], 32 | "source": [ 33 | "class Pet:\n", 34 | " def __init__(self, name):\n", 35 | " self.name = name" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 2, 41 | "metadata": { 42 | "collapsed": false 43 | }, 44 | "outputs": [ 45 | { 46 | "name": "stdout", 47 | "output_type": "stream", 48 | "text": [ 49 | "Gregory the Gila Monster\n" 50 | ] 51 | } 52 | ], 53 | "source": [ 54 | "my_pet = Pet('Gregory the Gila Monster')\n", 55 | "print(my_pet.name)" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "metadata": {}, 61 | "source": [ 62 | "---\n", 63 | "\n", 64 | "Over time this class may get more complex, like adding the pet's age." 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 3, 70 | "metadata": { 71 | "collapsed": true 72 | }, 73 | "outputs": [], 74 | "source": [ 75 | "class Pet:\n", 76 | " def __init__(self, name, age):\n", 77 | " self.name = name\n", 78 | " self.age = age" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 4, 84 | "metadata": { 85 | "collapsed": false 86 | }, 87 | "outputs": [ 88 | { 89 | "name": "stdout", 90 | "output_type": "stream", 91 | "text": [ 92 | "Gregory the Gila Monster is 3 years old\n" 93 | ] 94 | } 95 | ], 96 | "source": [ 97 | "my_pet = Pet('Gregory the Gila Monster', 3)\n", 98 | "print('%s is %d years old' % (my_pet.name, my_pet.age))" 99 | ] 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "metadata": {}, 104 | "source": [ 105 | "---\n", 106 | "\n", 107 | "You may also add an action method for feeding the pet, and keeping track of how much you've fed it." 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": 5, 113 | "metadata": { 114 | "collapsed": true 115 | }, 116 | "outputs": [], 117 | "source": [ 118 | "class Pet:\n", 119 | " def __init__(self, name, age):\n", 120 | " self.name = name\n", 121 | " self.age = age\n", 122 | " self.treats_eaten = 0\n", 123 | " \n", 124 | " def give_treats(self, count):\n", 125 | " self.treats_eaten += count" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": 6, 131 | "metadata": { 132 | "collapsed": false 133 | }, 134 | "outputs": [ 135 | { 136 | "name": "stdout", 137 | "output_type": "stream", 138 | "text": [ 139 | "Gregory the Gila Monster ate 2 treats\n" 140 | ] 141 | } 142 | ], 143 | "source": [ 144 | "my_pet = Pet('Gregory the Gila Monster', 3)\n", 145 | "my_pet.give_treats(2)\n", 146 | "print('%s ate %d treats' % (my_pet.name, my_pet.treats_eaten))" 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "metadata": {}, 152 | "source": [ 153 | "---\n", 154 | "\n", 155 | "Now we want to know what kind of pet it is, so we know how to take care of it. We add various attributes and helper methods to accomplish this." 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": 7, 161 | "metadata": { 162 | "collapsed": true 163 | }, 164 | "outputs": [], 165 | "source": [ 166 | "class Pet:\n", 167 | " def __init__(self, name, age, *, has_scales=False, lays_eggs=False, drinks_milk=False):\n", 168 | " self.name = name\n", 169 | " self.age = age\n", 170 | " self.treats_eaten = 0\n", 171 | " self.has_scales = has_scales\n", 172 | " self.lays_eggs = lays_eggs\n", 173 | " self.drinks_milk = drinks_milk\n", 174 | "\n", 175 | " def give_treats(self, count):\n", 176 | " self.treats_eaten += count\n", 177 | " \n", 178 | " @property\n", 179 | " def needs_heat_lamp(self):\n", 180 | " return (\n", 181 | " self.has_scales and\n", 182 | " self.lays_eggs and\n", 183 | " not self.drinks_milk)" 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": 8, 189 | "metadata": { 190 | "collapsed": false 191 | }, 192 | "outputs": [ 193 | { 194 | "name": "stdout", 195 | "output_type": "stream", 196 | "text": [ 197 | "Gregory the Gila Monster needs a heat lamp? True\n" 198 | ] 199 | } 200 | ], 201 | "source": [ 202 | "my_pet = Pet('Gregory the Gila Monster', 3, has_scales=True, lays_eggs=True)\n", 203 | "print('%s needs a heat lamp? %s' % (my_pet.name, my_pet.needs_heat_lamp))" 204 | ] 205 | }, 206 | { 207 | "cell_type": "code", 208 | "execution_count": 9, 209 | "metadata": { 210 | "collapsed": false 211 | }, 212 | "outputs": [ 213 | { 214 | "name": "stdout", 215 | "output_type": "stream", 216 | "text": [ 217 | "Gregory the Gila Monster ate 5 treats\n" 218 | ] 219 | } 220 | ], 221 | "source": [ 222 | "my_pet.give_treats(3)\n", 223 | "my_pet.give_treats(2)\n", 224 | "print('%s ate %d treats' % (my_pet.name, my_pet.treats_eaten))" 225 | ] 226 | }, 227 | { 228 | "cell_type": "markdown", 229 | "metadata": {}, 230 | "source": [ 231 | "---\n", 232 | "\n", 233 | "At this point, it's looking like this class may end up with too many responsibilities. We should create a new class called `Animal` that will separate the intrinsic attributes of the creature from what makes it a `Pet` (like a given name).\n", 234 | "\n", 235 | "We do this by extracting the class `Animal` from `Pet`. We move fields like `has_scales` from the old usage to the new usage. And we use a parameter object to change the constructor of `Pet`." 236 | ] 237 | }, 238 | { 239 | "cell_type": "code", 240 | "execution_count": 10, 241 | "metadata": { 242 | "collapsed": true 243 | }, 244 | "outputs": [], 245 | "source": [ 246 | "import warnings" 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": 11, 252 | "metadata": { 253 | "collapsed": false 254 | }, 255 | "outputs": [], 256 | "source": [ 257 | "class Animal:\n", 258 | " def __init__(self, *, has_scales=False, lays_eggs=False, drinks_milk=False):\n", 259 | " self.has_scales = has_scales\n", 260 | " self.lays_eggs = lays_eggs\n", 261 | " self.drinks_milk = drinks_milk\n", 262 | "\n", 263 | "class Pet:\n", 264 | " def __init__(self, name, age, animal=None, **kwargs):\n", 265 | " self.name = name\n", 266 | " self.age = age\n", 267 | " self.treats_eaten = 0\n", 268 | "\n", 269 | " if kwargs and animal is not None:\n", 270 | " raise TypeError('Must supply either an Animal instance or keyword arguments')\n", 271 | "\n", 272 | " if animal is None:\n", 273 | " warnings.warn('Must directly pass an Animal instance')\n", 274 | " animal = Animal(**kwargs)\n", 275 | " \n", 276 | " self.animal = animal\n", 277 | "\n", 278 | " def give_treats(self, count):\n", 279 | " self.treats_eaten += count\n", 280 | "\n", 281 | " @property\n", 282 | " def needs_heat_lamp(self): # Changed to reference self.animal\n", 283 | " return (\n", 284 | " self.animal.has_scales and\n", 285 | " self.animal.lays_eggs and\n", 286 | " not self.animal.drinks_milk)\n", 287 | "\n", 288 | " @property\n", 289 | " def has_scales(self):\n", 290 | " warnings.warn('Use animal.has_scales instead')\n", 291 | " return self.animal.has_scales\n", 292 | "\n", 293 | " @property\n", 294 | " def lays_eggs(self):\n", 295 | " warnings.warn('Use animal.lays_eggs instead')\n", 296 | " return self.animal.lays_eggs\n", 297 | "\n", 298 | " @property\n", 299 | " def drinks_milk(self):\n", 300 | " warnings.warn('Use animal.drinks_milk instead')\n", 301 | " return self.animal.drinks_milk" 302 | ] 303 | }, 304 | { 305 | "cell_type": "code", 306 | "execution_count": 12, 307 | "metadata": { 308 | "collapsed": false 309 | }, 310 | "outputs": [ 311 | { 312 | "name": "stdout", 313 | "output_type": "stream", 314 | "text": [ 315 | "Gregory the Gila Monster needs a heat lamp? True\n" 316 | ] 317 | } 318 | ], 319 | "source": [ 320 | "animal = Animal(has_scales=True, lays_eggs=True)\n", 321 | "my_pet = Pet('Gregory the Gila Monster', 3, animal) # New usage with no warnings\n", 322 | "print('%s needs a heat lamp? %s' % (my_pet.name, my_pet.needs_heat_lamp))" 323 | ] 324 | }, 325 | { 326 | "cell_type": "code", 327 | "execution_count": 13, 328 | "metadata": { 329 | "collapsed": false 330 | }, 331 | "outputs": [ 332 | { 333 | "name": "stdout", 334 | "output_type": "stream", 335 | "text": [ 336 | "Gregory the Gila Monster needs a heat lamp? True\n" 337 | ] 338 | }, 339 | { 340 | "name": "stderr", 341 | "output_type": "stream", 342 | "text": [ 343 | "/Users/bslatkin/projects/pycon2016/lib/python3.5/site-packages/ipykernel/__main__.py:17: UserWarning: Must directly pass an Animal instance\n" 344 | ] 345 | } 346 | ], 347 | "source": [ 348 | "my_pet = Pet('Gregory the Gila Monster', 3, has_scales=True, lays_eggs=True) # Warning expected because of old usage\n", 349 | "print('%s needs a heat lamp? %s' % (my_pet.name, my_pet.needs_heat_lamp))" 350 | ] 351 | }, 352 | { 353 | "cell_type": "code", 354 | "execution_count": 16, 355 | "metadata": { 356 | "collapsed": false 357 | }, 358 | "outputs": [ 359 | { 360 | "name": "stderr", 361 | "output_type": "stream", 362 | "text": [ 363 | "/Users/bslatkin/projects/pycon2016/lib/python3.5/site-packages/ipykernel/__main__.py:17: UserWarning: Must directly pass an Animal instance\n" 364 | ] 365 | } 366 | ], 367 | "source": [ 368 | "my_pet = Pet('Gregory the Gila Monster', 3) # Warning because this is still the old usage without Animal" 369 | ] 370 | }, 371 | { 372 | "cell_type": "code", 373 | "execution_count": 17, 374 | "metadata": { 375 | "collapsed": false 376 | }, 377 | "outputs": [ 378 | { 379 | "ename": "TypeError", 380 | "evalue": "Must supply either an Animal instance or keyword arguments", 381 | "output_type": "error", 382 | "traceback": [ 383 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 384 | "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", 385 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mmy_pet\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mPet\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Gregory the Gila Monster'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m3\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0manimal\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mhas_scales\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m# Error expected because of mixed usage\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 386 | "\u001b[0;32m\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, name, age, animal, **kwargs)\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 13\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mkwargs\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0manimal\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 14\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mTypeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Must supply either an Animal instance or keyword arguments'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 15\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0manimal\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", 387 | "\u001b[0;31mTypeError\u001b[0m: Must supply either an Animal instance or keyword arguments" 388 | ] 389 | } 390 | ], 391 | "source": [ 392 | "my_pet = Pet('Gregory the Gila Monster', 3, animal, has_scales=True) # Error expected because of mixed usage" 393 | ] 394 | }, 395 | { 396 | "cell_type": "markdown", 397 | "metadata": {}, 398 | "source": [ 399 | "---\n", 400 | "\n", 401 | "Here's the final extracted class now that all of the usage has been moved to the new version." 402 | ] 403 | }, 404 | { 405 | "cell_type": "code", 406 | "execution_count": 18, 407 | "metadata": { 408 | "collapsed": true 409 | }, 410 | "outputs": [], 411 | "source": [ 412 | "class Animal:\n", 413 | " def __init__(self, *, has_scales=False, lays_eggs=False, drinks_milk=False):\n", 414 | " self.has_scales = has_scales\n", 415 | " self.lays_eggs = lays_eggs\n", 416 | " self.drinks_milk = drinks_milk\n", 417 | "\n", 418 | "class Pet:\n", 419 | " def __init__(self, name, age, animal):\n", 420 | " self.name = name\n", 421 | " self.age = age\n", 422 | " self.animal = animal\n", 423 | " self.treats_eaten = 0\n", 424 | "\n", 425 | " def give_treats(self, count):\n", 426 | " self.treats_eaten += count\n", 427 | "\n", 428 | " @property\n", 429 | " def needs_heat_lamp(self):\n", 430 | " return (\n", 431 | " self.animal.has_scales and\n", 432 | " self.animal.lays_eggs and\n", 433 | " not self.animal.drinks_milk)" 434 | ] 435 | }, 436 | { 437 | "cell_type": "code", 438 | "execution_count": 19, 439 | "metadata": { 440 | "collapsed": false 441 | }, 442 | "outputs": [ 443 | { 444 | "name": "stdout", 445 | "output_type": "stream", 446 | "text": [ 447 | "Gregory the Gila Monster needs a heat lamp? True\n" 448 | ] 449 | } 450 | ], 451 | "source": [ 452 | "animal = Animal(has_scales=True, lays_eggs=True)\n", 453 | "my_pet = Pet('Gregory the Gila Monster', 3, animal)\n", 454 | "print('%s needs a heat lamp? %s' % (my_pet.name, my_pet.needs_heat_lamp))" 455 | ] 456 | }, 457 | { 458 | "cell_type": "markdown", 459 | "metadata": {}, 460 | "source": [ 461 | "---\n", 462 | "\n", 463 | "Another intrinsic property of the pet is its `age`. We should refactor this into the interior class. This is more challenging because it's a property being assigned. If you do it the same was as the other attributes, it will break." 464 | ] 465 | }, 466 | { 467 | "cell_type": "code", 468 | "execution_count": 20, 469 | "metadata": { 470 | "collapsed": false 471 | }, 472 | "outputs": [ 473 | { 474 | "name": "stdout", 475 | "output_type": "stream", 476 | "text": [ 477 | "Gregory the Gila Monster is 5 years old\n" 478 | ] 479 | } 480 | ], 481 | "source": [ 482 | "animal = Animal(has_scales=True, lays_eggs=True)\n", 483 | "my_pet = Pet('Gregory the Gila Monster', 3, animal)\n", 484 | "my_pet.age = 5\n", 485 | "print('%s is %d years old' % (my_pet.name, my_pet.age))" 486 | ] 487 | }, 488 | { 489 | "cell_type": "code", 490 | "execution_count": 21, 491 | "metadata": { 492 | "collapsed": false 493 | }, 494 | "outputs": [], 495 | "source": [ 496 | "class Animal:\n", 497 | " def __init__(self, age=None, *, has_scales=False, lays_eggs=False, drinks_milk=False):\n", 498 | " if age is None:\n", 499 | " warnings.warn('Should specify \"age\" for Animal')\n", 500 | " self.age = age\n", 501 | " self.has_scales = has_scales\n", 502 | " self.lays_eggs = lays_eggs\n", 503 | " self.drinks_milk = drinks_milk\n", 504 | "\n", 505 | " \n", 506 | "class Pet:\n", 507 | " def __init__(self, name, age_or_animal, maybe_animal=None):\n", 508 | " self.name = name\n", 509 | "\n", 510 | " if maybe_animal is not None:\n", 511 | " warnings.warn('Should specify \"age\" for Animal')\n", 512 | " self.animal = maybe_animal\n", 513 | " self.animal.age = age_or_animal\n", 514 | " else:\n", 515 | " self.animal = age_or_animal\n", 516 | "\n", 517 | " self.treats_eaten = 0\n", 518 | "\n", 519 | " def give_treats(self, count):\n", 520 | " self.treats_eaten += count\n", 521 | "\n", 522 | " @property\n", 523 | " def needs_heat_lamp(self):\n", 524 | " return (\n", 525 | " self.animal.has_scales and\n", 526 | " self.animal.lays_eggs and\n", 527 | " not self.animal.drinks_milk)\n", 528 | "\n", 529 | " @property\n", 530 | " def age(self):\n", 531 | " warnings.warn('Should use animal.age')\n", 532 | " return self.animal.age" 533 | ] 534 | }, 535 | { 536 | "cell_type": "code", 537 | "execution_count": 22, 538 | "metadata": { 539 | "collapsed": false 540 | }, 541 | "outputs": [ 542 | { 543 | "name": "stdout", 544 | "output_type": "stream", 545 | "text": [ 546 | "Gregory the Gila Monster is 3 years old\n" 547 | ] 548 | }, 549 | { 550 | "name": "stderr", 551 | "output_type": "stream", 552 | "text": [ 553 | "/Users/bslatkin/projects/pycon2016/lib/python3.5/site-packages/ipykernel/__main__.py:4: UserWarning: Should specify \"age\" for Animal\n", 554 | "/Users/bslatkin/projects/pycon2016/lib/python3.5/site-packages/ipykernel/__main__.py:16: UserWarning: Should specify \"age\" for Animal\n", 555 | "/Users/bslatkin/projects/pycon2016/lib/python3.5/site-packages/ipykernel/__main__.py:36: UserWarning: Should use animal.age\n" 556 | ] 557 | } 558 | ], 559 | "source": [ 560 | "animal = Animal(has_scales=True, lays_eggs=True) # Warning expected\n", 561 | "my_pet = Pet('Gregory the Gila Monster', 3, animal) # Warning expected\n", 562 | "print('%s is %d years old' % (my_pet.name, my_pet.age)) # Warning expected" 563 | ] 564 | }, 565 | { 566 | "cell_type": "code", 567 | "execution_count": 23, 568 | "metadata": { 569 | "collapsed": false 570 | }, 571 | "outputs": [ 572 | { 573 | "name": "stdout", 574 | "output_type": "stream", 575 | "text": [ 576 | "Gregory the Gila Monster is 3 years old\n" 577 | ] 578 | } 579 | ], 580 | "source": [ 581 | "animal = Animal(3, has_scales=True, lays_eggs=True) # No warnings for new usage\n", 582 | "my_pet = Pet('Gregory the Gila Monster', animal)\n", 583 | "print('%s is %d years old' % (my_pet.name, my_pet.animal.age)) # No warnings for new usage" 584 | ] 585 | }, 586 | { 587 | "cell_type": "markdown", 588 | "metadata": {}, 589 | "source": [ 590 | "---\n", 591 | "\n", 592 | "One problem is that this middle refactoring state breaks when you try to assign to the `age` attribute." 593 | ] 594 | }, 595 | { 596 | "cell_type": "code", 597 | "execution_count": 24, 598 | "metadata": { 599 | "collapsed": false 600 | }, 601 | "outputs": [ 602 | { 603 | "ename": "AttributeError", 604 | "evalue": "can't set attribute", 605 | "output_type": "error", 606 | "traceback": [ 607 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 608 | "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", 609 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mmy_pet\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mage\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m5\u001b[0m \u001b[0;31m# Error expected\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 610 | "\u001b[0;31mAttributeError\u001b[0m: can't set attribute" 611 | ] 612 | } 613 | ], 614 | "source": [ 615 | "my_pet.age = 5 # Error expected" 616 | ] 617 | }, 618 | { 619 | "cell_type": "markdown", 620 | "metadata": {}, 621 | "source": [ 622 | "What's missing is the `@property.setter` that can handle assignment during the transition period." 623 | ] 624 | }, 625 | { 626 | "cell_type": "code", 627 | "execution_count": 25, 628 | "metadata": { 629 | "collapsed": true 630 | }, 631 | "outputs": [], 632 | "source": [ 633 | "class Animal:\n", 634 | " def __init__(self, age=None, *, has_scales=False, lays_eggs=False, drinks_milk=False):\n", 635 | " if age is None:\n", 636 | " warnings.warn('Should specify \"age\" for Animal')\n", 637 | " self.age = age\n", 638 | " self.has_scales = has_scales\n", 639 | " self.lays_eggs = lays_eggs\n", 640 | " self.drinks_milk = drinks_milk\n", 641 | "\n", 642 | " \n", 643 | "class Pet:\n", 644 | " def __init__(self, name, age_or_animal, maybe_animal=None):\n", 645 | " self.name = name\n", 646 | "\n", 647 | " if maybe_animal is not None:\n", 648 | " warnings.warn('Should specify \"age\" for Animal')\n", 649 | " self.animal = maybe_animal\n", 650 | " self.animal.age = age_or_animal\n", 651 | " else:\n", 652 | " self.animal = age_or_animal\n", 653 | "\n", 654 | " self.treats_eaten = 0\n", 655 | "\n", 656 | " def give_treats(self, count):\n", 657 | " self.treats_eaten += count\n", 658 | "\n", 659 | " @property\n", 660 | " def needs_heat_lamp(self):\n", 661 | " return (\n", 662 | " self.animal.has_scales and\n", 663 | " self.animal.lays_eggs and\n", 664 | " not self.animal.drinks_milk)\n", 665 | "\n", 666 | " @property\n", 667 | " def age(self):\n", 668 | " warnings.warn('Should use animal.age')\n", 669 | " return self.animal.age\n", 670 | "\n", 671 | " @age.setter\n", 672 | " def age(self, new_age):\n", 673 | " warnings.warn('Should assign animal.age')\n", 674 | " self.animal.age = new_age" 675 | ] 676 | }, 677 | { 678 | "cell_type": "code", 679 | "execution_count": 26, 680 | "metadata": { 681 | "collapsed": false 682 | }, 683 | "outputs": [ 684 | { 685 | "name": "stderr", 686 | "output_type": "stream", 687 | "text": [ 688 | "/Users/bslatkin/projects/pycon2016/lib/python3.5/site-packages/ipykernel/__main__.py:41: UserWarning: Should assign animal.age\n" 689 | ] 690 | } 691 | ], 692 | "source": [ 693 | "animal = Animal(3, has_scales=True, lays_eggs=True)\n", 694 | "my_pet = Pet('Gregory the Gila Monster', animal)\n", 695 | "my_pet.age = 5 # Warning expected because of old usage" 696 | ] 697 | }, 698 | { 699 | "cell_type": "markdown", 700 | "metadata": {}, 701 | "source": [ 702 | "---\n", 703 | "\n", 704 | "Now we can move everything over to the new usage, and remove the old way." 705 | ] 706 | }, 707 | { 708 | "cell_type": "code", 709 | "execution_count": 27, 710 | "metadata": { 711 | "collapsed": false 712 | }, 713 | "outputs": [], 714 | "source": [ 715 | "class Animal:\n", 716 | " def __init__(self, age, *, has_scales=False, lays_eggs=False, drinks_milk=False):\n", 717 | " self.age = age\n", 718 | " self.has_scales = has_scales\n", 719 | " self.lays_eggs = lays_eggs\n", 720 | " self.drinks_milk = drinks_milk\n", 721 | " \n", 722 | "class Pet:\n", 723 | " def __init__(self, name, animal):\n", 724 | " self.name = name\n", 725 | " self.animal = animal\n", 726 | " self.treats_eaten = 0\n", 727 | "\n", 728 | " def give_treats(self, count):\n", 729 | " self.treats_eaten += count\n", 730 | " \n", 731 | " @property\n", 732 | " def needs_heat_lamp(self):\n", 733 | " return (\n", 734 | " self.animal.has_scales and\n", 735 | " self.animal.lays_eggs and\n", 736 | " not self.animal.drinks_milk)" 737 | ] 738 | }, 739 | { 740 | "cell_type": "code", 741 | "execution_count": 28, 742 | "metadata": { 743 | "collapsed": false 744 | }, 745 | "outputs": [ 746 | { 747 | "name": "stdout", 748 | "output_type": "stream", 749 | "text": [ 750 | "Gregory the Gila Monster is 5 years old\n" 751 | ] 752 | } 753 | ], 754 | "source": [ 755 | "animal = Animal(3, has_scales=True, lays_eggs=True) # New usage works\n", 756 | "my_pet = Pet('Gregory the Gila Monster', animal)\n", 757 | "my_pet.animal.age = 5\n", 758 | "print('%s is %d years old' % (my_pet.name, my_pet.animal.age)) # New usage works" 759 | ] 760 | }, 761 | { 762 | "cell_type": "markdown", 763 | "metadata": {}, 764 | "source": [ 765 | "---\n", 766 | "\n", 767 | "One gotcha of extracting assignable attributes is you can still accidentally use the old assignments because Python will add an attribute to a class when it's assigned." 768 | ] 769 | }, 770 | { 771 | "cell_type": "code", 772 | "execution_count": 29, 773 | "metadata": { 774 | "collapsed": false 775 | }, 776 | "outputs": [ 777 | { 778 | "name": "stdout", 779 | "output_type": "stream", 780 | "text": [ 781 | "Gregory the Gila Monster is 3 years old\n" 782 | ] 783 | } 784 | ], 785 | "source": [ 786 | "animal = Animal(3, has_scales=True, lays_eggs=True)\n", 787 | "my_pet = Pet('Gregory the Gila Monster', animal)\n", 788 | "my_pet.age = 5 # Old usage doesn't raise an error\n", 789 | "print('%s is %d years old' % (my_pet.name, my_pet.animal.age)) # Prints wrong result" 790 | ] 791 | }, 792 | { 793 | "cell_type": "markdown", 794 | "metadata": {}, 795 | "source": [ 796 | "You can avoid this by leaving behind a tombstone property to prevent this type of usage. Old habits and muscle memory are hard to shake, so this can be a worthwhile investment if you're refactoring particularly commonly used components." 797 | ] 798 | }, 799 | { 800 | "cell_type": "code", 801 | "execution_count": 30, 802 | "metadata": { 803 | "collapsed": true 804 | }, 805 | "outputs": [], 806 | "source": [ 807 | "class Animal:\n", 808 | " def __init__(self, age, *, has_scales=False, lays_eggs=False, drinks_milk=False):\n", 809 | " self.age = age\n", 810 | " self.has_scales = has_scales\n", 811 | " self.lays_eggs = lays_eggs\n", 812 | " self.drinks_milk = drinks_milk\n", 813 | "\n", 814 | " \n", 815 | "class Pet:\n", 816 | " def __init__(self, name, animal):\n", 817 | " self.name = name\n", 818 | " self.animal = animal\n", 819 | " self.treats_eaten = 0\n", 820 | "\n", 821 | " def give_treats(self, count):\n", 822 | " self.treats_eaten += count\n", 823 | "\n", 824 | " @property\n", 825 | " def needs_heat_lamp(self):\n", 826 | " return (\n", 827 | " self.animal.has_scales and\n", 828 | " self.animal.lays_eggs and\n", 829 | " not self.animal.drinks_milk)\n", 830 | " \n", 831 | " @property\n", 832 | " def age(self):\n", 833 | " raise AttributeError('Must use animal.age')\n", 834 | "\n", 835 | " @age.setter\n", 836 | " def age(self, new_age):\n", 837 | " raise AttributeError('Must assign animal.age')" 838 | ] 839 | }, 840 | { 841 | "cell_type": "markdown", 842 | "metadata": {}, 843 | "source": [ 844 | "Now we'll get an error immediately on accidental usage of the pre-migration approach." 845 | ] 846 | }, 847 | { 848 | "cell_type": "code", 849 | "execution_count": 31, 850 | "metadata": { 851 | "collapsed": false 852 | }, 853 | "outputs": [ 854 | { 855 | "ename": "AttributeError", 856 | "evalue": "Must assign animal.age", 857 | "output_type": "error", 858 | "traceback": [ 859 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 860 | "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", 861 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0manimal\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mAnimal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mhas_scales\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mlays_eggs\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0mmy_pet\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mPet\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Gregory the Gila Monster'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0manimal\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mmy_pet\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mage\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m5\u001b[0m \u001b[0;31m# Old usage raises an error\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 862 | "\u001b[0;32m\u001b[0m in \u001b[0;36mage\u001b[0;34m(self, new_age)\u001b[0m\n\u001b[1;32m 29\u001b[0m \u001b[0;34m@\u001b[0m\u001b[0mage\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msetter\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 30\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mage\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnew_age\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 31\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mAttributeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Must assign animal.age'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 863 | "\u001b[0;31mAttributeError\u001b[0m: Must assign animal.age" 864 | ] 865 | } 866 | ], 867 | "source": [ 868 | "animal = Animal(3, has_scales=True, lays_eggs=True)\n", 869 | "my_pet = Pet('Gregory the Gila Monster', animal)\n", 870 | "my_pet.age = 5 # Old usage raises an error" 871 | ] 872 | } 873 | ], 874 | "metadata": { 875 | "kernelspec": { 876 | "display_name": "Python 3", 877 | "language": "python", 878 | "name": "python3" 879 | }, 880 | "language_info": { 881 | "codemirror_mode": { 882 | "name": "ipython", 883 | "version": 3 884 | }, 885 | "file_extension": ".py", 886 | "mimetype": "text/x-python", 887 | "name": "python", 888 | "nbconvert_exporter": "python", 889 | "pygments_lexer": "ipython3", 890 | "version": "3.5.1" 891 | } 892 | }, 893 | "nbformat": 4, 894 | "nbformat_minor": 0 895 | } 896 | -------------------------------------------------------------------------------- /Extract Closure.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Example 3: Extract Closure\n", 8 | "\n", 9 | "[Extract method in the refactoring catalog.](http://refactoring.com/catalog/extractMethod.html)" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "---\n", 17 | "\n", 18 | "Say you're keeping track of the grades for a set of students in a class. This code works, but the `print_stats` function has a lot going on. If you want to compute any more stats (like median) it's going to get ugly." 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 3, 24 | "metadata": { 25 | "collapsed": true 26 | }, 27 | "outputs": [], 28 | "source": [ 29 | "class Grade:\n", 30 | " def __init__(self, student, score):\n", 31 | " self.student = student\n", 32 | " self.score = score\n", 33 | "\n", 34 | "\n", 35 | "def print_stats(grades):\n", 36 | " if not grades:\n", 37 | " raise ValueError('Must supply at least one Grade')\n", 38 | " \n", 39 | " total, count = 0, 0\n", 40 | " low, high = float('inf'), float('-inf')\n", 41 | " for grade in grades:\n", 42 | " total += grade.score\n", 43 | " count += 1\n", 44 | " if grade.score < low:\n", 45 | " low = grade.score\n", 46 | " elif grade.score > high:\n", 47 | " high = grade.score\n", 48 | "\n", 49 | " average = total / count\n", 50 | "\n", 51 | " print('Average score: %.1f, low score: %.1f, high score %.1f' %\n", 52 | " (average, low, high))" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 4, 58 | "metadata": { 59 | "collapsed": false 60 | }, 61 | "outputs": [ 62 | { 63 | "name": "stdout", 64 | "output_type": "stream", 65 | "text": [ 66 | "Average score: 87.5, low score: 73.0, high score 96.0\n" 67 | ] 68 | } 69 | ], 70 | "source": [ 71 | "grades = [Grade('Bob', 92), Grade('Sally', 89), Grade('Roger', 73), Grade('Alice', 96)]\n", 72 | "print_stats(grades)" 73 | ] 74 | }, 75 | { 76 | "cell_type": "markdown", 77 | "metadata": {}, 78 | "source": [ 79 | "---\n", 80 | "\n", 81 | "One common way people try to make this more readable is with a closure because it at least isolates the inside of the loop. This is annoying because you need to use the `nonlocal` keyword to make sure the closure works. In Python 2 it's even worse because `nonlocal` isn't available." 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 5, 87 | "metadata": { 88 | "collapsed": true 89 | }, 90 | "outputs": [], 91 | "source": [ 92 | "def print_stats(grades):\n", 93 | " if not grades:\n", 94 | " raise ValueError('Must supply at least one Grade')\n", 95 | " \n", 96 | " total, count = 0, 0\n", 97 | " low, high = float('inf'), float('-inf')\n", 98 | "\n", 99 | " def adjust_stats(grade):\n", 100 | " nonlocal total, count, low, high\n", 101 | " total += grade.score\n", 102 | " count += 1\n", 103 | " if grade.score < low:\n", 104 | " low = grade.score\n", 105 | " elif grade.score > high:\n", 106 | " high = grade.score\n", 107 | "\n", 108 | " for grade in grades:\n", 109 | " adjust_stats(grade)\n", 110 | " \n", 111 | " average = total / count\n", 112 | "\n", 113 | " print('Average score: %.1f, low score: %.1f, high score %.1f' %\n", 114 | " (average, low, high))" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 6, 120 | "metadata": { 121 | "collapsed": false 122 | }, 123 | "outputs": [ 124 | { 125 | "name": "stdout", 126 | "output_type": "stream", 127 | "text": [ 128 | "Average score: 87.5, low score: 73.0, high score 96.0\n" 129 | ] 130 | } 131 | ], 132 | "source": [ 133 | "print_stats(grades)" 134 | ] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "metadata": {}, 139 | "source": [ 140 | "---\n", 141 | "\n", 142 | "What's better is to split the inner closure into a helper class. You make the helper class having a single entrypoint named `__call__` so it acts like a plain function. The presence of `__call__` is a hint to the reader than the purpose of the class is to be a stateful closure." 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": 7, 148 | "metadata": { 149 | "collapsed": true 150 | }, 151 | "outputs": [], 152 | "source": [ 153 | "class CalculateStats:\n", 154 | " def __init__(self):\n", 155 | " self.total = 0\n", 156 | " self.count = 0\n", 157 | " self.low = float('inf')\n", 158 | " self.high = float('-inf')\n", 159 | "\n", 160 | " def __call__(self, grades):\n", 161 | " for grade in grades:\n", 162 | " self.total += grade.score\n", 163 | " self.count += 1\n", 164 | " if grade.score < self.low:\n", 165 | " self.low = grade.score\n", 166 | " elif grade.score > self.high:\n", 167 | " self.high = grade.score\n", 168 | "\n", 169 | " \n", 170 | "def print_stats(grades):\n", 171 | " if not grades:\n", 172 | " raise ValueError('Must supply at least one Grade')\n", 173 | "\n", 174 | " stats = CalculateStats()\n", 175 | " stats(grades)\n", 176 | " average = stats.total / stats.count\n", 177 | "\n", 178 | " print('Average score: %.1f, low score: %.1f, high score %.1f' %\n", 179 | " (average, stats.low, stats.high))" 180 | ] 181 | }, 182 | { 183 | "cell_type": "code", 184 | "execution_count": 8, 185 | "metadata": { 186 | "collapsed": false 187 | }, 188 | "outputs": [ 189 | { 190 | "name": "stdout", 191 | "output_type": "stream", 192 | "text": [ 193 | "Average score: 87.5, low score: 73.0, high score 96.0\n" 194 | ] 195 | } 196 | ], 197 | "source": [ 198 | "print_stats(grades)" 199 | ] 200 | }, 201 | { 202 | "cell_type": "markdown", 203 | "metadata": {}, 204 | "source": [ 205 | "---\n", 206 | "\n", 207 | "You can even add other properties to this closure to give the illusion it's doing more bookkeeping than it really is." 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": 9, 213 | "metadata": { 214 | "collapsed": true 215 | }, 216 | "outputs": [], 217 | "source": [ 218 | "class CalculateStats:\n", 219 | " def __init__(self):\n", 220 | " self.total = 0\n", 221 | " self.count = 0\n", 222 | " self.low = float('inf')\n", 223 | " self.high = float('-inf')\n", 224 | "\n", 225 | " def __call__(self, grades):\n", 226 | " for grade in grades:\n", 227 | " self.total += grade.score\n", 228 | " self.count += 1\n", 229 | " if grade.score < self.low:\n", 230 | " self.low = grade.score\n", 231 | " elif grade.score > self.high:\n", 232 | " self.high = grade.score\n", 233 | "\n", 234 | " @property\n", 235 | " def average(self):\n", 236 | " return self.total / self.count\n", 237 | "\n", 238 | " \n", 239 | "def print_stats(grades):\n", 240 | " if not grades:\n", 241 | " raise ValueError('Must supply at least one Grade')\n", 242 | "\n", 243 | " stats = CalculateStats()\n", 244 | " stats(grades)\n", 245 | "\n", 246 | " print('Average score: %.1f, low score: %.1f, high score %.1f' %\n", 247 | " (stats.average, stats.low, stats.high))" 248 | ] 249 | }, 250 | { 251 | "cell_type": "code", 252 | "execution_count": 10, 253 | "metadata": { 254 | "collapsed": false 255 | }, 256 | "outputs": [ 257 | { 258 | "name": "stdout", 259 | "output_type": "stream", 260 | "text": [ 261 | "Average score: 87.5, low score: 73.0, high score 96.0\n" 262 | ] 263 | } 264 | ], 265 | "source": [ 266 | "print_stats(grades)" 267 | ] 268 | }, 269 | { 270 | "cell_type": "markdown", 271 | "metadata": {}, 272 | "source": [ 273 | "---\n", 274 | "\n", 275 | "If you need more than one entrypoint method, you probably need to redraw the boundaries of responsibility between the classes and go for real method names, not just `__call__`." 276 | ] 277 | } 278 | ], 279 | "metadata": { 280 | "kernelspec": { 281 | "display_name": "Python 3", 282 | "language": "python", 283 | "name": "python3" 284 | }, 285 | "language_info": { 286 | "codemirror_mode": { 287 | "name": "ipython", 288 | "version": 3 289 | }, 290 | "file_extension": ".py", 291 | "mimetype": "text/x-python", 292 | "name": "python", 293 | "nbconvert_exporter": "python", 294 | "pygments_lexer": "ipython3", 295 | "version": "3.5.1" 296 | } 297 | }, 298 | "nbformat": 4, 299 | "nbformat_minor": 0 300 | } 301 | -------------------------------------------------------------------------------- /Extract Variable.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Example 1: Extract variable\n", 8 | "\n", 9 | "[Extract variable in the refactoring catalog](http://refactoring.com/catalog/extractVariable.html)" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "---\n", 17 | "Some logic to determine the best time to eat certain foods." 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": 5, 23 | "metadata": { 24 | "collapsed": false 25 | }, 26 | "outputs": [], 27 | "source": [ 28 | "MONTHS = ('January', 'February', 'March', 'April', 'May', 'June',\n", 29 | " 'July', 'August', 'September', 'October', 'November', 'December')" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 6, 35 | "metadata": { 36 | "collapsed": false 37 | }, 38 | "outputs": [ 39 | { 40 | "name": "stdout", 41 | "output_type": "stream", 42 | "text": [ 43 | "November is a good time to eat oysters\n" 44 | ] 45 | } 46 | ], 47 | "source": [ 48 | "import random\n", 49 | "month = random.choice(MONTHS)\n", 50 | "\n", 51 | "if (month.lower().endswith('r') or\n", 52 | " month.lower().endswith('ary')):\n", 53 | " print('%s is a good time to eat oysters' % month)\n", 54 | "elif 8 > MONTHS.index(month) > 4:\n", 55 | " print('%s is a good time to eat tomatoes' % month)\n", 56 | "else:\n", 57 | " print('%s is a good time to eat asparagus' % month)" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "---\n", 65 | "\n", 66 | "Python creates temporaries for every expression, so there's no cost in extracting variables for the sake of clarity." 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 7, 72 | "metadata": { 73 | "collapsed": false 74 | }, 75 | "outputs": [ 76 | { 77 | "name": "stdout", 78 | "output_type": "stream", 79 | "text": [ 80 | "November is a good time to eat oysters\n" 81 | ] 82 | } 83 | ], 84 | "source": [ 85 | "lowered = month.lower()\n", 86 | "ends_in_r = lowered.endswith('r')\n", 87 | "ends_in_ary = lowered.endswith('ary')\n", 88 | "summer = 8 > MONTHS.index(month) > 4\n", 89 | "\n", 90 | "if ends_in_r or ends_in_ary:\n", 91 | " print('%s is a good time to eat oysters' % month)\n", 92 | "elif summer:\n", 93 | " print('%s is a good time to eat tomatoes' % month)\n", 94 | "else:\n", 95 | " print('%s is a good time to eat asparagus' % month)" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "metadata": {}, 101 | "source": [ 102 | "---\n", 103 | "\n", 104 | "If the logic is getting complicated move it into a helper class that determines the condition based on parameters." 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": 8, 110 | "metadata": { 111 | "collapsed": false 112 | }, 113 | "outputs": [], 114 | "source": [ 115 | "def oysters_good(month):\n", 116 | " month_lowered = month.lower()\n", 117 | " return (\n", 118 | " month_lowered.endswith('r') or\n", 119 | " month_lowered.endswith('ary'))\n", 120 | "\n", 121 | "def tomatoes_good(month):\n", 122 | " index = MONTHS.index(month)\n", 123 | " return 8 > index > 4" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 9, 129 | "metadata": { 130 | "collapsed": false 131 | }, 132 | "outputs": [ 133 | { 134 | "name": "stdout", 135 | "output_type": "stream", 136 | "text": [ 137 | "November is a good time to eat oysters\n" 138 | ] 139 | } 140 | ], 141 | "source": [ 142 | "time_for_oysters = oysters_good(month)\n", 143 | "time_for_tomatoes = tomatoes_good(month)\n", 144 | "\n", 145 | "if time_for_oysters:\n", 146 | " print('%s is a good time to eat oysters' % month)\n", 147 | "elif time_for_tomatoes:\n", 148 | " print('%s is a good time to eat tomatoes' % month)\n", 149 | "else:\n", 150 | " print('%s is a good time to eat asparagus' % month)" 151 | ] 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "metadata": {}, 156 | "source": [ 157 | "---\n", 158 | "\n", 159 | "Alternatively, implement `__nonzero__` or `__bool__` so you can drop the object in place of the old expression. This can reduce the number of delta lines in a refactoring commit, which makes a refactoring feel less scary. It can also be easier to read." 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": 10, 165 | "metadata": { 166 | "collapsed": false 167 | }, 168 | "outputs": [], 169 | "source": [ 170 | "class OystersGood:\n", 171 | " def __init__(self, month):\n", 172 | " month = month\n", 173 | " month_lowered = month.lower()\n", 174 | " self.ends_in_r = month_lowered.endswith('r')\n", 175 | " self.ends_in_ary = month_lowered.endswith('ary')\n", 176 | " self._result = self.ends_in_r or self.ends_in_ary\n", 177 | "\n", 178 | " def __bool__(self): # Equivalent to __nonzero__ in Python 2\n", 179 | " return self._result\n", 180 | " \n", 181 | "\n", 182 | "class TomatoesGood:\n", 183 | " def __init__(self, month):\n", 184 | " self.index = MONTHS.index(month)\n", 185 | " self._result = 8 > self.index > 4\n", 186 | " \n", 187 | " def __bool__(self): # Equivalent to __nonzero__ in Python 2\n", 188 | " return self._result" 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": 11, 194 | "metadata": { 195 | "collapsed": false 196 | }, 197 | "outputs": [ 198 | { 199 | "name": "stdout", 200 | "output_type": "stream", 201 | "text": [ 202 | "November is a good time to eat oysters\n" 203 | ] 204 | } 205 | ], 206 | "source": [ 207 | "time_for_oysters = OystersGood(month)\n", 208 | "time_for_tomatoes = TomatoesGood(month)\n", 209 | "\n", 210 | "if time_for_oysters:\n", 211 | " print('%s is a good time to eat oysters' % month)\n", 212 | "elif time_for_tomatoes:\n", 213 | " print('%s is a good time to eat tomatoes' % month)\n", 214 | "else:\n", 215 | " print('%s is a good time to eat asparagus' % month)" 216 | ] 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "metadata": {}, 221 | "source": [ 222 | "---\n", 223 | "\n", 224 | "Now the helper function is easy to test and introspect." 225 | ] 226 | }, 227 | { 228 | "cell_type": "code", 229 | "execution_count": 12, 230 | "metadata": { 231 | "collapsed": true 232 | }, 233 | "outputs": [], 234 | "source": [ 235 | "test = OystersGood('November')\n", 236 | "assert test\n", 237 | "assert test.ends_in_r\n", 238 | "assert not test.ends_in_ary\n", 239 | "\n", 240 | "test = OystersGood('July')\n", 241 | "assert not test\n", 242 | "assert not test.ends_in_r\n", 243 | "assert not test.ends_in_ary" 244 | ] 245 | } 246 | ], 247 | "metadata": { 248 | "kernelspec": { 249 | "display_name": "Python 3", 250 | "language": "python", 251 | "name": "python3" 252 | }, 253 | "language_info": { 254 | "codemirror_mode": { 255 | "name": "ipython", 256 | "version": 3 257 | }, 258 | "file_extension": ".py", 259 | "mimetype": "text/x-python", 260 | "name": "python", 261 | "nbconvert_exporter": "python", 262 | "pygments_lexer": "ipython3", 263 | "version": "3.5.1" 264 | } 265 | }, 266 | "nbformat": 4, 267 | "nbformat_minor": 0 268 | } 269 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Brett Slatkin 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Refactoring Python: Why and how to restructure your code 2 | 3 | From the PyCon 2016 talk in Portland, OR ([offical schedule link](https://us.pycon.org/2016/schedule/presentation/2073/)). 4 | 5 | Slides are here (click image below to view): 6 | 7 | [![Slides](slides_screenshot.png)](https://docs.google.com/presentation/d/1o9SWhgtGmk5NfddReoRzinu1kBbHLKmIWWZ0nnpuZ_o/edit?usp=sharing) 8 | 9 | ## Example code 10 | 11 | Here are full Jupyter notebooks for each of the examples in the slides: 12 | 13 | - [Example 1: Extract Variable & Function](Extract Variable.ipynb) 14 | - [Example 2: Extract Class](Extract Class.ipynb) 15 | - [Example 3: Extract Closure](Extract Method.ipynb) 16 | 17 | To run the example code (must have Python3 installed): 18 | 19 | 1. Download this repo 20 | 2. Go into the repo directory on your local machine 21 | 2. `pyvenv .` 22 | 3. `source bin/activate` 23 | 4. `pip3 install -r requirements.txt` 24 | 5. `jupyter notebook` 25 | 6. Control-C to kill the notebook when you're done 26 | 7. `deactivate` to leave the Python environment 27 | 28 | ## License 29 | 30 | The code within is [released under the Apache 2.0 License](LICENSE). 31 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appnope==0.1.0 2 | backports.shutil-get-terminal-size==1.0.0 3 | decorator==4.0.9 4 | entrypoints==0.2.1 5 | gnureadline==6.3.3 6 | ipykernel==4.3.1 7 | ipython==4.2.0 8 | ipython-genutils==0.1.0 9 | ipywidgets==5.1.3 10 | Jinja2==2.8 11 | jsonschema==2.5.1 12 | jupyter==1.0.0 13 | jupyter-client==4.2.2 14 | jupyter-console==4.1.1 15 | jupyter-core==4.1.0 16 | MarkupSafe==0.23 17 | mistune==0.7.2 18 | nbconvert==4.2.0 19 | nbformat==4.0.1 20 | notebook==4.2.0 21 | pexpect==4.0.1 22 | pickleshare==0.7.2 23 | ptyprocess==0.5.1 24 | Pygments==2.1.3 25 | pyzmq==15.2.0 26 | qtconsole==4.2.1 27 | simplegeneric==0.8.1 28 | terminado==0.6 29 | tornado==4.3 30 | traitlets==4.2.1 31 | widgetsnbextension==1.2.2 32 | -------------------------------------------------------------------------------- /slides_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bslatkin/pycon2016/a966e2035583db5f046e30f0a6a196cbe3b2b8df/slides_screenshot.png --------------------------------------------------------------------------------