Skip to content

Instantly share code, notes, and snippets.

@ecridge
Last active September 1, 2017 09:56
Show Gist options
  • Save ecridge/1af198ee6d5608af7ea4ba53a4faad38 to your computer and use it in GitHub Desktop.
Save ecridge/1af198ee6d5608af7ea4ba53a4faad38 to your computer and use it in GitHub Desktop.
Some personal notes on Python 3 after spending too long with ES6.

Python notes

Some personal notes on Python 3 after spending too long with ES6. YMMV.

Strings and lists

You can reverse a string or list using colon notation:

>>> s = 'This is a string.'
>>> s[::-1]
'.gnirts a si sihT'
>>> L = [s, 1, 2.4]
>>> L[::-1]
[2.4, 1, 'This is a string.']

You can concatenate strings and lists using the + operator:

>>> s1 = 'Hello'
>>> s2 = ' world'
>>> s1 + s2
'Hello world'
>>> L1 = [0, 1]
>>> L2 = ['a', 'b']
>>> L1 + L2
[0, 1, 'a', 'b']

You can also use the * operator on strings and lists:

>>> s = 'Abc'
>>> s * 2
'AbcAbc'
>>> L = [1, 'z', 2.3]
>>> L * 2
[1, 'z', 2.3, 1, 'z', 2.3]
>>> 2 * L
[1, 'z', 2.3, 1, 'z', 2.3]

Lists are mutable, strings are not.

Tuples and sets

Tuples are like lists, but immutable.

Sets are a bit like a list–dictionary hybrid. They are an unordered list of unique items. They have all the nice mathematical operations that their name would imply.

The comparison operators are also implemented for sets, so for two sets A and B, you have the choice of e.g. A <= B or A.issubset(B).

Truthy and falsey values

The following values are falsey:

  • False
  • None
  • Zero-valued numerics (0, 0.0, 0j, ...).
  • Empty sequences ('', (), [], ...).
  • Empty mappings ({}, ...).
  • Instances of classes where __nonzero__() or __len__() are defined, when that method returns 0 or False.

Loops

You can use else as a sort ‘finally’ block on a for or while loop: it will get called after the final iteration of the look, but not if you break out from within the loop! (Unlike a true finally...)

You can also use an else block under a try block: it will run before finally if no exception was thrown. The use case for this is that you have additional logic that needs to be performed before the finally block if the first part was successful, but you don’t want to catch exceptions due to that additional logic. Or (if there is no finally): you have some clean-up code to run after the first part succeeds, but don’t need to run it when the first part fails.

Lambdas

The syntax is a bit odd, but otherwise it’s like ES6 arrow functions:

lambda x: x[0]  # Like `x => x[0]` in ES6.

Note that Python’s equivalent of the ternary operator is the conditional expression:

# This is a silly example, but hey.
lambda x: x if (x >= 0) else 0  # Parens for readability.

Reduce

Reduce in Python is a bit different to reduce in other languages. Unlike map() and filter(), you have to import it to use it:

from functools import reduce

It takes a binary function, and repeatedly acts upon the first pair of items in a sequence to reduce them to one value, stopping when there are no values left. For example, if a function <> is applied to [a, b, c, d, e], the reduced result is

(((a <> b) <> c) <> d) <> e

It can be helpful to visualise this as a tree:

a   b
 \ /
  *   c
   \ /                    x   y
    *   d         Key:     \ /     z = x <> y
     \ /                    z
      *    e
       \  /
      result

The calling syntax is reduce(function, sequence[, initial]), where the initial value (if present) acts as a virtual item before the start of the sequence.

args ’n’ kwargs

When defining a function, *args collects excess positional arguments and packs them into a tuple called args (similar to ...rest in ES6).

**kwargs collects keyword arguments and packs them into a dictionary called kwargs.

>>> def foo(a, b, *args, **kwargs):
...     print(a)
...     print(b)
...     print(args)
...     print(kwargs)
...
>>> foo(1, 2, 3, 4, 5, x=6, y=7)
1
2
(3, 4, 5)
{'x': 6, 'y': 7}

When calling a function, the *tuple and **dictionary operators unpack their respective objects into positional and keyword arguments (though note that this is fragile if the function being called isn’t defined with *args or **kwargs, respectively, because it won’t be prepared for any additional arguments that might come along with the unpacking).

Scope

Essentially function scoped, like ES5: each def block creates a new scope, and each nested scope inherits from the enclosing def scope, until you reach the global namespace. Note that lambda also creates its own scope (not that it matters – you can’t make assignments there anyway); and class creates its own scope, but that scope doesn’t get inherited by the enclosed methods.

