Summary of Python tips, tricks, and to-dos
These pointers are what I picked up from the book Python Tricks: The Book
The book itself is a summary, and here i’ll be doing a summary of a summary.
Python is great because of its flexibility, but that itself could potentially be a double edged sword. It can be so easy to abuse and write really messy code, yet the program still runs fine.
Lets talk about the points made in the book. I only picked out points that I feel that are useful and that I have very little exposure to. Don’t get me wrong, all the points in the book are great, just some greater than others.
Assertions
If the asserted condition returns true, nothing happens.
If the asserted condition returns false, AssertionError
exception is raised.
def price_after_discount(0ld_price, discount): new_price = 0ld_price * discount assert 0 <= new_price <= old_price
This block of code applies a discount the an item. We assert that the new price is greater than zero, and not more than the old price.
Assert is different from a regular exception in that it’s meant for unrecoverable errors. Recoverable errors are things like File not found
, where you can fix it (by putting the file where it should be) and try to run the program again. Asserts are meant for internal sanity checking.
Don’t use Assert for data validation, because it can be optimized away.
def delete_product(prod_id, user): assert user.is_admin() assert store.has_product(prod_id) 'Unknown product' store.get_product(prod_id).delete()
When you optimize away asserts, we remove checking if the user is admin, or if the store has the product.
Context Managers
When you do OO in python and you create classes to use, you can set context managers that dictate what happens when you enter and exit the code.
This is done by defining __enter__
and __exit__
functions
class ManagedFile: def __init__(self, name): self.name = name def __enter__(self): self.file = open(self.name, 'w') return self.file def __exit__(self, exc_type, exc_val, exc_tb): if self.file: self.file.close()
__enter__
is called when the execution enters the context of the statement, and __exit__
is called when it leaves the context.
Underscores and Dunders
On naming the variables in python, each name has a different meaning:
- SingleLeadingUnderscore:
_var
Purely conventional, this tells the reader that the variable is only meant for use internal to the function. - SingleTrailingUnderscore:
var_
Purely conventional, putting an underscore at the back prevents naming conflicts with Python’s keywords
- DoubleLeadingUnderscore:
__var
When double underscores are infront, Python name-mangles the variable, and puts the class name in front of it.
class Test: def __init__(self): self.foo = 11 self._bar = 23 self.__baz = 42
When you look at the attributes of object
Test
, we see that__baz
has become_Test__baz
>>> t = Test() >>> dir(t) ['_Test__baz', '__class__', '__delattr__' ... ]
This is done to protect the variable from being overridden in subclasses that extends from the parent class
- DoubleLeadingandTrailingUnderscore:
__var__
Leading and trailing underscores are left untouched by Python. They are reserved for special usage in Python, such as
__init__
and__call__
- SingleUnderscore:
_
Meant to represent a variable that is temporary and insignificant
for _ in range(5): print("Hello World)
_
also represents the last value of the Python interpreter session
String Formatting
Old method: "Hello, %s" % name
New method: "Hello, {}".format(name)
The new method is more powerful, because the order in format
doesn’t matter
'Hey {name}, there is a 0x{errno:x} error!'.format(errno=errno, name=name)
Python Functions
Python’s functions are first class objects.
What this means is that they can be assigned to variables, stored in data structures, and passed as arguements
>>> funcs = [bark, str.lower, str.capitalize] >>> funcs [<function yell at 0x10ff96510>, <method 'lower' of 'str' objects>, <method 'capitalize' of 'str' objects>]
Lambdas
Lambdas declare small anonymous functions. It’s a declarative way of programming
>>> add = lambda x, y: x + y >>> add(5, 3) 8
The syntax: lambda x, y
are the inputs. x + y
is the action to carry out and return.
A more complete example:
>>> tuples = [(1, 'd'), (2, 'b'), (4, 'a'), (3, 'c')] >>> sorted(tuples, key=lambda x: x[1]) [(4, 'a'), (2, 'b'), (3, 'c'), (1, 'd')]
The tuple is passed into the lambda function, and it returns the second element, which is assigned to key. The output is then sorted according to the second value.
Decorators
Decorators let you modify the behavior of the callee, without modifying the callee’s code itself.
Some common use case for decorators are:
- Logging
- User authentication
def uppercase(func): def wrapper(): original_result = func() modified_result = original_result.upper() return modified_result return wrapper @uppercase def greet(): return 'Hello!' >>> greet() 'HELLO!'
When we put the decorator on greet()
, we are passing the function to our decorator function.
The output is then gotten from the decorator
Decorators are done bottom to top
@strong @emphasis def greet(): return 'Hello!
emphasis
is executed first, before strong
Decorators can also accept arguments by using args
and kwargs
. The arguments are gotten from the original function, and passed to the decorators.
def proxy(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper
*args and **kwargs
*args
and **kwargs
are optional arguments to a function.
def foo(required, *args, **kwargs): print(required) if args: print(args) if kwargs: print(kwargs) >>> foo() TypeError: "foo() missing 1 required positional arg: 'required'" >>> foo('hello') hello >>> foo('hello', 1, 2, 3) hello (1, 2, 3) >>> foo('hello', 1, 2, 3, key1='value', key2=999) hello (1, 2, 3) {'key1': 'value', 'key2': 999}
*args
collects extra positional arguments
**kwargs
collects extra keywords as a dictionary
Writing your own exception class
class NameTooShortError(ValueError): pass def validate(name): if len(name) < 10:: raise NameTooShortError(name)
References, Shallow Copying and Deep Copying
References
new_list = original_list new_dict = original_dict new_set = original_set
This just creates references, and any modifications done to original_
will also modify new_
>>> xs [[1, 2, 3], [4, 5, 6], [7, 8, 9]] >>> ys [[1, 2, 3], [4, 5, 6], [7, 8, 9]] >>> xs.append("Hello") >>> xs [[1, 2, 3], [4, 5, 6], [7, 8, 9], "Hello"] >>> ys [[1, 2, 3], [4, 5, 6], [7, 8, 9], "Hello"]
Shallow Copying
new_list = list(original_list) new_dict = dict(original_dict) new_set = set(original_set)
This makes a new list, but the children objects in the list are not copied.
>>> xs [[1, 2, 3], [4, 5, 6], [7, 8, 9]] >>> ys [[1, 2, 3], [4, 5, 6], [7, 8, 9]] >>> xs.append("Hello") >>> xs [[1, 2, 3], [4, 5, 6], [7, 8, 9], "Hello"] >>> ys [[1, 2, 3], [4, 5, 6], [7, 8, 9]] >>> xs[1][0] = "X" >>> xs [[1, 2, 3], ['X', 5, 6], [7, 8, 9], "Hello"] >>> ys [[1, 2, 3], ['X', 5, 6], [7, 8, 9]]
Deep Copying
new_list = copy.deepcopy(original_list) new_dict = copy.deepcopy(original_dict) new_set = copy.deepcopy(original_set)
This creates an entirely new instance, and copies all the children too.
>>> xs [[1, 2, 3], [4, 5, 6], [7, 8, 9]] >>> ys [[1, 2, 3], [4, 5, 6], [7, 8, 9]] >>> xs.append("Hello") >>> xs [[1, 2, 3], [4, 5, 6], [7, 8, 9], "Hello"] >>> ys [[1, 2, 3], [4, 5, 6], [7, 8, 9]] >>> xs[1][0] = "X" >>> xs [[1, 2, 3], ['X', 5, 6], [7, 8, 9], "Hello"] >>> ys [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Generators
Generators generate values JIT (Just In Time). This is opposed to making a list, and iterating through it.
genexpr = ('Hello' for i in range(3)) >>> next(genexpr) 'Hello' >>> next(genexpr) 'Hello' >>> next(genexpr) 'Hello' >>> next(genexpr) StopIteration
Leave a Reply