The Jupyter Notebook was cool! I only did some of the exercises but that's already more than the zero I did in the previous posts. convenience sure does matter.
I appreciate reading these logs, but I'm also a little confused about what you're aiming to learn!
I personally work with decorators, higher-order functions, and complicated introspection all the time... but that's because I'm building testing tools like Hypothesis (and to a lesser extent Pytest). If that's the kind of project you want to work on I'd be happy to point you at some nice issues, but it's definitely a niche.
It looks to me as if in the process of learning about decorators, konstell is learning a bunch of other things too, and very likely those things will be of greater long-term benefit than the specific knowledge of decorators.
Knowing how to sugar and desugar the syntax is fundamentally all there is to know about decorators per se. But to use them well, one has to know a lot more Python than that. Everything else I'm teaching konstell could be done without the sugar, but decorators are a convenient focus for now.
I'm also a little confused about what you're aiming to learn!
There are lots of gaps in my Python knowledge (this applies to my CS knowledge in general as well) and I'm trying to close those gaps. I asked gilch about decorators because I encountered them in pytest
and was very confused about how they worked.
I didn't have a project in mind, when I signed up for this apprenticeship, I just saw gilch offering to teach Python and thought I wanted to get better and learning from a mentor could be great.
I have attempted to contribute to open source in the past but have failed (ran into issues building things locally and didn't know how to get help), would love to try again.
Leaning in to current confusions on e.g. decorators makes sense :-)
To ask a slightly different question - what kind of thing do you want to do with Python? It's a large and flexible language, and you'd be best served focussing on somewhat different topics depending on whether you want to use Python for {scientific computing, executable psudeocode, web dev, async stuff, OSS libraries, ML research, desktop apps, etc}.
I'll also make the usual LW suggestion of learning from a good textbook - Fluent Python is the usual intermediate-to-advanced suggestion. After than I learned mostly by following, and contributing to, various open source projects - the open logs and design documents are an amazing resource, as is feedback from library maintainers.
For open-source contributions, you should expect most of the learning curve for your first few patches to be about the contribution process, culture, and tools, and just navigating a large and unfamilar codebase. These are very useful skills though! If you need someone to help get you unstuck, I'm on the Pytest core team and would be happy to help you (or another LWer) with #3426 or #8820 if you're interested.
what kind of thing do you want to do with Python?
Out of the things you listed, scientific computing & OSS libraries are things I want to explore more. I also don't just want to learn Python - although I have chosen Python to be the language to try to get pretty good at - my goal is to get myself a proper CS education. I think it would be difficult to truly get good at a language without understanding how things work underneath.
Also, what gjm said.
The skills of 'working on an existing project' I mentioned above are not usually covered as part of a CS education, but complementary skills for most things you might want to do once you have one. I also agree entirely with gjm; you'll learn a lot any time you get hands-on practice with close feedback from a mentor.
For OSS libraries, those pytest issues would be a great start. Scientific computing varies substantially by domain - largely with the associated data structures, being some combination of large arrays, sequences, or graphs. Tools like Numpy, Scipy, Dask, Pandas, or Xarray are close to universal though, and their developers are also very friendly.
>>> x = True
>>> id(x)
[etc...]
Due to Python's style of reference passing, most of these print statements will show matching id values even if you use any kind of object, not just True/False. Try to predict the output here, then run it to check:
def compare(x, y):
print(x == y, id(x) == id(y), x is y)
a = {"0": "1"}
b = {"0": "1"}
print(a == b, id(a) == id(b), a is b)
compare(a, b)
c = a
d = a
print(c == d, id(c) == id(d), c is d)
compare(c, d)
[Note to readers: We've converted this post into a Jupyter notebook so you can code along! (The notebook works on Firefox and Chrome, but may not work on all browsers.)] Let us know what you think!
[Epistemic Status: this post is reconstructed from primarily chat logs, so I am more confident in the accuracy of this post than the previous one which was reconstructed from more fragmented record.]
Previously: https://www.lesswrong.com/posts/jkaCF3yrfKvFQL4ym/an-apprentice-experiment-in-python-programming-part-2
Since the last pair-programming session, gilch and I have done some more Python programming.
Registry
After we talked about the solutions for the previous puzzle, gilch sent me the next one (gilch later clarified that I could put any code above the provided code):
This one was pretty straightforward after I had figured out the previous one, I saw that all the decorator
register
had to do was to put the functions in the list:While the output was exactly what I expected, I was a bit surprised that I got to use
registry
before it was declared, but then I realized the variable was not actually used until the first instance of@register
, afterregistry
was instantiated. To which gilch responded that globals don't need to exist until they are actually used. We moved on to the next puzzle:Register, Part 2
I played around the Python shell and noticed the function name was in the string representation of the function object:
So I used the first way I could think of to parse the function name in my solution:
This gave me the output I wanted, but I asked gilch if there was a simpler way to get the function names. Gilch's solution was:
I didn't know
__name__
existed.Gilch remarked,
Me:
Python Objects in Memory
Noticing my confusion, gilch started talking about how objects were stored in memory in Python:
When asked about whether this also applied to small types like boolean or int:
I was surprised to learn this, because some pieces in my mental model have come from Python Tutor, which displays complex objects such as functions and arrays as references on the stack, but integers and strings as values on the stack:
Gilch noted that "Python Tutor has an option to 'render all objects on the heap (Python/Java)'."
Constants (Only One Copy in Memory)
Gilch:
A More Difficult Puzzle
As before, the format of the puzzles is that I'm allowed to add any code before the given code snippet to produce the specified output.
Attempt 1
I saw
@run_with.fixture
, so I thought "class method", and when I saw@run_with
, I thought I'd need to re-purpose a constructor.This did not work:
So I thought, I'd just make everything a class method instead of an instance method:
Didn't work either:
I think I was not making class variable the right way. In addition, I also couldn't return anything in a constructor.
Gilch:
I had another idea:
This time we run into problems when decorating the tests:
I asked, "I thought
__new__
was only taking in the function it's decorating, why does python tell me it's given 2 arguments?"To which gilch answered,
Me:
Gilch:
Attempt 2
The modified puzzle that we're now solving:
The expected output stays the same:
Gilch also commented that this strategy is generalizable:
I came up with a first-pass solution:
While I was able to get the output I wanted from the tests, I could only run one test in a session. Running both tests in the same session didn't work:
Also, since I used
*fixtures.values()
to unpack the fixtures passed in as arguments, if we changed the order in which the fixtures were passed in, this solution would no longer work. I suspected this was because the two tests were referencing the samefixtures
object, so I tried to make a copy offixtures
:This didn't work, I was still unable to run both tests in the same session.
Gilch asked,
To which I replied,
Gilch:
I ran the code with a debugger and noticed that the original
fixtures
was still modified. I was not expecting this:When I wrote the line
fixtures_copy = fixtures.copy()
I was specifically trying to make a deep copy of the dictionary, I guess I still ended up with a shallow copy.Gilch:
Attempt 3
Once again, we decided to solve an easier problem first.
The modified problem:
I decided to move the mechanism to generate fixture within each time a test was run:
I noticed that calling the test functions within the
fixture
function was printing the extra lines. So I fixed that:Wasted Motion
Gilch asked me if I could find any wasted motion in the code I wrote above. Upon reading the code again, I realized that the
replacement_test
layer was not necessary, sorun_with
could be simplified to:The feedback I got from gilch was that I had not eliminated all of the waste. Then I realized that
wrapper
was also not necessary:Gilch remarked that the
fixture_store=fixtures
part is also not required.Passing in Fixtures in Any Order
Gilch gave me a hint, "do you remember all the types of packing and unpacking we discussed before?" I answered, "there's unpacking an iterable of arguments, like
*args
, and there's unpacking a dictionary of keyword arguments, like*kwargs
." "Not right.**kwargs
. Two stars. If you unpack a dict with one star, you just get the keys. This is because dicts are both mappings and iterables," gilch corrected me.Then I realized that I could pass in the fixtures as keyword arguments in any order:
Dictionary Comprehension
Gilch asked me if I knew about list comprehensions, then asked me to rewrite the decorator with a dict comprehension. So
run_with
can be abbreviated to one line:Docstrings
Next up, gilch made a small tweak:
"The test output should be the same, but in addition,
help(test1)
should show the documentation."Naturally, I thought of the
wraps
decorator we had seen earlier:This did not work as I expected:
"Aaah, you put the waste back in! A natural thing to try though," gilch was amused.
"I didn't figure out how to use
@wraps
without a function definition, " I replied."Try desugaring the wraps, and then tell me where the waste is."
"
test_with_fixtures = (wraps(test))(test_with_fixtures)
" I finally managed to desugar a decorator correctly."Yes, and?"
"We could just do
(wraps(test))(partial(test, **{f.__name__: f() for f in fixtures}))
?" I asked, unsure where this was going."Yes!"
"It did not occur to me that we could use decorators in the desugared way too, but this looks obvious in hindsight."
"Very important not to forget it. Decorators are just higher-order functions."
With this, we were able to get the docstrings working:
I was still confused, because the fact that I put an extra layer of functions in the code didn't explain why the docstring was not working, "can I not use
wraps
on a partial function or something?""Because the one you modified with wraps was not the one the decorator returned. Look closely. Which function did you modify with wraps? Which function did you return?"
"Oh I see, I returned the partial, not
test_with_fixtures
.""Yes. And that's also why it was a waste.
test_with_fixtures
was superfluous. It didn't do anything useful."Going Back to
@run_with.fixture
I didn't understand how class methods worked very well, but I thought I'd just try again at implementing the
__new__
method to take in a parameter.It worked!
Magic Methods
There were multiple ways to solve this problem. Alluding to other approaches, gilch gave me an introduction to magic methods:
Solution 2
My first attempt at implementing
__call__
didn't work:Gilch pointed out that, "unlike the class method
__new__
, which takes the class as its first argumentcls
,__call__
is a normal instance method that requires aself
instance. When did you construct an instance?"So I constructed an instance and it worked:
More Magic Methods
There was another was to solve this problem. Gilch started to talk about
__dict__
: "__dict__
is an important protocol to understand. It corresponds to the builtinvars()
. Seehelp(vars)
."I stated that the attributes of an object were stored in a dict, and variables in a given scope were also in a dict. Gilch corrected me: "Some kinds of objects, especially the more primitive kind, do not implement the
.__dict__
protocol, and thus don't work with vars. But they do implement the__dir__
protocol, and so can still list their attributes. Technically, there are some scopes where variables are not stored in a dict."So I tried
dir()
:Gilch: "Note that
__dict__
is not in that list. Ints are one of the types that don't have one. Thus,vars()
doesn't work on ints. You could still make one yourself with something like{k:getattr(42, k) for k in dir(42)}
. It's possible to implement__dir__
to return anything. It's supposed to be a list of all the object's attributes, but in some cases it doesn't list all of them. With neither__dir__
reporting it nor a__dict__
, it's possible for a secret attribute to exist. These are hard to find."I asked some tangential questions on the implementation of
__dir__
, and eventually the topic came back to, "so, given all of that, can you solve the puzzle without declaring a new class?"At this point, I was still not entirely clear about how I'd do this:
Gilch: "What does
__dict__
do?"Me:
Gilch: "What about
vars(run_with)
?"Me:
Gilch: "I'll rephrase: what is the
__dict__
for?"Me: "
__dict__
returns attributes of an object."Gilch: "Why is it empty? Doesn't
run_with
have any attributes?"I was quite baffled:
Gilch: "How about
vars(type(run_with))
?type
uses the__class__
protocol, btw. Sorun_with.__class__.__dict__
should be the same."I tried
vars(type(run_with))
:Gilch: "Or how about
vars(type(run_with)).keys()
? That might make it easier to see."I was still pretty confused and was just thinking out loud at this point:
Gilch:
Me:
Gilch:
Me:
Gilch:
I was utterly confused:
I also tried the command gilch suggested:
Gilch:
Me:
Gilch:
Then I had the idea of modifying the
__dict__
to make an attribute:Gilch also gave me their version:
Loose Threads
While I figured out the solution of the problem, I still don't have a full understanding of
vars
,dir
and__dict__
and how they interact with each other. We'll pick this up next time!Observations