mirror of
https://github.com/kickingvegas/elisp-for-python.git
synced 2025-04-16 12:30:47 -05:00
363 lines
25 KiB
Org Mode
363 lines
25 KiB
Org Mode
* Elisp Cheatsheet for Python Programmers
|
||
|
||
This document is for readers who are familiar with the Python programming language and wish to apply that knowledge to writing Emacs Lisp (Elisp). It is intended to be a “cheat sheet”/quick reference and should not be considered a primary source for either Python or Emacs APIs.
|
||
|
||
❗ This is a *work in progress*. Constructive [[https://github.com/kickingvegas/elisp-for-python/issues][feedback]] is encouraged.
|
||
|
||
* Table of Contents :TOC_3:
|
||
- [[#elisp-cheatsheet-for-python-programmers][Elisp Cheatsheet for Python Programmers]]
|
||
- [[#guiding-principles][Guiding Principles]]
|
||
- [[#collections][Collections]]
|
||
- [[#comparison-functions][Comparison Functions]]
|
||
- [[#sequence-types][Sequence Types]]
|
||
- [[#python-list-to-elisp-list][Python List to Elisp List]]
|
||
- [[#python-tuple-to-elisp-vector][Python Tuple to Elisp Vector]]
|
||
- [[#map-types][Map Types]]
|
||
- [[#python-dictionary-to-elisp-hash-table][Python Dictionary to Elisp Hash Table]]
|
||
- [[#python-dictionary-to-elisp-association-list-alist][Python Dictionary to Elisp Association List (alist)]]
|
||
- [[#python-dictionary-to-elisp-property-list-plist][Python Dictionary to Elisp Property List (plist)]]
|
||
- [[#looping][Looping]]
|
||
- [[#python-string-to-elisp-string][Python String to Elisp String]]
|
||
- [[#file-io][File I/O]]
|
||
- [[#license][License]]
|
||
|
||
* Guiding Principles
|
||
- This document uses Python 3.12 and Emacs Lisp 29.1+ APIs from built-in packages.
|
||
- Python code translated to Elisp emphasizes using generic (aka [[https://en.wikipedia.org/wiki/Polymorphism_(computer_science)#:~:text=A%20function%20that%20can%20evaluate,which%20such%20specializations%20are%20made.][polymorphic]]) functions.
|
||
- This lowers the cognitive load of working with different Elisp sequence types (*list*, *vector*) and map types (*hash-table*, *alist*).
|
||
- The Emacs packages ~seq.el~ and ~map.el~ do the heavy lifting here for sequence and map types respectively.
|
||
- This document aims to provide guidance for a proficient Python programmer to implement Elisp code using familiar programming abstractions without surprise.
|
||
- Performance is at best a tertiary concern.
|
||
|
||
* Collections
|
||
** Comparison Functions
|
||
In Elisp, the comparison function used to disambiguate elements in a sequence-type collection or keys in a map-type collection is /significant/. When in doubt, it is better to specify the comparison function than trust (hope) that the default comparison function will behave to developer intent. [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Equality-Predicates.html][Info (elisp) Equality Predicates]] details the built-in comparison functions.
|
||
|
||
That said, here is some general guidance. Use the comparison function:
|
||
- ~eql~ if you want to compare numbers or symbols.
|
||
- ~equal~ if you want to compare strings.
|
||
|
||
Depending on the type of object to compare, you may have to resort to writing a custom comparison function.
|
||
|
||
Python shields you from this implementation detail. Unfortunately Elisp does not.
|
||
|
||
** Sequence Types
|
||
*** Python List to Elisp List
|
||
|
||
The Elisp *list* type is a /linked-list/ data structure. Elisp analogs to the Python *list* methods to handle insertion, appending, and updating are left to the developer to implement. Arguably, the omission of these functions is reluctance on the part of Emacs Core to make the trade-off design decisions required to implement them.
|
||
|
||
#+begin_example
|
||
s: list
|
||
x: any
|
||
t: list
|
||
n: integer
|
||
i: integer
|
||
j: integer
|
||
cmp: comparison function
|
||
#+end_example
|
||
|
||
❗All Elisp translations will not mutate the original input unless noted.
|
||
|
||
| Python | Elisp | Notes |
|
||
|--------------------+--------------------------------------------+----------------------------------------------|
|
||
| ~s = []~, ~s = list()~ | ~(setq s (list))~ | |
|
||
| ~list(range(0, n))~ | ~(number-sequence 0, (1- n))~ | |
|
||
| ~x in s~ | ~(seq-contains-p s x #'cmp)~ | Make sure ~cmp~ will compare the element type! |
|
||
| ~x not in s~ | ~(not (seq-contains-p s x #'cmp))~ | Make sure ~cmp~ will compare the element type! |
|
||
| ~s + t~ | ~(seq-concatenate 'list s t)~ | |
|
||
| ~s * n~ or ~n * s~ | ~(seq-map (lambda (a) (* n a)) s)~ | |
|
||
| ~s[i]~ | ~(seq-elt s i)~ | |
|
||
| ~s[i:j]~ | ~(seq-subseq s i j)~ | |
|
||
| ~s[i:j:k]~ | | |
|
||
| ~len(s)~ | ~(seq-length s)~ | |
|
||
| ~min(s)~ | ~(seq-min s)~ | Elements of ~s~ must be orderable. |
|
||
| ~max(s)~ | ~(seq-max s)~ | Elements of ~s~ must be orderable. |
|
||
| ~s.index(x)~ | ~(seq-position s x)~ | |
|
||
| ~s.count(x)~ | ~(seq-count (lambda (a) (= x a)) s)~ | |
|
||
| ~s[0]~ | ~(seq-first s)~ | |
|
||
| ~s[-n]~ | ~(seq-first (seq-subseq s -n))~ | |
|
||
| ~if not s:~ | ~(seq-empty-p s)~ | |
|
||
|--------------------+--------------------------------------------+----------------------------------------------|
|
||
| ~s[i] = x~ | | |
|
||
| ~s[i:j] = t~ | | |
|
||
| ~del s[i:j]~ | | |
|
||
| ~del s[i]~ | ~(seq-remove-at-position s i)~ | |
|
||
| ~s[i:j:k] = t~ | | |
|
||
| ~del s[i:j:k]~ | | |
|
||
| ~s.append(x)~ | | |
|
||
| ~s.clear()~ | ~(setq s (list))~ | |
|
||
| ~s.copy()~ | ~(seq-copy s)~ | |
|
||
| ~s.extend(t)~ | ~(append s t)~ | |
|
||
| ~s *=n~ | ~(seq-map (lambda (a) (* n a)) s)~ | |
|
||
| ~s.insert(i, x)~ | | |
|
||
| ~s.pop()~ | ~(pop s)~ | ~s~ is mutated. |
|
||
| ~s.insert(0, x)~ | ~(push s x)~ | ~s~ is mutated. |
|
||
| ~s.remove(x)~ | ~(seq-remove (lambda (a) (= x a)) s)~ | |
|
||
| ~s.reverse()~ | ~(seq-reverse s)~, ~(reverse s)~, ~(nreverse s)~ | ~nreverse~ will destructively mutate ~s~. |
|
||
|
||
*** Python Tuple to Elisp Vector
|
||
|
||
The closest Elisp analog to a Python *tuple* is a *vector*. They both model immutable sequences.
|
||
|
||
#+begin_example
|
||
s: tuple/vector
|
||
x: any
|
||
t: tuple/vector
|
||
n: integer
|
||
i: integer
|
||
j: integer
|
||
cmp: comparison function
|
||
#+end_example
|
||
|
||
|
||
| Python | Elisp | Notes |
|
||
|------------------------------------+-----------------------------------------------------+----------------------------------------------|
|
||
| ~s = (1, 2, 3)~, ~s = tuple(range(3))~ | ~(setq s (vector 1 2 3))~, ~(setq s [1 2 3])~ | |
|
||
| ~x in s~ | ~(seq-contains-p s x #'cmp)~ | Make sure ~cmp~ will compare the element type! |
|
||
| ~x not in s~ | ~(not (seq-contains-p s x #'cmp))~ | Make sure ~cmp~ will compare the element type! |
|
||
| ~s + t~ | ~(seq-concatenate 'vector s t)~ | |
|
||
| ~s * n~ or ~n * s~ | ~(seq-into (seq-map (lambda (a) (* n a)) s) 'vector)~ | |
|
||
| ~s[i]~ | ~(seq-elt s i)~ | |
|
||
| ~s[i:j]~ | ~(seq-subseq s i j)~ | |
|
||
| ~s[i:j:k]~ | | |
|
||
| ~len(s)~ | ~(seq-length s)~ | |
|
||
| ~min(s)~ | ~(seq-min s)~ | Elements of ~s~ can be ordered. |
|
||
| ~max(s)~ | ~(seq-max s)~ | Elements of ~s~ can be ordered. |
|
||
| ~s.index(x)~ | ~(seq-position s x)~ | |
|
||
| ~s.count(x)~ | ~(seq-count (lambda (a) (= x a)) s)~ | |
|
||
| ~s[0]~ | ~(seq-first s)~ | |
|
||
| ~s[-n]~ | ~(seq-first (seq-subseq s -n))~ | |
|
||
| ~if not s:~ | ~(seq-empty-p s)~ | |
|
||
|
||
** Map Types
|
||
*** Python Dictionary to Elisp Hash Table
|
||
|
||
The Elisp ~hash-table~ is the most straightforward analog to a Python dictionary. That said, there are gotchas, particularly around ~hash-table~ creation. If the keys are of type *string*, then the key comparison should be set to the function ~equal~ via the ~:test~ slot. If ~:test~ is omitted the default function ~eql~ is used which compares numbers.
|
||
|
||
#+begin_example
|
||
d: dictionary/hash-table
|
||
k: key
|
||
v: value
|
||
cmp: comparison function
|
||
#+end_example
|
||
|
||
| Python | Elisp | Notes |
|
||
|--------------------+----------------------------------------+------------------------------------------|
|
||
| ~d = dict()~, ~d = {}~ | ~(setq d (make-hash-table :test #'cmp))~ | If ~:test~ is omitted, default ~cmp~ is ~eql~. |
|
||
| ~list(d)~ | ~(map-keys d)~ | |
|
||
| ~len(d)~ | ~(map-length d)~ | |
|
||
| ~d[k]~ | ~(map-elt d k)~ | |
|
||
| ~d[k] = v~ | ~(map-put! d k v)~ | |
|
||
| ~del d[k]~ | ~(map-delete d k)~ | |
|
||
| ~k in d~ | ~(map-contains-key d k)~ | |
|
||
| ~k not in d~ | ~(not (map-contains-key d k))~ | |
|
||
| ~iter(d)~ | | |
|
||
| ~d.clear()~ | ~(clrhash d)~ | |
|
||
| ~d.copy()~ | ~(map-copy d)~ | |
|
||
| ~d.get(k)~ | ~(map-elt d k)~ | |
|
||
| ~d.items()~ | ~(map-pairs d)~ | |
|
||
| ~d.keys()~ | ~(map-keys d)~ | |
|
||
| ~d.pop(k)~ | | |
|
||
| ~d.popitem()~ | | |
|
||
| ~reversed(d)~ | | |
|
||
| ~d.values()~ | ~(map-values d)~ | |
|
||
| | ~(map-insert d k v)~ | Like ~map-put!~ but does not mutate ~d~. |
|
||
|
||
*** Python Dictionary to Elisp Association List (alist)
|
||
|
||
An *alist* is a convention to construct a basic list such that key-value semantics can be applied to it. An *alist* is allowed to possess degenerate keys (that is, keys are not necessarily unique!). This is because in truth, an *alist* is still a list with no actual enforcement of how (key, value) pairs are stored in it. IMHO Elisp *alists* are an abomination, albeit a pragmatic one. Conventional Elisp wisdom arguing for *alist* usage boils down to convenient serialization and the notion that in practice, *alist* sizes are small enough to not merit the overhead of using hash-tables.
|
||
|
||
Regardless, my guidance is to exercise caution when translating Python dictionary code to an Elisp *alist*.
|
||
|
||
#+begin_example
|
||
d: dictionary/alist
|
||
k: key
|
||
v: value
|
||
#+end_example
|
||
|
||
|
||
| Python | Elisp | Notes |
|
||
|--------------------+------------------------------+-----------------------------------------------------------------------|
|
||
| ~d = dict()~, ~d = {}~ | ~(setq d (list))~ | |
|
||
| ~list(d)~ | ~(map-keys d)~ | |
|
||
| ~len(d)~ | ~(map-length d)~ | |
|
||
| ~d[k]~ | ~(map-elt d k)~ | Type-specific behavior requires specifying test function ~t~. |
|
||
| ~d[k] = v~ | ~(map-put! d k v)~ | This only works if ~d~ is not nil. To initialize use ~(push '(k . v) d)~. |
|
||
| ~del d[k]~ | ~(setq d (map-delete d k))~ | Type-specific behavior is dependent on key type. 😞 |
|
||
| ~k in d~ | ~(map-contains-key d k)~ | |
|
||
| ~k not in d~ | ~(not (map-contains-key d k))~ | |
|
||
| ~iter(d)~ | | |
|
||
| ~d.clear()~ | ~(setq d (list))~ | |
|
||
| ~d.copy()~ | ~(map-copy d)~ | |
|
||
| ~d.get(k)~ | ~(map-elt d k)~ | |
|
||
| ~d.items()~ | ~(map-pairs d)~ | |
|
||
| ~d.keys()~ | ~(map-keys d)~ | |
|
||
| ~d.pop(k)~ | | |
|
||
| ~d.popitem()~ | | |
|
||
| ~reversed(d)~ | | |
|
||
| ~d.values()~ | ~(map-values d)~ | |
|
||
| | ~(map-insert d k v)~ | Like ~map-put!~ but does not mutate ~d~. Also does not check uniqueness. |
|
||
|
||
*** Python Dictionary to Elisp Property List (plist)
|
||
|
||
TBD
|
||
|
||
*** Looping
|
||
|
||
Two functions which can iterate through a map are ~map-do~ and ~map-apply~. Shown below are the Python translated equivalents.
|
||
|
||
#+begin_src elisp :lexical no
|
||
(map-do f d) ; return nil
|
||
#+end_src
|
||
|
||
#+begin_src python
|
||
def map_do(d):
|
||
for k,v in d.items():
|
||
f(k, v)
|
||
#+end_src
|
||
|
||
#+begin_src elisp :lexical no
|
||
(map-apply f d) ; return results of f applied to each element of d as a list
|
||
#+end_src
|
||
|
||
#+begin_src python
|
||
def map_apply(d):
|
||
results = []
|
||
for k,v in d.items():
|
||
results.append(f(k, v))
|
||
return results
|
||
#+end_src
|
||
|
||
|
||
|
||
** Python String to Elisp String
|
||
|
||
#+begin_example
|
||
s: string
|
||
a: string
|
||
b: string
|
||
c: string
|
||
sep: separator string
|
||
strs: list of strings
|
||
#+end_example
|
||
|
||
|
||
| Python | Elisp | Notes |
|
||
|-------------------------------+------------------------------------+-------------------------------|
|
||
| ~""~ | ~(make-string 0 ? )~ | |
|
||
| ~a + b + c~ | ~(concat a b c)~ | |
|
||
| ~s.strip()~ | ~(string-clean-whitespace s)~ | |
|
||
| ~s.capitalize()~ | ~(capitalize s)~ | |
|
||
| ~s.casefold()~ | | |
|
||
| ~s.center(width)~ | | |
|
||
| ~s.count(sub)~ | | |
|
||
| ~s.encode(encoding)~ | | |
|
||
| ~s.endswith(suffix)~ | ~(string-suffix-p suffix s)~ | |
|
||
| ~s.expandtabs(tabsize)~ | | |
|
||
| ~s.find(sub)~ | ~(string-search sub s)~ | |
|
||
| ~s.format(*args, **kwargs)~ | ~(format fmt args…)~ | |
|
||
| ~s.index(sub)~ | ~(string-search sub s)~ | |
|
||
| ~s.isalnum()~ | ~(string-match "^[[:alnum:]]*$" s)~ | |
|
||
| ~s.isalpha()~ | ~(string-match "^[[:alpha:]]*$" s)~ | |
|
||
| ~s.isascii()~ | ~(string-match "^[[:ascii:]]*$" s)~ | |
|
||
| ~s.isdecimal()~ | | |
|
||
| ~s.isdigit()~ | ~(string-match "^[[:digit:]]*$" s)~ | |
|
||
| ~s.islower()~ | ~(string-match "^[[:lower:]]*$" s)~ | ~case-fold-search~ must be nil. |
|
||
| ~s.isnumeric()~ | | |
|
||
| ~s.isprintable()~ | ~(string-match "^[[:print:]]*$" s)~ | |
|
||
| ~s.isspace()~ | ~(string-match "^[[:space:]]*$" s)~ | |
|
||
| ~s.istitle()~ | | |
|
||
| ~s.isupper()~ | ~(string-match "^[[:upper:]]*$" s)~ | ~case-fold-search~ must be nil. |
|
||
| ~sep.join(strs)~ | ~(string-join strs sep)~ | |
|
||
| ~s.ljust(width)~ | | |
|
||
| ~s.lower()~ | ~(downcase s)~ | |
|
||
| ~s.lstrip()~ | ~(string-trim-left s)~ | |
|
||
| ~s.removeprefix(prefix)~ | ~(string-remove-prefix prefix s)~ | |
|
||
| ~s.removesuffix(suffix)~ | ~(string-remove-suffix suffix s)~ | |
|
||
| ~s.replace(old, new, count=-1)~ | ~(string-replace old new s)~ | |
|
||
| ~s.rfind(sub)~ | | |
|
||
| ~s.rindex(sub)~ | | |
|
||
| ~s.rjust(width)~ | | |
|
||
| ~s.rsplit(sep)~ | | |
|
||
| ~s.rstrip()~ | ~(string-trim-right s)~ | |
|
||
| ~s.split(sep)~ | ~(split-string s sep)~ | |
|
||
| ~s.splitlines()~ | ~(string-lines s)~ | |
|
||
| ~s.startswith(prefix)~ | ~(string-prefix-p prefix s)~ | |
|
||
| ~s.strip()~ | ~(string-trim s)~ | |
|
||
| ~s.swapcase()~ | | |
|
||
| ~s.title()~ | ~(upcase-initials s)~ | |
|
||
| ~s.upper()~ | ~(upcase s)~ | |
|
||
| ~s.zfill(width)~ | | |
|
||
|
||
|
||
* File I/O
|
||
|
||
The in-memory representation of a file in Emacs is a *buffer*, whose closest analog in a general purpose language like Python is a *file handle*. A common pattern is to read the contents of a file into list of strings, each string separated by a newline ("\n").
|
||
|
||
Here is an example of this in Python.
|
||
#+begin_src python
|
||
def read_file_lines(filename):
|
||
with open(filename, "r") as infile:
|
||
lines = infile.readlines()
|
||
return lines
|
||
|
||
for line in read_file_lines(filename):
|
||
print(line.rstrip('\n'))
|
||
#+end_src
|
||
|
||
Here is an Elisp equivalent.
|
||
#+begin_src elisp :lexical no
|
||
(defun read-file-lines (filename)
|
||
"Load FILENAME into a buffer and read each line."
|
||
(with-temp-buffer
|
||
;; Insert the contents of the file into the temporary buffer
|
||
(insert-file-contents filename)
|
||
;; Move to the beginning of the buffer
|
||
(goto-char (point-min))
|
||
;; Initialize an empty list to hold the lines
|
||
(let ((lines '()))
|
||
;; Loop until the end of the buffer is reached
|
||
(while (not (eobp))
|
||
;; Read the current line
|
||
(let ((line (string-trim-right (thing-at-point 'line t))))
|
||
;; Add the line to the list
|
||
(push line lines))
|
||
;; Move to the next line
|
||
(forward-line 1))
|
||
;; Return the lines in the correct order
|
||
(nreverse lines))))
|
||
|
||
;; Example usage:
|
||
(let ((lines (read-file-lines "somefile.log")))
|
||
(dolist (line lines)
|
||
(message "%s" line)))
|
||
#+end_src
|
||
|
||
Writing an Elisp list to a file is illustrated in the following example.
|
||
|
||
#+begin_src elisp :lexical no
|
||
(defun write-strings-to-file (strings filename)
|
||
"Write a list of STRINGS to FILENAME, one string per line."
|
||
(with-temp-file filename
|
||
;; Iterate over each string in the list
|
||
(dolist (str strings)
|
||
;; Insert the string followed by a newline character
|
||
(insert str "\n"))))
|
||
|
||
;; Example usage:
|
||
(let ((my-strings (read-file-lines "somefile.log"))
|
||
(file-path "some-other-file.log"))
|
||
(write-strings-to-file my-strings file-path))
|
||
#+end_src
|
||
|
||
Although the above examples work as advertised, conventional Elisp wisdom frowns upon pipeline style processing of collections arguing that:
|
||
|
||
1. Elisp has been optimized to work in-place with buffer contents and that transformations should be made directly to the buffer content.
|
||
2. Pipeline style processing of collections is slow. If you are going to process a large log file, using Elisp is not the right tool for the job.
|
||
|
||
It is left to the reader whether to heed this guidance.
|
||
|
||
* License
|
||
[[https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by.svg]]\\
|
||
© 2025. This work is openly licensed via [[https://creativecommons.org/licenses/by/4.0/][CC BY 4.0]].
|