-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Any Python program fits in 24 characters
- Loading branch information
1 parent
15e6d18
commit 1fb24a8
Showing
6 changed files
with
117 additions
and
55 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<!doctypehtml><html prefix="og: http://ogp.me/ns#"lang=en_US><meta charset=utf-8><meta content=width=device-width,initial-scale=1 name=viewport><title>Any Python program fits in 24 characters* | purplesyringa's blog</title><link href=../../favicon.ico?v=2 rel=icon><link href=../../all.css rel=stylesheet><link href=../../blog.css rel=stylesheet><link href=../../vendor/Temml-Local.css rel=stylesheet><link crossorigin href=https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,400;0,700;1,400;1,700&family=Slabo+27px&display=swap rel=stylesheet><link href=../../fonts/webfont.css rel=stylesheet><link media="screen and (prefers-color-scheme: dark"href=../../vendor/atom-one-dark.min.css rel=stylesheet><link media="screen and (prefers-color-scheme: light"href=../../vendor/a11y-light.min.css rel=stylesheet><link title="Blog posts"href=../../blog/feed.rss rel=alternate type=application/rss+xml><meta content="Any Python program fits in 24 characters*"property=og:title><meta content=article property=og:type><meta content=https://purplesyringa.moe/blog/any-python-program-fits-in-24-characters/og.png property=og:image><meta content=https://purplesyringa.moe/blog/any-python-program-fits-in-24-characters/ property=og:url><meta content="* If you don’t take whitespace into account. | ||
My friend challenged me to find the shortest solution to a certain Leetcode-style problem in Python. They were generous enough to let me use whitespace for free, so that the code stays readable. So that’s exactly what we’ll abuse to encode any Python program in 24 bytes, ignoring whitespace."property=og:description><meta content=en_US property=og:locale><meta content="purplesyringa's blog"property=og:site_name><meta content=summary_large_image name=twitter:card><meta content=https://purplesyringa.moe/blog/any-python-program-fits-in-24-characters/og.png name=twitter:image><script data-website-id=0da1961d-43f2-45cc-a8e2-75679eefbb69 defer src=https://zond.tei.su/script.js></script><body><header><div class=viewport-container><div class=media><a href=https://github.com/purplesyringa><img alt=GitHub src=../../images/github-mark-white.svg></a></div><h1><a href=/>purplesyringa</a></h1><nav><a href=../..>about</a><a class=current href=../../blog/>blog</a><a href=../../sink/>kitchen sink</a></nav></div></header><section><div class=viewport-container><h2>Any Python program fits in 24 characters*</h2><time>November 17, 2024</time><p><em>* If you don’t take whitespace into account.</em><p>My friend challenged me to find the shortest solution to a certain Leetcode-style problem in Python. They were generous enough to let me use whitespace for free, so that the code stays readable. So that’s exactly what we’ll abuse to encode <em>any</em> Python program in <eq><math><mn>24</mn></math></eq> bytes, ignoring whitespace.<blockquote><p>This post originally stated that <eq><math><mn>30</mn></math></eq> characters are always enough. Since then, someone on the codegolf Discord server has devised a better solution, reaching <eq><math><mn>24</mn></math></eq> bytes. After a few minor modifications, it satisfies the requirements of this problem, so I publish it here too.</blockquote><p class=next-group><span aria-level=3 class=side-header role=heading><span>Bits</span></span>We can encode arbitrary data in a string by only using whitespace. For example, we could encode <code>0</code> bits as spaces and <code>1</code> bits as tabs. Now you just have to decode this.<p>As you start implementing the decoder, it immediately becomes clear that this approach requires about 50 characters at minimum. You can use <code>c % 2 for c in b"..."</code> to extract individual bits, then you need to merge bits by using <code>str</code> and concatenating then with <code>"".join(...)</code>, then you to parse the bits with <code>int.to_bytes(...)</code>, and finally call <code>exec</code>. We need to find another solution.<p class=next-group><span aria-level=3 class=side-header role=heading><span>Characters</span></span>What if we didn’t go from characters to bits and then back? What if instead, we mapped each whitespace character to its own non-whitespace character and then evaluated that?<pre><code class=language-python><span class=hljs-built_in>exec</span>( | ||
<span class=hljs-string>"[whitespace...]"</span> | ||
.replace(<span class=hljs-string>" "</span>, <span class=hljs-string>"A"</span>) | ||
.replace(<span class=hljs-string>"\t"</span>, <span class=hljs-string>"B"</span>) | ||
.replace(<span class=hljs-string>"\v"</span>, <span class=hljs-string>"C"</span>) | ||
.replace(<span class=hljs-string>"\f"</span>, <span class=hljs-string>"D"</span>) | ||
... | ||
) | ||
</code></pre><p>Unicode has quite a lot of whitespace characters, so this should be possible, in theory. Unfortunately, this takes even more bytes in practice. Under 50 characters, we can fit just two <code>replace</code> calls:<pre><code class=language-python><span class=hljs-built_in>exec</span>(<span class=hljs-string>"[whitespace...]"</span>.replace(<span class=hljs-string>" "</span>,<span class=hljs-string>"A"</span>).replace(<span class=hljs-string>"\t"</span>,<span class=hljs-string>"B"</span>)) | ||
</code></pre><p>But we don’t have to use <code>replace</code>! The less-known <code>str.translate</code> method can perform multiple single-character replaces at once:<pre><code class=language-python><span class=hljs-meta>>>> </span><span class=hljs-string>"Hello, world!"</span>.translate({<span class=hljs-built_in>ord</span>(<span class=hljs-string>"H"</span>): <span class=hljs-string>"h"</span>, <span class=hljs-built_in>ord</span>(<span class=hljs-string>"!"</span>): <span class=hljs-string>"."</span>}) | ||
<span class=hljs-string>'hello, world.'</span> | ||
</code></pre><p>The following fits in 50 characters:<pre><code class=language-python><span class=hljs-built_in>exec</span>(<span class=hljs-string>"[whitespace...]"</span>.translate({<span class=hljs-number>9</span>: <span class=hljs-string>"A"</span>, <span class=hljs-number>11</span>: <span class=hljs-string>"B"</span>, <span class=hljs-number>12</span>: <span class=hljs-string>"C"</span>, <span class=hljs-number>28</span>: <span class=hljs-string>"D"</span>}) | ||
</code></pre><p>4 characters isn’t much to work with, but here’s some good news: <code>translate</code> takes anything indexable with integers (code points). We can thus replace the dict with a string:<pre><code class=language-python><span class=hljs-built_in>exec</span>( | ||
<span class=hljs-string>"[whitespace...]"</span>.translate( | ||
<span class=hljs-string>" A BC DEFGH I J"</span> | ||
) | ||
) | ||
</code></pre><p>The characters <code>ABCDEFGHIJ</code> are located at indices <eq><math><mrow><mn>9</mn><mo separator=true>,</mo></mrow><mrow><mn>11</mn><mo separator=true>,</mo></mrow><mrow><mn>12</mn><mo separator=true>,</mo></mrow><mrow><mn>28</mn><mo separator=true>,</mo></mrow><mrow><mn>29</mn><mo separator=true>,</mo></mrow><mrow><mn>30</mn><mo separator=true>,</mo></mrow><mrow><mn>31</mn><mo separator=true>,</mo></mrow><mrow><mn>32</mn><mo separator=true>,</mo></mrow><mrow><mn>133</mn><mo separator=true>,</mo></mrow><mrow><mn>160</mn></mrow></math></eq> – all whitespace code points below <eq><math><mn>256</mn></math></eq> except CR and LF, which are invalid in a string. While this code is long, most of it is just whitespace, which we ignore. After removing whitespace, it’s only <eq><math><mn>32</mn></math></eq> characters:<pre><code class=language-python><span class=hljs-built_in>exec</span>(<span class=hljs-string>""</span>.translate(<span class=hljs-string>"ABCDEFGHIJ"</span>)) | ||
</code></pre><p>We can now encode any Python program that uses at most <eq><math><mn>10</mn></math></eq> different characters. We could now use <a href=https://github.com/kuangkzh/PyFuck>PyFuck</a>, which transforms any Python script to an equivalent script that uses only <eq><math><mn>8</mn></math></eq> characters: <code>exc('%0)</code>. This reduces the code size to <eq><math><mn>30</mn></math></eq> charaters (plus whitespace). A bit of postprocessing is necessary to get it working well, as PyFuck often has exponential output, but that’s a minor issue.<p class=next-group><span aria-level=3 class=side-header role=heading><span>A better way</span></span>But it turns out there’s another way to translate whitespace to non-whitespace.<blockquote><p>This solution was found by a reader of my blog – thanks!</blockquote><p>When <code>repr</code> is applied to Unicode strings, it replaces the Unicode codepoints with their <code>\uXXXX</code> representations. For example, <code>U+2001 Em Quad</code> is encoded as <code>'\u2001'</code>. All in all, Unicode whitespace gives us unlimited supply of <code>\</code>, <code>x</code>, and the whole hexadecimal alphabet (plus two instances of <code>'</code>).<p>Say we wanted to extract the least significant digits of characters from <code>U+2000</code> to <code>U+2007</code>. Here’s how to do this:<pre><code class=language-python><span class=hljs-comment># Imagine these \uXXXX escapes are literal whitespace characters</span> | ||
<span class=hljs-meta>>>> </span><span class=hljs-built_in>repr</span>(<span class=hljs-string>"\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007"</span>)[<span class=hljs-number>6</span>::<span class=hljs-number>6</span>] | ||
<span class=hljs-string>'01234567'</span> | ||
</code></pre><p>To get <code>\</code>, <code>x</code>, and the rest of the hexadecimal alphabet, we need characters like <code>U+000B</code> and <code>U+001F</code>. We also need to align the strings exactly, so that one of the columns contains all the alphabet:<pre><code class=language-python> v | ||
\: <span class=hljs-string>" \t "</span> | ||
x: <span class=hljs-string>" \x0b"</span> | ||
<span class=hljs-number>0</span>: <span class=hljs-string>"\u2000 "</span> | ||
<span class=hljs-number>1</span>: <span class=hljs-string>"\u2001 "</span> | ||
<span class=hljs-number>2</span>: <span class=hljs-string>"\u2002 "</span> | ||
<span class=hljs-number>3</span>: <span class=hljs-string>"\u2003 "</span> | ||
<span class=hljs-number>4</span>: <span class=hljs-string>"\u2004 "</span> | ||
<span class=hljs-number>5</span>: <span class=hljs-string>"\u2005 "</span> | ||
<span class=hljs-number>6</span>: <span class=hljs-string>"\u2006 "</span> | ||
<span class=hljs-number>7</span>: <span class=hljs-string>"\u2007 "</span> | ||
<span class=hljs-number>8</span>: <span class=hljs-string>"\u2008 "</span> | ||
<span class=hljs-number>9</span>: <span class=hljs-string>"\u2009 "</span> | ||
a: <span class=hljs-string>"\u200a "</span> | ||
b: <span class=hljs-string>" \x0b "</span> | ||
c: <span class=hljs-string>" \x0c "</span> | ||
d: <span class=hljs-string>" \x1d "</span> | ||
e: <span class=hljs-string>" \x1e "</span> | ||
f: <span class=hljs-string>" \x1f "</span> | ||
^ | ||
</code></pre><p>This requires us to increase the step to <eq><math><mn>8</mn></math></eq>, but it works!<p>Now, if we have free access to <code>\</code>, <code>x</code>, and the hexadecimal alphabet, we can reduce any program to just <eq><math><mn>4</mn></math></eq> characters outside this alphabet (we’re lucky that <code>exec</code> is free):<pre><code class=language-python><span class=hljs-comment># print("Hello, world!")</span> | ||
<span class=hljs-built_in>exec</span>(<span class=hljs-string>'\x70\x72\x69\x6e\x74\x28\x22\x48\x65\x6c\x6c\x6f\x2c\x20\x77\x6f\x72\x6c\x64\x21\x22\x29'</span>) | ||
</code></pre><p>Now we can encode this using the previous trick, leaving <code>('')</code> as-is, and run it:<pre><code class=language-python><span class=hljs-built_in>exec</span>(<span class=hljs-built_in>repr</span>(<span class=hljs-string>"[encoding of exec]([padding]'[user code]'[padding])"</span>)[<span class=hljs-number>6</span>::<span class=hljs-number>8</span>]) | ||
</code></pre><p class=next-group><span aria-level=3 class=side-header role=heading><span>The end</span></span>So that’s how you print <em>Lorem Ipsum</em> in only <eq><math><mn>24</mn></math></eq> characters and just <eq><math><mn>10</mn></math></eq> KiB of whitespace. <a href=https://github.com/purplesyringa/24-characters-of-python>Check out the repo on GitHub.</a><p>Hope you found this entertaining! If anyone knows how to bring this to <eq><math><mn>23</mn></math></eq> characters or less, I’m all ears. :)</div></section><footer><div class=viewport-container><h2>Made with my own bare hands (why.)</h2></div></footer><script>window.addEventListener("keydown", e => { | ||
if (e.key === "Enter") { | ||
if (e.ctrlKey) { | ||
window.open("https://github.com/purplesyringa/site/edit/master/blog/any-python-program-fits-in-24-characters/index.md", "_blank"); | ||
} else if ( | ||
e.target.type === "checkbox" | ||
&& e.target.parentNode | ||
&& e.target.parentNode.className === "expansible-code" | ||
) { | ||
e.target.click(); | ||
} | ||
} | ||
});</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file renamed
BIN
+126 KB
...thon-program-fits-in-30-characters/og.png → ...thon-program-fits-in-24-characters/og.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.