Beautiful Code is being nice
“A set of rules? About Code? Decided by whom? Why? What about (…)?”
If “Beautiful Code” sounds a lot like (sometimes objectionable) social norms, it is because code beauty IS a social norm. Moreover, it is a social norm within a subgroup often reknown for being unsociable. When I started programming, code beauty was just something to ignore while I was “getting things done”.
Then I read “Clean Code”. Then “The Humane Interface”. Then I started reading other people’s code at work. It’s amazing how quickly I gained appreciation for social norms once they had vanished.
My mom once told me to “be nice”; I realized then that it was something I had to apply to code.
Some Features are not nice
The thing about using more involved language features is that I am always concerned the person after me won’t know them, or won’t readily adapt to them. For example, lambda functions are great for code locality, but in some cases, terrible for readability.
Moreover, the “magic” class attributes and functions are not always obvious to someone when they come to a language.
def get_login_user_info(user): if user.is_logged_in(): with SqlConnectionClass() as conn: conn.execute("select foo from bar where ~~~~")
Someone unfamiliar with Python is going to come to this and be confused by parts of the syntax. The “with” statement has multiple “magic” class functions (”enter” and ”exit”) which are called when you run a function. Maybe a future programmer is going to read that as a class init and make some changes in init and get frustrated when conn does not necessarily return what they think should be returned. At this point, they will look up the details on Stackoverflow, and hopefully get familiar with “context managers”.
An educated person shouldn’t necessarily hold back due to the needs of an ignorant person. However, I can see the potential confusion that may arise if this pattern continues. having multiple contexts make it visually unclear where and when they end; moreover, this function has multiple roles, some of which should be stripped out and used across many different functions.
PyCharm helps with the confusion, but I think there is a better way.
Decorators Make Things Beautiful
“…we need to make sure that the statements within our function are
all at the same level of abstraction.” – Robert C. Martin
Decorators make things easier to understand.
def user_login_required(fn): def validate_login_fn(*args, **kwargs): if "user_id" in kwargs and "password" in kwargs: if check_login(user_id=kwargs["user_id"], password=kwargs["password"]): return fn(user_details=get_details(kwargs.get("user_id"), *args, **kwargs) raise ValueError("Invalid user_id or password") raise KeyError("You must pass the user_id and password as keyword arguments") return validate_fn def db_cursor_required(fn): def add_db_cursor(*args, **kwargs): # Automatically closes the connection when the function finishes executing with SqlConnectionCursor() as cursor: # Automatically closes the cursor when this closes. with cursor: return fn(cursor=cursor, *args, **kwargs) else: return fn(*args, **kwargs) return add_db_cursor
@user_login_required @db_cursor_required def get_user_foobar(*args, **kwargs): return kwargs["cursor"].execute("select foo from bar where ~~~~")
This significantly more readable. A DB Connection is required. A User Login is required. All aspects related to both are dealt with directly in the decorator function, ensuring the same level of abstraction throughout. You never have to deal with the mental overhead of remembering login functions and the related etails.
 Yes, the decorator functions themselves are complicated, but they are destined to be heavily-tested building-blocks for your application. As a result, you can also put in more strenuous (read: less readable) data checks within the login code; as you only have it in one place, it isn’t asking too much to learn it once, as opposed to learning it over and over again.
Incidentally, adding in another layer is not necessarily difficult either, although it may change the variables you need to pass the function.
@user_login_required @memcache_check @db_cursor_required def get_user_foobar(*args, **kwargs): return kwargs["cursor"].execute("select foo from bar where ~~~~")
Decorators are Nice
The first rule of functions is that they should be small. The second
rule of functions is that they should be smaller than that. – Robert
I firmly believe that learning a code base becomes easier with decorators, for many reasons.
- Decorators enforce consistency; you always pass “user_id” and “password” (for example); not doing so raises an exception.
- Decorators force you to use keyword arguments; I ALWAYS find them significantly more readable than the lack. (The people who argue against that are usually highly skilled and experienced programmers – who have never had to follow a poorly designed application built by someone before them)
- There is a logical place to start “learning” the code; figure out the decorators, which are always going to be a major aspect of the setup of the code.
- Each function can concentrate entirely on one thing (in this case: performing the DB call). If the data being returned is incorrect, you will fix the get_user_foobar function. If the database is not connecting properly, you will fix the db_cursor_required decorator function. If the login data is not being perused correctly, you fix the user_login_required function.
If you never used decorators, or find functional programming to be needlessly complex or unwieldy, you may not “like” this. Dynamically modifying functions is not a cheap process either, computationally-speaking. However, I am sure you will agree that the end function is much smaller. If we can agree on a social contract based on simplifying code and making it easier to read, Decorator are necessary.