You can define a variable as global to make it leak all the way out to the global namespace, but why would you do that?

Additionally, list comprehensions and generator expressions get their own scope:

(x*2 for x in range(256))
[y*2 for y in range(256)]  # Sugar for list(<line above>).

# x and y are not defined here.

Dictionary comprehensions (!)

They work as you might expect, and they’re pretty cool.

>>> L = ['a', 'b', 'c']
>>> {val: i for (i, val) in enumerate(L)}
{'a': 0, 'b': 1, 'c': 2}

Namespaces

Each module gets its own ‘global namespace’ (as it would be called in ES6).

Classes

Python classes are a little quirky, and best explained via example:

# NOTE: These comments are ES6y rather than Pythonic. In Python it is
#   conventional to put the comment for a class or function _under_ the first
#   line of its definition, but that collides with the block comments here...
class Foo(object):  # First argument is superclass.
    # This is a class variable.
    #
    # It _isn’t_ in scope of the methods defined below: they need to access it
    # using either `Foo.BAR` or `self.BAR` (though note that assigning a value
    # to `self.BAR` will create a shadow on the instance rather than updating
    # the shared class value).
    BAR = 27

    # Class method.
    @classmethod
    def some_class_method(cls, other, args):
        # Class is implicitly passed as first argument.
        pass

    # Static method.
    @staticmethod
    def some_static_method(its, args):
        # No class reference is passed.
        pass

    # Constructor.
    __init__(self, qux):
        # This is an instance variable.
        self.qux = qux

    # Instance method.
    def frobnicate(self):  # All instance methods take `self` as the 1st arg.
        return self.qux ** 2

# Instances are created by ‘calling’ the class. The arguments are passed to
# `__init__`, but with the uninitialised instance being inserted before the
# rest of the arguments.
f = Foo(10)

# Instance variables are available as you would expect.
f.qux  # -> 10

# Class variables are available as you would expect...
Foo.BAR  # -> 27

# ...as well as on the individual objects (?!) [assuming no shadowing].
f.BAR  # -> 27

# Instance methods are called as you would expect (`self` passed implicitly).
f.frobnicate()  # -> 100

# Class methods can be called similarly to class variables...
Foo.some_class_method(1.23, 'abc')  # other=1.23, args='abc'
f.some_class_method(1.23, 'abc')    # Exactly the same [if no shadowing].

# Static methods are _exactly_ like class methods, the just don’t receive
# a reference to the class as implicit first arguments. _Their attachment to
# the class is purely organisational – there’s no functional benefit_.
Foo.some_static_method('dd', 88)  # its='dd', args=88
f.some_static_method('dd', 88)    # Exactly the same [if no shadowing].

# Attempting to assign to a class attribute via an instance causes shadowing.
f.BAR = 999
Foo.BAR  # -> 27

Inheritance

Again, by (half-hearted) example:

class BaseFoo(object):
    __init__(self):
        # Do whatever.
        pass

class SpecializedFoo(BaseFoo):
    __init__(self):
        BaseFoo.__init__(self)

        # Special do whatever.
        pass

    def bar(qux)
        print(qux)

sf = SpecializedFoo()

‘Special’ methods

These can be used to set how the Python built-ins interact with instances of the class.

  • __len__ determines what len() returns
  • __str__ determines what print() returns
  • There are a set of comparison operator methods (e.g. __gt__, which determines how > works), but take note that these should be implemented in an all or nothing approach [because the interpreter is technically at liberty to replace < with the logical inverse of >=, etc.].

Conventions

Modules start like this:

"""This is the example module.

This module does stuff.
"""

from __future__ import barry_as_FLUFL

__all__ = ['a', 'b', 'c']
__version__ = '0.1'
__author__ = 'Cardinal Biggles'

import os
import sys

Lines break before binary operators (like displayed equations).

‘Internal’ variables use a _leading_underscore.

Keyword conflicts use a trailing_underscore_.

Packages have lowercasenames; modules have snake_case_names; classes use PascalCase; exceptions are classes, but have an ‘error’ suffix when appropriate: PascalCaseError. To prevent subclasses from overriding attributes (whether they are methods or instance variables), give them names with __double_leading_underscores.

Single letter names are allowed (a, b, A, B), with the exception of l (lower-ell), I (upper-eye), and O (upper-oh).

The first argument to instance methods is always self; the first argument to class methods is always cls.


The presence of a docstring makes a symbol ‘public’, unless the docstring explicitly states that the symbol is not public. You can use regular comments to annotate non-public symbols, but they still come after the def:

def foo():
    """This is public..."""
    pass

def bar():
    # ...but this is not.
    pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment