Fluent Python Notes

A work in progress collection of my notes on what I learned while reading Fluent Python by Luciano Ramalho.

Chapter 3: Dictionaries and Sets

  • Mappings (think dicts) can be merged into a new mapping using the | operator

    >>> dict_1 = {'a':1, 'b':2}
    >>> dict_2 = {'c':3, 'd':4}
    >>> dict_1 | dict_2
    {'a': 1, 'b': 2, 'c': 3, 'd': 4}
    
  • Mappings can be merged in place with the |= operator

    dict_1 = {'a':1, 'b':2}
    dict_2 = {'c':3, 'd':4}
    >>> dict_1 |= dict_2
    >>> dict_1
    {'a': 1, 'b': 2, 'c': 3, 'd': 4}
    
  • dict.setdefault will set the default value for the key if not in the dictionary and will return the value so it can be updated at the same time.

    >>> my_dict = {}
    >>> my_dict.setdefault("things", []).append("thing 1")
    >>> my_dict
    {'things': ['thing 1']}
    
  • A defaultdict will only create the missing value when accessed with bracket notation (my_dict["missing_key"]) and not when accessed with the .get() method.

  • MappingProxyType from the types module creates a read-only instance of the original mapping, but it is dynamically updated when the original mapping is updated.

  • The .keys(), .values(), and .items() dictionary methods all return dictionary view instances of classes dict_keys, dict_values, and dict_items. These views are read only and are dynamic proxies of the dictionary. If the dictionary is updated, instances of these views will be updated immediately.

  • Looking up values in a dict is very fast, but comes at the expense of more memory use than other data structures due to it being a hash tables.

  • "Set elements must be hashable. They set type is not hashable, so you can't build a set with nested set elements. But frozenset is hashable, so you can have frozenset elements inside a set."

  • To create an empty set, use set(). {} will create an empty dict, not an empty set.

  • Adding elements to a set may change the order of the elements.

Chapter 5: Data Class Builders

  • To set a mutable default value for a dataclass field, use default_factory which will create a new default for each instance of the dataclass instead of all instances sharing the same mutable object. dataclass will flag it if you don't do this for list, dict, or set, but won't for other mutable values.
@dataclass
class Class:
    students: list = field(default_factory=list)
  • A __post_init__ method can be added to a dataclass that will be called after __init__. This can be used to validate fields or compute field values based on other fields.

  • Referring to a data class as a code smell - "The main idea of object-oriented programming is to place behavior and data together in the same code unit: a class. If a class is widely used but has no significant behavior of its own, it's possible that code dealing with its instances is scattered (and even duplicated) in methods and functions throughout the system -- a recipe for maintenance headaches. That's why Fowler's refactorings to deal with a data class involve bringing responsibilities back into it.

Chapter 6: Object References, Mutability, and Recycling

  • Variables are assigned to objects, not objects to variables. Instead of saying an object was assigned to a variable, it's more accurate to say a variable is bound to an object.

  • Objects must exist before a variable name can be bound to it.

  • The only common use case for the is operator is checking if something is None. Most of the time opt for == instead of is since == also works with checking for None.

  • Tuples are containers so they hold references to objects. Tuples are only immutable in that you can't change which objects are referenced to, but if those references are to mutable objects, then you can change those objects. For example, if a tuple contains a list, you can't assign a different list to the tuple, but you can modify the list that is referenced in the tuple.

  • Python parameter passing is "call by sharing". Each parameter of a function get a copy of each reference in the arguments. The parameters are aliases of the actual arguments. A function can change a mutable object passed as a parameter but it can't replace immutable objects. This means, for example, that if a list is passed to a function, any changes to the list in the function will affect the list outside of the function. But any changes to an immutable object won't affect the object outside of the function.

  • Default mutable values for function parameters is a bad idea. Each default value is evaluated when the function is defined. This means that every call of the function will use the same object the parameter references. If the parameter is modified in one function call, the next function call will be using the same, and now modified, parameter.

  • In this example, self.players is an alias to players. players is an alias for the default list when no argument is given.

class Team:

    def __init__(self, players=[]):
        self. players = players
  • Instead of setting a parameter to a default mutable value, set the default value to None then in __init__ check if the parameter is None. If yes, set the attribute to the desired default.

  • When a mutable argument is passed to a function, unless you want any changes made to the object to appear outside of the function, you should make a copy of the object when assigning it to the instance variable.

  • A complete example of proper handling of mutable arguments

class Team:

    def __init__(self, players=None):
        if players is None:
            self.players = []
        else:
            self. players = list(player) # Makes a copy of players
  • del deletes references to objects and not the actual object. An object will be garbage collected when there are no more references to it.

  • For a tuple t, tuple(t) and t[:] return a reference to t and not a copy.

Chapter 7: Functions as First-Class Objects

  • map and filter have mostly been replaced by using list comprehensions or generator expressions.

  • reduce is mostly replaced by the built in sum function since summing a collection of items is the most common use case.

  • There are types of callable objects: user-defined functions, built-in functions, built-in methods, methods, classes, class instances, generator functions, native coroutine functions, asynchronous generator functions.

  • Use callable() on an object to determine if it is callable.

  • If a class defines a __call__ method, then instances of the class can be called.

  • To have keyword-only arguments in a function definition with variable positional arguments, specify the keyword-only arguments after the argument that captures the variable positional ones (the argument with a *).

  • For keyword-only arguments in a function definition that doesn't have variable positional arguments, put a * by itself in the definition to separate the position from the keyword arguments.

    def new_func(name, *, location="Here", time)
    
  • Keyword-only arguments do not need to have a default value.

  • To define positional-only parameters, put a \ in the parameter list. Arguments to the left of the \ are positional only. Other arguments may be added after the \.