Using attrs to create dummy python 🐍 objects.

Published
Β· 4 years ago
pythonclass
Attrs Image

At the end of this post you'll be able to understand

  • How to write clean code to create classes.
  • How to get human readable __repr__ for classes.
  • How to create dummy objects easily for testing purposes.
Let's get started
Python 🐍 has been known for its simplicity.
Just like
You can define a class in python 🐍 in just a couple of lines of code. πŸ‘‡πŸ»πŸ‘‡πŸ»πŸ‘‡πŸ»
class SimpleClass:
    pass
class SimpleClass:
    pass
Still devs figure out more ways to make it even simpler than ever before.
Let's say you've a vehicle and you want to put it to the test.
A vehicle 🚘 can have certain attributes like an id and load_capacity πŸ’° as mentioned below πŸ‘‡πŸ»πŸ‘‡πŸ»πŸ‘‡πŸ».
class VehicleSimple:
    def __init__(self, id, load_capacity):
        self.id = id
        self.load_capacity = load_capacity
class VehicleSimple:
    def __init__(self, id, load_capacity):
        self.id = id
        self.load_capacity = load_capacity
You notice that it's just the class definition and you have repeated word id and load_capacity thrice 😲.
Isn't it too much work πŸ€” ? Can't you do better ❓
Of course you can, because there's always a better way !!
πŸ™‡πŸ»β€β™‚οΈ Thanks to Hynek Schlawack for maintaining attrs library πŸ“š. It further enhances the code simplicity by reducing boilerplate code.
Let's install attrs first.
pip install attrs
pip install attrs
Writing πŸ“ a quick code snippet to use it. πŸ‘‡πŸ»πŸ‘‡πŸ»πŸ‘‡πŸ»
import attr
@attr.s
class VehicleUsingAttr:
    id = attr.ib()
    load_capacity = attr.ib()
import attr
@attr.s
class VehicleUsingAttr:
    id = attr.ib()
    load_capacity = attr.ib()

Here you've done three things to put attrs into effect:

  1. Used @attr.s class decorator πŸŽ„.
  2. Used attr.ib() as attributes' value.
  3. Removed __init__ method instead of typing id and load 3 times 😌.
Going further you'd instantiate class like this πŸ‘‡πŸ»πŸ‘‡πŸ»πŸ‘‡πŸ».
test_vehicle_simple = VehicleSimple(id=1, load_capacity=600)
test_vehicle_attrs = VehicleUsingAttr(id=2, load_capacity=600)
test_vehicle_simple = VehicleSimple(id=1, load_capacity=600)
test_vehicle_attrs = VehicleUsingAttr(id=2, load_capacity=600)
You can see there's no difference in the way you instantiate the class.
Let's test the console outputs πŸ‘‡πŸ»πŸ‘‡πŸ»πŸ‘‡πŸ»
print(test_vehicle_simple)
# Out: <__main__.VehicleSimple object at 0x7f8a7c973d10>
print(test_vehicle_simple)
# Out: <__main__.VehicleSimple object at 0x7f8a7c973d10>
πŸ˜• Gibberish πŸ€·πŸ»β€β™‚οΈ __repr__.
print(test_vehicle_attrs)
# Out: VehicleUsingAttr(id=2, load_capacity=600)
print(test_vehicle_attrs)
# Out: VehicleUsingAttr(id=2, load_capacity=600)
Nice πŸ‘€looking __repr__ for humans. Isn't that amazing ‼️

Things we achived:

  1. Reduced boilerplate code.
  2. Eased the implementation.
  3. A nice looking comprehensible __repr__.
I know you're an experienced developer and working on a project which still lacks the real world data to be fed in. So you instantiate your class and make a vehicle 🚘 object with sensible random data.
Suppose for testing purpose you need hundreds πŸ’― of such vehicle 🚘 objects. So you define two functions which randomly generates sane data. πŸ‘‡πŸ»πŸ‘‡πŸ»πŸ‘‡πŸ»
import random
def random_vehicle_id():
    return random.randint(1, 1000)
def random_load():
    return random.randint(100,1000)
import random
def random_vehicle_id():
    return random.randint(1, 1000)
def random_load():
    return random.randint(100,1000)
Now you create your objects with the dummy πŸ•Ά data using these functions πŸ‘‡πŸ»πŸ‘‡πŸ»πŸ‘‡πŸ»
def get_random_vehicle():
    return VehicleSimple(
        id=random_vehicle_id(),
        load_capacity=random_load()
)
test_100_v = [get_random_vehicle() for _ in range(100)]
def get_random_vehicle():
    return VehicleSimple(
        id=random_vehicle_id(),
        load_capacity=random_load()
)
test_100_v = [get_random_vehicle() for _ in range(100)]
Now you have 100 πŸ’― vehicles in hand for the testing purpose. We can move ahead and carry out your testing without having the real world 🌍 data in hand.
But you know attrs can make it further easy for you ! Wait what ? Yep!
Let's see how
attr.ib's factory argument takes callable and instantiate your vehicle with dummy data.
As simple as πŸ‘‡πŸ»πŸ‘‡πŸ»πŸ‘‡πŸ»
import attr
@attr.s
class VehicleUsingAttrs:
    id = attr.ib(factory=lambda: random.randint(1,1000))
    load_capacity = attr.ib(factory=lambda: random.randint(100,1000)) 
test_100_v = [VehicleUsingAttrs() for _ in range(100)]
import attr
@attr.s
class VehicleUsingAttrs:
    id = attr.ib(factory=lambda: random.randint(1,1000))
    load_capacity = attr.ib(factory=lambda: random.randint(100,1000)) 
test_100_v = [VehicleUsingAttrs() for _ in range(100)]
That's how you create dummy objects like a senior dev. 😎

Key takeaways:

  1. @attr.s class decorator makes the class definition simple and precise.
  2. Gives you a nice human πŸ€“ readable __repr__ that further helps in debugging πŸžπŸ”«.
  3. Helps you to create dummy objects with using callable during testing.
  4. Simply you can create and test class objects on the fly ✈️ and use anywhere πŸ’ƒπŸ».
For more information please refer attr's official documentation.
2023 Β· Akarsh Jain