#internals-and-peps
1 messages · Page 23 of 1
well, the context manager should close the file
but what if you don't use a context manager 🙂
but you can provide del as a fallback
then you're bad and you should feel bad 😛
also note in the literal sense the resource will get cleaned up, the main reason (IMHO) to provide del on a file is to flush buffers
only when your program exits
yes, but it will get cleaned up
if your problem is leaking stuff while the program runs, then you should definitely be using context managers for that
at global scope it's a bit less convenient sometimes, and while the OS will clean the file, you can simply lose the last N lines of your logfile
that depends on the resource. file descriptors and memory will be cleaned up by the OS, but other things won't (sysv shared memory, for instance, or named semaphores)
seems like a scary position to be in, presumably if the python interpreter is suitably killed it will still leak
yep, absolutely
what surprises me more in all this is that Kotlin works the same way as python, now I'm wondering if this is actually the normal behavior for GC languages
everybody knows that python's default arguments behavior is bananas, but maybe the closure behavior isn't and my intuition just isn't good
or rather, my intuition is being too influenced by C++/Rust
I'm trying to see how it works in swift now 😛
here's the kotlin for those interested
fun bar() {
var x = mutableListOf(5)
fun foo() { x.add(3) }
foo()
println(x)
x = mutableListOf(5)
println(x)
foo()
println(x)
}
fun main() {
bar()
}
[5, 3]
[5]
[5, 3]
i just think it's funny because, from my perspective in C++, I've found that one idea I had to get used to was that in GC languages, mutation vs reassignment and mutating via a mutation method can have very different consequences.
I expected this to be a situation where they behaved differently, but actually, they behave the same 😛
multiprocessing has this exciting hack in it:
On POSIX using the spawn or forkserver start methods will also start a resource tracker process which tracks the unlinked named system resources (such as named semaphores or SharedMemory objects) created by processes of the program. When all processes have exited the resource tracker unlinks any remaining tracked object. Usually there should be none, but if a process was killed by a signal there may be some “leaked” resources. (Neither leaked semaphores nor shared memory segments will be automatically unlinked until the next reboot. This is problematic for both objects because the system allows only a limited number of named semaphores, and shared memory segments occupy some space in the main memory.)
and it mostly works! Don't pkill -9 and things will mostly work out, heh
yeah, I suppose the python interpreter will have signal handlers installed for everything that allows it
so literally everything but sigkill, it should still clean up
I haven't looked at the implementation of that resource tracker, but that's my assumption
this stumped me for a few moments
then i realized it's the cell that's constant, not the object in the cell (object is changed by STORE_DEREF)
I admit I don't know what a cell is
container to store closures
Okay, sure
doing foo.__closure__[0] in bar() should give a cell object
That's basically what I figured was happening
I haven't yet decided how to feel about it
I want to see how more languages handle this
lua ```lua
function table_string(tbl)
s = ""
add_comma = false
for _, elem in next, tbl do
if add_comma then
s = s .. ", "
end
s = s .. elem
end
return "{" .. s .. "}"
end
local function bar()
x = {}
local function foo()
x[#x + 1] = 5
end
foo()
print(table_string(x))
x = {}
print(table_string(x))
foo()
print(table_string(x))
end
bar()
output:
{5}
{}
{5}
For dynamic languages it's less surprising since they tend to just implement everything as hashtables from string to whatever
I wanted to test c# and swift
But I got lazy
oh wait
i didn't local x
hold on
ok same thing
lua uses registers
also for some reason "arrays" are treated differently from "hashtables"
one has a length, the other doesn't
Weird
i guess it makes for a fast scripting language though
I believe java just straight up forbids this - if you have a closure over a variable, it may not be assigned in a way that would have this come into play (don't know the exact rules, the term is effectively final). Some algol lets you pick which one you want.
Yeah I came across this
It's hard to say what the right behavior is I think
They both have significant downsides I'd say
I think this is just one of those cases where the language dev has to make a decision, and that decision will just suck in some cases.
In non GC languages there's no confusion but that comes with other downsides
Has there ever been any consideration of adding methods to generators? Something like this would be nice:
for item in generator.limit(10):
...
to get only the first ten items that generator will produce.
.
oops, sent too early
I think there are several problems. Mostly: why limit this to generators? All of these functions will likely work on any iterator
And you cannot add methods to iterators.
itertools.islice(generator, 10)?
it's somewhat like len() being a built-in function instead of a method
I'm assuming this is just for the sake of notation (to avoid nesting)
You can totally make a custom class, so that you can do Pipe(generator).limit(10).map(int)
On the bright side, it's only almost every other language that can do this 🙂
Being duck-typing compatible with a generator is a moderately common thing to take advantage of, so adding any methods would be kinda sorta a breaking change. (coroutine wrappers for example, though I can't quite where they come into play).
>>> def f():
... i = 0
... while True:
... yield i
... i += 1
...
>>> from iters import iter
>>> for x in iter(f()).slice(5):
... print(x)
...
0
1
2
3
4
>>>
that uses https://nekitdev.github.io/iters/
Composable external iteration.
I find this method chaining weird looking in Python code. For this usecase I'd rather see a head(iterable, count=10) function. (In reality I usually just zip with a range.)
itertools.islice() also exists
is there any reason why cpython prefers plain makefiles over cmake?
probably history. Those makefiles started 33 years ago.
there is talk of changing to a new build system (probably cmake) but it's a lot of effort
Do you have a link for that?
Surely python used something to generate the make files though right
It's not just using make
yeah, it's autoconf automake etc I believe.
ah wait, no automake is used, it's just a Makefile that is then configured by autoconf
By automake you mean auto tools?
Oh I see
That's pretty scary
I wonder if python is the largest active codebase in the world to use pure make in that way
- https://discuss.python.org/t/revisiting-the-case-for-cmake-as-a-primary-cross-major-plats-build-config/11758
- https://discuss.python.org/t/what-do-you-want-to-see-in-tomorrow-s-cpython-build-system/28197
- https://discuss.python.org/t/what-do-you-consider-pain-points-of-todays-build-system/31815
- https://discuss.python.org/t/what-should-be-used-to-build-the-cpython-of-tomorrow/36001

Is there ever talk of C++ or nah
I find it a bit absurd that the most popular python implementation is in c.
people talk about it sometimes, but I don't think there's much appetite for it in the core team
at this point Rust is probably more likely than C++
but rewriting a huge codebase into another language is a very difficult task
probably the most likely way this would happen is if there's some component someone wrote in C++ or Rust that we'd want
true, C++ makes that easier, but I don't know that we'd gain as much from C++
and I think there's some portability concerns
I mean what you'd do is get the current codebase to compile in C++, then change compilation to C++
And then you can use c++ features anywhere you want
gcc did this so it's eminently doable
I agree rust is nicer than C++ fwiw but c++ is much closer to rust than C value wise, and there's actually a realistic path
yes, this is how gcc done it I think.
Yep
And I'm pretty sure they're quite happy with it
They had some good talks about it with lots of specific examples
their code-base is really c-like tho 😄
not like clang
That I'd be curious about. Is python really targeting something with no gcc target, just some embedded C compiler?
the point that im trying to make is ofc it is a huge effort, but it is going to happen anyways.
I've been seeing a lot of success with using zig to build c projects (even those that aren't using zig for anything other than build system) (both my own stuff, but also being adopted by quite a few companies for it). Quite a few benefits including getting cross compilation out of the box and very portable build tools.
Sure, I mean most of it was written in C originally, and they're not randomly rewriting working code for fun
zig is kinda popular now, Im happy they are moving away from llvm.
Eh it's still very niche
I just have vage memories of something I read, this isn't something I've personally given much thought
tried to search for "c++" on discuss.python but that's not a great idea
Heh
I do think there were some concerns somewhere about exotic platforms we support where you can't get a C++ and/or Rust compiler, might be mixing it up with something else though
maybe mimalloc (the new memory allocator we're using for nogil) doesn't support all platforms we support
C++ compiler is quite a bit easier than rust I think, since gcc still supports more targets than llvm (plus you get the union)
yes that's probably right
Very embedded targets are something of a pain still in rust from what I hear, but that's like targets that don't have a heap
of course C++ is also not exactly a superset of C, so there might have been some concerns about that
yes if im right gcc supports like 40 while llvm "only" supports 29 targets:D
yeah those would usually use micropython not cpython
It's hard to imagine something without a heap running python and hard to imagine something with a heap not being supported by gcc 😛
But maybe it's my imagination that's the problem here
That's generally what the work is, in that first step of "compile the C as C++"
I'm guessing python is mostly C89 right
Since windows didn't support anything past 89 until an hour ago 😛
probably, I think we switched the minimum support to C11 only a few years back
If there's substantial amounts of C99 and C11 that'll make it harder
microsoft has priorities I suppose
I don't blame MS to be clear 🙂
But yeah I assume when gcc did it, it was basically C89, and C89 is almost a subset, and most of the non subset things are trivial fixes
Although there's a lot of things that are legal C99/C11, are not legal C++, but which gcc will compile happily with the right flags anyway (even by default)
Yep, we switched to C11 for 3.11+ https://peps.python.org/pep-0007/#c-dialect
Python Enhancement Proposals (PEPs)
The JDK's make system is also massive
Is Pypy still stuck with an older version of python because of rpython(2.7)?
Pypy has supported python 3 for a while now
PyPy 3.10 is available.
since june 20 2014, specifically
Is there anyone in here who would like to help make a project.
I am looking for someone who is good with python who would like to do something challenging.
In [8]: import inspect
In [9]: def my_func(a: str, b: int = 5): ...
In [10]: sig = inspect.signature(my_func)
Out[10]: <Signature (a: str, b: int = 5)>
In [12]: sig.parameters
Out[12]: mappingproxy({'a': <Parameter "a: str">, 'b': <Parameter "b: int = 5">})
In [14]: sig.parameters['b']
Out[14]: <Parameter "b: int = 5">
In [16]: sig.parameters['b'].default
Out[16]: 5
@full jay
I believe the code object holds the information from which inspect gets its info
So does it only know that in situ?
In [4]: def f(a=3):
...: b=5
...:
In [5]: f.__code__.co_argcount
Out[5]: 1
In [6]: f.__code__.co_varnames
Out[6]: ('a', 'b')
In [7]: f.__defaults__
Out[7]: (3,)
```I think you can use these and several other attributes to reconstruct the information from the function object
Man, Python internals are wild
can I ask what triggered this conversation?
@feral island #pedagogy message
You can check out the implementation of how inspect.signature figures out stuff here: https://github.com/python/cpython/blob/b905fad83819ec9102ecfb97e3d8ab0aaddd9784/Lib/inspect.py#L3376-L3379
It's quite involved, but it's all pure Python 🙂
Lib/inspect.py lines 3376 to 3379
def signature(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False):
"""Get a signature object for the passed callable."""
return Signature.from_callable(obj, follow_wrapped=follow_wrapped,
globals=globals, locals=locals, eval_str=eval_str)```
also the docs on all the attributes on code objects are here: https://docs.python.org/3/reference/datamodel.html#code-objects
hm I see that too, might be a recent bug
It's a new accessibility thing
deliberate, I think
Currently this theme changes the colour of hyperlinks but doesn't underline them.
For example: https://docs.python.org/3/whatsnew/3.12.html
When hovering over a link, it changes colour and a l...
(The PR was merged a few months ago, but was only released as part of python-docs-theme==2024.1, which was pushed to PyPI on 27 January)
i dont like this, it looks ugly and dull
I disagree, I think it looks much better with underlines.
i am curious as to why it's happening at all, considering that underlining links can be done by browsers. surely that removes reason for individual websites to care?
I don't have a strong opinion on it, but if you dislike it (or think it didn't need to be done), I'd probably open an issue over at python-docs-theme
complaining here won't change anything 😉
most browsers do underline links by default, unless you use CSS to tell them not to, which I assume is what the case was before.
(attached pictures are just an examples of some webpages)
underlined links are definitely not the default, i've never seen anyone with always-underlined links
imo, links should be underlined only on hover - in that way you get visual response from website that you are indeed hovering the link and you clearly see size of the link (so you can easily notice that there is adjacent link, look at pic.2)
underlines by default just add a whole bunch of visual noise without any real benefit, and they make recognizing single link even harder because color change that happens on hover is comically small
Browsers do underline links by default, it's disabled via CSS on Wikipedia (text-decoration: none;); look in your browser's devtools.
Not underlining links is a relatively recent trend.
@dusk comet created an issue here FTR, so if people agree or disagree, that's a good place to chime in: https://github.com/python/python-docs-theme/issues/168
google.com (arguably the most visited website in the world) doesnt underline links (this is indeed disabled in css)
wikipedia doesnt do it
https://www.python.org/ doesnt do it
and i dont think https://docs.python.org/ should do it, it is just inconsistent with everything else
consistency or not with the rest of the world tends to be a terrible argument when discussing accessibility. If such arguments held weight, we'd never have wheelchair accessible public buildings. Sometimes it takes people dragging the world forward a little bit at a time to help those who need it, even when the initial solutions could be improved on (such as providing a toggle).
Edit: to clarify, "such as providing a toggle" for those who don't read the whole discussion, I don't actually think a toggle is a net good, the good comes from making a site that the purpose of is primarily to convey documentation accessible and functional first, then work to improve aesthetics without compromising on function.
I think a good reason to not underline links be default is that it devaluates the weight underlines have on the reader. Just look at the table of contents at the side of any page, or the actual table of contents pages. Almost every single word is underlined, and with so many so close together, underlines as a tool for text emphasis become way less useful.
Underlining for emphasis in typesetting is and has been generally frowned upon prior to the internet and hyperlinks as we know them, I'm not sure that comes in as a good reason.
I don't think the docs really use underlines for emphasis, yeah
Meanwhile, there's all of these from the PR, and several other resources that show that underlining links has benefits
https://www.w3.org/TR/WCAG20-TECHS/F73.html
https://adrianroselli.com/2016/06/on-link-underlines.html
https://www.nngroup.com/articles/guidelines-for-visualizing-links/
I mentioned the first link in a comment on the issue, the light mode theme already passed that check without underlines.
I'm curious is it that complicated to make the underlines optional
by adding a user setting?
in practical terms, when these things matter for accessibility that's generally the approach. we don't make colors uglier for people in general, so that they can be more accessible to people who are color blind, instead lots of video games, websites, and so on, offer color blind or other accessibility modes
I guess? Could you have a cookie or something like that which the CSS checks to decide whether to underline or not? I'm profoundly ignorant of such things so forgive if this question is silly 🙂
yes that's possible, we already have it for dark mode
right; so if the cookie is already there anyway then adding another field to store an underline/not preference similar to dark/light seems straightforward enough
Seems like the best of all worlds
yes and no.
this can quickly turn into a pile of toggles, instead of just designing so that the default experience is both more accessible and aesthetically pleasing.
if someone needs a wheelchair - they use it, it doesnt mean that we all should use wheelchairs, that would be stupid
same applies here: if you need underlines - you enable them, everyone else can enjoy their underline-less life
That's not the argument I made, and I don't appreciate the strawmanning of it
the very message you replied to includes me pointing out a toggle could possibly be an improvement. I don't think it actually is though. I think the solution comes from those who think the current underlines are ugly finding ways to contribute to improved designs that remain accessible by default so that we don't have a pile of hidden toggles people need to interact with before using a site.
Is a wheelchair accessible ramp, rather than stairs a good thing? Doesn't the ramp work for everyone? that's the parallel there. We need to find a way to make the designs more pleasing, but we shouldn't be sacrificing other users actual needs for perceived aesthetics.
I think the underlines aren't aesthetically pleasing, however even if a toggle existed I probably wouldn't use it. I even more likely wouldn't know it existed, since non-pleasing underlines are at about 0 on my list of concerns, over just reading the docs and getting on with my day.
I don't really understand the big problem with a toggle here, especially when we already have one
it's kind of the classic slippery slope argument
these websites are not a good examples:
-
(pics 1 and 2)
it is awful looking even though i have perfect color vision
even the underlines dont help -
(pic 3)
ridiculous underline on hover that doesn't fit the style of the page at all
and they have other clickable stuff that is recognized as clickable by me only because it is blue -
(pic 4)
they dont even use underlines, lol
and slippery slope arguments are weak, because nobody is proposing adding a thousand more toggles, we're talking about adding one more toggle
if there were zero toggles currently, then that argument might be more compelling, as 0 to 1 is more involved in terms of how it changes support and such
Discoverability of settings leans to accessibility being on by default when reasonable to do so. It's also reasonable to want to make the default more visually pleasing. The two goals can find a balance that doesn't require a setting that people may not even know about or need to then interact with on every site they use that goes this route.
they can, and when someoen finds a design that does both well, you can always shift to it. at the moment there's one design that's vastly more popular and by all indications, tends to be preferred.
And another design that's more accessible. So a toggle is reasonable.
Like, if there's an argument against a toggle it's not this, it's "someone has to do the work" - this is kind of just arguing for the sake of it 🤷♂️
and to take this at face value, it's vastly more common to have both stairs and a ramp
the reason being that most of the time the ramp was added after the fact...
in new buildings, I still usually see both
stairs are a lot more comfortable, convenient and efficient, for people who are healthy enough to easily walk up stairs, which is still a rather strong majority of people
on some websited there is a toggle that toggles all accessibility-related things: link underlines, better color contrast, bigger elements and stuff like that
if we really care about that - i think we should make a toggle that toggles all/some of that, instead of enabling this by default
https://github.com/python/cpython/commit/0e71a29
"inlining" is happening apparently
How would this be used to provide what benefits
I don't know what this means
if Mark Shannon is doing it, then the benefit is speed.
Right, he's leading the faster Cpython stuff are Microsoft
After reading what function inlining is, I guess I sort of get it.
i love mark shannon, he's a wizard that just waves his wand and invokes his dark magic and we get faster code that no one except him can understand
from the PR itself:
Adds a tier 2 optimizer pass that converts the micro-ops for loading globals and builtins to constants.
This should have two benefits:
- The resulting code is faster
- It will enable further optimizations thanks to better constant and type propagation.
Benchmarking and stats show a ~1% speedup, but it is the stats that are interesting.
The tier 2 stats show that we have replaced 3 billion guards with 1.2 cheaper guards, and that all
LOAD_GLOBAL_MODULEandLOAD_GLOBAL_BUILTINShave been replaced with inline constants.There are some changes to the tier 1 stats, which I think are a result of not optimizing when the global/builtin keys version doesn't match, so we avoid poor optimization. This drops us back into tier 1 for later re-optimization to tier 2, hopefully when the set of global variables has stabilized.
This shows up in the tier 1 stats and optimization attempts.
What this suggests is that we should be de-optimizing faster in tier 1 in this case, as once the keys version has changed it will never go back to the original value. But that's for another PR.
Does CPython specify a custom type or does every single entry in the symbol table must use these?
typedef enum _block_type {
FunctionBlock, ClassBlock, ModuleBlock,
// Used for annotations if 'from __future__ import annotations' is active.
// Annotation blocks cannot bind names and are not evaluated.
AnnotationBlock,
// Used for generics and type aliases. These work mostly like functions
// (see PEP 695 for details). The three different blocks function identically;
// they are different enum entries only so that error messages can be more
// precise.
TypeVarBoundBlock, TypeAliasBlock, TypeParamBlock
} _Py_block_ty;
is there a more low-level type system?
like i16 or whatever corresponds to one instruction.
I am not sure what you're asking, can you clarify?
that enum is used only in symtable.c which is primarily responsible for figuring out where each name is defined and what scope it comes from
it's not directly involved with bytecode generation
well okay, I suppose.
That PR doesn't inline any functions (at least, not yet). The "inline" in that context just means it's promoting the global/builtin value to a constant and loading the constant from the bytecode cache entry itself rather than from the usual globals/builtins namespace. (So, more like inline cache, not function inlining).
yea
yes
✅ silenced current channel for 8 minute(s).
❌ current channel is already silenced.
!shhh
❌ current channel is already silenced.
!unhush
✅ unsilenced current channel.
Topic
When are dis.Positions attached to an instruction included, when are they null, and why are the fields optional?
COPY_FREE_VARS for example doesn't have any line number associated with it. But is there a place where it may make sense to say it comes from?
the beginning of the function
I'm aware that it's at the start of the function, yes.
More wondering about the general case, I've already discovered fn.__code__.co_firstlineno
Hiiiii everyone
Hello, I've heard cpython is considering a rewrite of its build system, as someone coming from a rather long background of maintaining the JDK's make systems, I'm intrigued, is there anywhere I can view this effort or help out with it?
. These links should be a good start
@hybrid relic ^
Thank you!
Good one
Just a procedures question - I've got an open PR that's received some really helpful feedback, and that I've simplified/reduced the scope of radically. Would it be better practice/courtesy to the maintainers to push all the changes to the existing PR, or open a "clean" PR with a new branch?
I'd probably use the existing PR, unless the discussion on it is so long and complicated that it would be hard to follow
Makes sense, thank you!
Guys, I really suck at backtracking. When i see a question on backtracking my mind goes blank. Could anyone please suggest some good material to master backtracking
I really want to learn it
Any specific contexts/versions of backtracking you are interested in? Depending on the problem it might be easier to think about if modeled recursively (at least I often find it easier to think about then)
for example this question here - https://leetcode.com/problems/combination-sum/description/. But i do not want to restrict to one question or go solving question by question. I want to understand backtracking in general and then go solve questions on it
Backtracking at it's core is really just "undoing" steps and then trying something else, whatever that means for the specific context
It might mean going back in a maze and picking a different path, it might mean undoing some parsing and attempting to parser it as something else, etc, etc
Think of it almost like creating a save point in a game before trying something, when it goes wrong just load the save and try something else
Most backtracking problems can be modeled as a tree, where you are trying to find a specific leaf
Can anyone help me to learn pandas library ?
If you want a general introduction, you can look at our resources. If you have a specific question, ask in a help channel. This channel is about the internals of Python.
We're a large, friendly community focused around the Python programming language. Our community is open to those who wish to learn the language, as well as those looking to help others.
hi, to my surprise when i run a python program after a couple of hours, it takes approx 20secs to even start executing the program. attached screenshot shows the time difference between
- time of command invocation
- time of main invocation
- end of main invocation
- end of command invocation
pls let me know the reason why the time delay
That would depend on main.py and where it prints the date. I'm guessing you won't see this effect if you put the printing of the current date on top of the script, even above all imports.
python/cpython#115480 seems exciting
as mentioned earlier, dates were printed start of main
per your inputs, have added another print of now() after imports. result turned out to be having significant delay as earlier
please find the latest screenshot
Ah, you mean __main__ in the sense of if __name__ == "__main__"? Yeah, all code above that will also take time to execute, including imports and definitions.
Most of the time is likely taken by your imports. Just initializing Python should be much faster
e.g. for me this is 20 ms % time python -c pass python -c pass 0.02s user 0.02s system 55% cpu 0.071 total
repeated execution of the same script doesn't show any delay !!
if it's because of imports, wont it be the same case every execution cycle?
the first time will be slower because of pyc file creation
and possibly warming up file system caches
Depends on the imports. They can do whatever they want, including updating some on-disk cache if it's older than X
(and @quick snow is right that in general imports can run arbitrary Python code, so all kinds of things might be happening)
if pyc was already done and pycache have the necessary files, can we skip that part in a particular execution?
yes, if the pyc files are already there Python will just use them
not the case here. if i run the same command after an hour's time delay is clearly seen
could also be file system caches, in that case
but can still see files under pycache folders
OSes often cache recently accessed files in memory, making them much faster to access
but if you don't use those files for some time, they might get bumped out of the cache by something else
that could be what's going on for you, though it's hard to say without access to your system. you should consider using a profiling tool to get more insights into where your program spends time
ok. let me spend time profiling to get better insights. thanks again
is there a good summary somewhere of why the GIL-ectomy is so hard
not doubting it is of course, I'm just trying to grok it fully
!pep 703
might be a good start
maybe Larry has a writeup of his previous project (which used the name "gilectomy"), not sure. Definitely should be able to find videos of his PyCon talks about it
thanks!
short answer: because the goal is to not slow down single-threaded code.
yeah, it's just not immediately obvious to me why that would be the case
Like, you make all the refcounts atomic. okay, that has some performance cost, but incrementing an atomic is still very cheap
Why it would be the case that the effort needs to not slow down single threaded code, or why it would slow down single threaded code?
uhm because there's mountains of existing python code that's single threaded, so e.g. a 30% performance hit or something like that would really suck
@mild cobalt was asking for clarification of your question
I thought mirashii was asking you for clarification 🙂
you said "the goal is not to slow down single threaded code" and the first part of their question was "why is that important"
Also hi Ned, long time no see, used to bump into you all the time on Freenode… probably 15 years ago or so now. Good to see you’re still around and answering questions as always
this turns out to be wrong, unfortunately. Incrementing an atomic may be cheap, but it's still a lot less cheap than incrementing a non-atomic integer
And Python programs change a lot of refcounts
interesting
so I guess, a lot of times the situation may be that rather than incrementing something many times in the "GIL-ectomied" code, you now maybe want to simply for example increment it once at the start, and then do the rest of the work keeping track of the ref count in a local variable
i.e. write to the "true" ref count fewer times
and take advantage of the fact that if you +1's the refcount once, that' ssufficient to ensure it won't be destroyed
one thing I will say is that AFAIU, in single threaded code, incrementing an atomic integer should actually be pretty much the same cost as incrementing a normal integer, on x86-64
well, very close to it
most of the "lot less cheap" of atomics vs non-atomics occurs due to extra cache invalidation due to contention
if there's no other threads then you will never get invalidated
yes, I think that's one of the techniques PEP 703 uses
if I recall correctly Larry observed that multi-threaded Python that naively used atomic refcounts was slower than equivalent single-threaded code, because of all this invalidation
not just slower per core, but slower overall
gotcha
that's pretty crazy
guess there's a lot of reference counting
I guess this is the big advantage of tracing GC's?
once you start considering multithreading
I was a bit curious about this from a parallel convo and I looked it up:https://developers.redhat.com/articles/2021/11/02/how-choose-best-java-garbage-collector
It's pretty wild that Java has, currently, competitively, five garbage collectors, each of which is fairly state of the art and probably represents more man hours than python's GC strategy
In practice, you don’t need contention for atomics to cause bandwidth issues due to cache handling. In practice, many microarchitectures, including most intel chips last I checked, prevent any sort of instruction level parallelism the second an atomic enters the pipeline, even if it’s on an uncontended address
https://arxiv.org/pdf/2010.09852.pdf is slightly dated in the hardware it tests on
But quite insightful
On x86-64, IIRC, acquire-release is completely free
and that should be sufficient ordering strength for a refcount
at least, that' smy understanding
actually that may only be for a pure read or write, rather than for a fetch-add
at any rate thanks for the link
The overall conclusions about cache proximity you can take from it are still accurate to current hardware if people were curious on if that being dated matters.
Me and a friend are attempting to add a new operator to pythons syntax. We've found where the grammar file is, and have some semblance of an understanding of the lexer/parser/compiler but we still have not found where the definition of any of the keywords or operators are. Does anybody know where we could find this? I'm just looking for the place where it says "Oh yea, when you add, you do 1+1"
that would be in the bytecode implementation (Python/bytecodes.c now I think?)
do we need refcounts at all? they require memory, they should be updated basically every time you touch the object, every time they are updated they should be checked if they are 0, ...
as i see it, the main benefit of having refcounts is that 99% of objects are collected by RC-GC right at the moment they become trash
if we remove RCs, trash will live longer and garbage collection will become slower because smart GC has to collect all the trash, but is this a problem?
uhm it depends how you define "slower", first of all
As far as a general approach goes (i.e. without getting into the specific python implementation and issues caused by transitions)
Pretty much all high performance GC's are not refcount + cycle detection
they're fancy tracing generational GC's
Removing refcounts isn't trivial, at all. There are ways to keep refcounting without needing atomic operations: https://csaws.cs.technion.ac.il/~erez/Papers/refcount.pdf
so "do we need RC's at all?" not as such, no, but transitioning python to tracing GC is probably a pretty insane task
iirc, python's gc is both tracing and generational
In fact, just about no other implementation of python has reference counts
i'm not really sure what you mean by that
AFAIU python's GC is very simple: it does the refcount thing, and occasionally it runs something that finds cycle
I've never looked at the implementation but that's how it's invariably described
that's not how e.g. Java's GC works
There is a concept of generations involved as a heurestic to figure out when to run the cycle detection
sure, but that's not, AFAIU, what is generally meant by "generational" garbage collection
that usually refers to putting objects in different allocator areas, that are specialized for different tasks
there is an OS called Phantom - it is a persistent OS
they use pretty cool idea for garbage collection: if you take snapshot of the state and slowly find all the trash in it, you can remove this trash from the main state, because trash cannot be resurrected
i imagine in python it could work like this: fork process, run gc in it (main process still does its thing), when gc is done, all found trash is collected in the main process, then gc's process dies
e.g. starting objects in a nursery
i mean if python was going to reimplement GC, we don't need to look at relatively obscure OS's: there's a pile of insanely tested, high performance, etc GC's that are used in Java, often for very similar purposes as python code (i.e. backend web servers)
but I'm sure python isn't going to reimplement its GC anytime soon
I would not be so sure that removing refcounts for a tracing gc would be an uplift in performance for python
As far as I can tell, python has a generational GC in the common sense of the word.
IDK what's different about python, AFAIK all high performance GC's (in basically any sense of the word "high performance") are tracing and not refcount
it just boils down to how much you want to rewrite
and the languages have other design choices involved as well that help guide some of that
it isn't just "This is always better" because that's not really the case
there are ways to improve the refcounting performance, I think a few of them have come up over in either the faster-cpython work, or the subinterpreters discussion, but I'd have to go look.
without strong evidence to the contrary I'm going to assume that python isn't different to everything else
i dont understand why you say that python's gc is not tracing: it does pretty much the same things that you described and wikipedia article described
AFAIK, generational is largely a strategy within tracing: that's what most sources seem to say. One of the biggest points of generations is that you reap the nursery more often; with refcounts this isn't really necessary (since most objects get deleted immedialy as the refcount hits 0)
Yeah, that's how the cpython gc works, it reaps the nursery first, and anything that survives leaves the nursery
but "reaping" is only for cycles, is it not?
python has both reference counting and a cycle detector. It's a strategy that tends to outperform mark and sweep in single threaded code, and have smaller gc pauses in general
!e ```py
import gc
print(gc.get_threshold())
@dusk comet :white_check_mark: Your 3.12 eval job has completed with return code 0.
(700, 10, 10)
Well, it would also work for other objects
but yes, only cycles in practice
A tracing, generational GC to handle the complex case, and a refcounting strategy to handle the simple case.
one of the only mainstream languages using it, in the world, is also one of the slowest, so I have no idea what you plan to point to, to back up your claims 🤷♂️ .
For Java, despite the existence of GC's that purely target single threaded, and GC's that emphasize latency, none of the GC's (AFAIK) are using reference counting
except python isn't the only language that has a hybrid approach
and the slowness isn't attributable to that
it does literally this thing: https://en.wikipedia.org/wiki/Tracing_garbage_collection
and additionally it performs refcounting
which language are you thinking of?
Hello, I learned the basics of Python and I'm learning Django, can anyone help me with what would be the pillars that I should focus on in Django for better learning?
I mean, you're just misunderstanding terminology at this point. refcount + cycle detection is what python's GC strategy is called. If you want to call the cycle detector a tracing GC: you can do it, fo rsure.
obviously the cycle detector is similar to a tracer in that they both traverse the object graph
probably check pins in #web-development
but they're still different things with different goals. the tracer is looking for all objects that are disconnected, the cycle detector is.... lookign for cycles 🤷♂️
I mean, it isn't looking for cycles, we call it the cycle detector, but it is just a tracing GC, it finds disconnected cycles by them being... disconnected, not by them being cycles
modern optimized garbage collection is hybrid in nature, and you seem to be the only one misunderstanding things here. Others who arent sure have asked questions.
Thanks
I mean you can call it what you want, it just confuses the conversation and doesn't seem to be in line with the standard usage
cool story
(a quick google will find a dozen sources that say that the JVM does not do reference counting, but believe what you want, I've learned in the past it's not likely to be constructive to continue this with you)
cool, I didn't say what the JVM used at all in this as it isn't actually relevent to what the right choice is for python
modern optimized garbage collection is hybrid in nature,
sure
TIL, JVM GC's (all 5) are not modern 🤷♂️
JVM GCs are famously bad for latency (though I do believe modern ones are better at this), python does better here since there is less garbage to collect during pauses.
there's two JVM GC's that are optimized for latency
sure, i'm not offhand sure how that's relevant
to be extra clear: nobody was criticizing python for choosing refcount, 30+ years ago
or for choosing refcount at all
just saying it's not really a terribly efficient option, as it's turned out, but moving away from it probably isn't practical
I'm willing to say it's still the correct choice, and that there are gains available without changing the actual gc strategy.
It's also worth pointing out that in compiled languages, lifetime analysis can serve as a substitute for reference counts in the same strategy
that doesn't make those strategies non-hybrid in nature, and I dont appreicate the way you're just trying to say that "java does it, must be the only example or only point"
The fact 0 other languages have that as an option makes it a bit hard to actually figure out if it is actually worse (it is arguably worse in multi-threaded), but its hard to even figure out if it beats a naive mark-and-sweep style implementation.
I'm not even sure what you're trying to say. Reference counting means something specific: objects get destroyed immediately when reference count hits 0.
afaik, JS's GC is also causing significant latencies
i might be wrong, im not a JS expert
In many compiled and functional languages, you know when nothing has a reference without explcit counting
Java does not do that; neither does Go, etc. Swift does that, but it does not really have a full GC, since it does not have cycle detection (you manually mark references as weak)
....
k, my fault for continuing to engage
anyhow
most languages with refcounts seem to not have cycle detection (swift, lobster, one of nims modes), some cannot express ref cycles (kokka).
Most python implementation with alternative GCs also have a JIT of sorts, which once again makes it hard to tell
I think the reason for that is pretty clear: refcounts big advantage is determinism. You trade for that determinism, by giving away optimization opportunities
modern GC's leverage every bit of that flexibility of when to destroy an object
IG once nuitka-python gets their PGO working, we may have an apples-to-apples-ish GC comparison.
that's how they've managed to optimize so much, over the decades
refcount + cycle detector basically ruins that because it's no longer really deterministic anyway
like, finalizers in python are not more encouraged than they are in Java
so, you haven't really gained much
in Swift, it's different: you have to handle weak manually, but in exchange you actually get true determinism
determinism has a lot of value; it means you for example don't really need context managers anymore
Refcount just has much less scope for optimization to start with. but yes, if we actually see more mainstream languages that use reference counting GC, that aren't slower for unrelated reasons (like python)
we'll have a better sense
relying on finalizers in python is almost always a bad idea, they are the last resort to keep consistency in bad situations
cool fact: zip can reuse the tuple it yields, but that happens only if the tuple is not stored anywhere else (RC==1)
(not sure if it is a thing in modern cpython, but iirc Raymond Hettinger was talking about this in some old pycon)
If you look at the actual tradeoffs involved, there are better ways to implement reference counting for performance while retaining the benefits of fewer gc pauses. An example for this would be looking at what actually gets generated for rust's Rc vs what python does at the interpreter level.
Other languages have made significant improvements in their gc implementations, but attributing the issue to reference counting when there is adequate research showing the known performance issues with reference counting have strategies available to remedy them, and that's been linked, isnt helpful to people interestedi n making python faster.
You can't really do the level of static analysis (presumably)rust Rc, nim arc and kokka do to elide refcount changes in most cases in python.
python is uniquely poorly suited to sound static analysis
this isn't entirely true, in the case of function local reference count changes, python can use deffered reference count updating too.
unless a debugger messed with the locals
most languages (even dynamic ones) at least give you this as an axiom, python doesn't.
def f():
x = y = object()
g()
assert x is y
There's other optimizations like that, like string +=
If refcount is 1 then it can mutate in place
that's UB and out of scope anyhow /shrug
faster cpython people have to worry about it, so no
the debugger is well defined as a core part of CPython
i hope it gets better in the future
if i understand correctly, microopcodes can be used to perform some kind of analysis
It's hard to imagine how tbh
Python is very dynamic
Not usually idiomatically, but legally
And optimizers have to consider any legal code
I don't think JavaScript is much less dynamic than Python, and there are well-understood techniques for fast JITs in JS
int += n might do this as well, but i dont know if it is actually happening in cpython
Jits yes, I was responding to comments about static analysis
oh yes, I agree then
Probably not? Much harder to make it worthwhile
if the interpreter can't assume a debugger changing reference counts is illegal, then half of the things that have been done for faster cpython seem invalid to me
what's to say a debugger didn't change fucntion code?
changing the code array is definitely a step too far, but changing the locals is not
in the PEP 659 adaptive bytecodes you can see lots of DEOPT checks for that sort of thing
changing the values of locals wouldnt change the ref counts in any supported way of doing so
code objects are immutable.
and yet it's doable with a debugger, or jsut with ctypes
debuggers can't change bytecode. ctypes can, but it can also change a 2 to a 3, so all bets are off.
if debugger_is_running(): do_slow_but_reliable_thing()
else: go_fast()
The debugger can still jump to another line in a function in 3.12
by bringing up ctypes it seems like you're talking about a C-level debugger now, previously we were talking about a Python debugger
Ye, if you attach gdb, all bets are off
can we replace code object in a frame object with other code object?
ctypes is a part of cpython, and as I pointed out, the python debugger methods of updating a value wouldnt touch refcounts in a way that breaks them
changing python internals with ctypes is crossing a line, and Python doesn't have to keep things working if you do that.
It breaks the static analysis that rust and similar languages do to make refcounts fast
Python's gc is not tracing - it does not have a collection of root objects that it explores outwards from. In fact, it doesn't even generally know what objects hold references to other objects, it only knows how many references are held to each GC'd object. The cycle detection is a clever algorithm that looks only at the reference counts of all GC-aware objects.
I'm pretty sure that it doesn't break the capability to do what I claimed: ie. deferred reference count updates. The python interpreter is aware of it's own debugger.
ctypes and a C debugger and writing to /proc/self/mem can all be used to break all of the memory safety that Python gives you. All bets are off for all of those. That's fundamentally different from a Python debugger.
I wasn't claiming python can use all of the things available via static anaylsis
What does rust do to make refcounts fast?
It mostly just makes it possible to avoid them, and allows you to safely use non atomic integers when safe
I assume it does the usual increment reference+decrement reference becomes a no-op thing at least
Python's gc is generational, though. Young objects are checked for cycles more often than old objects.
What exactly are you thinking of?
Are you talking about moves?
lifetime analysis in rust allows some reference count updates to be completely elided
If I have a program like
a = b
a = object()
```then I do not need to actually change the refcount of the object in b (-ish)
Link if you have a godbolt
We can look at the assembly for it. I'm not really sure what will happen. I'm not aware of any rust level optimization for this and LLVM is typically super conservative with atomics
i dont think this is true
there is a tp_traverse field that provides information about what references the current object is holding
i dont think detecting cycles is possible by looking only at RCs
also, if there is no "root" object, then what is keeping everything alive? we can consider everything a trash and collect literally every object
IIRC it works by subtracting from the refcount all the references from other GCed objects
if the remaining number is nonzero, that means there are references outside of the objects the GC tracks
https://devguide.python.org/internals/garbage-collector/ covers this pretty well
you beat me to pasting it 🙂
there is a
tp_traversefield that provides information about what references the current object is holding
That's true, but irrelevant here. Consider references on the stack, for instance - the C stack doesn't have atp_traversethat lets you ask what objects it holds references to.
ok, i have read this in past, but i guess i have to read it again
there is a thread state for every thread, and i imagine it holds a reference to the frame object, which holds references to all locals
list of threads is also stored somewhere
@flat gazelle so here's an example
your intuition is wrong 🙂
// Type your code here, or load an example.
// As of Rust 1.75, small functions are automatically
// marked as #[inline] so they will not show up in
// the output when compiling with optimisations. Use
// #[no_mangle] or #[inline(never)] to work around
// this issue.
// See https://github.com/compiler-explorer/compiler-explorer/issues/593...
Here's the rust code
use std::sync::Arc;
struct Foo {
x: f64,
y: f64,
}
#[no_mangle]
pub fn square(x: &Arc<Foo>) -> Arc<Foo> {
let mut y = x.clone();
y = Arc::new(Foo{x:3.0, y:5.0});
y
}
I don't think it manages to elide the refcount changes on x
a reference may also be held inside a C function
e.g. a C function could do Py_INCREF(obj), then invoke some Python code, then Py_DECREF(obj)
ok, that makes sense
if GC runs while the Python code is running, then there may be nothing that holds a reference to obj except for the C stack
@flat gazelle
push r14
push rbx
push rax
mov r14, qword ptr [rdi]
lock inc qword ptr [r14]
jle .LBB1_3
mov qword ptr [rsp], r14
mov rax, qword ptr [rip + __rust_no_alloc_shim_is_unstable@GOTPCREL]
movzx eax, byte ptr [rax]
mov edi, 32
mov esi, 8
call qword ptr [rip + __rust_alloc@GOTPCREL]
test rax, rax
je .LBB1_2
mov rbx, rax
mov qword ptr [rax], 1
mov qword ptr [rax + 8], 1
movabs rax, 4613937818241073152
mov qword ptr [rbx + 16], rax
movabs rax, 4617315517961601024
mov qword ptr [rbx + 24], rax
lock dec qword ptr [r14]
jne .LBB1_9
mov rdi, rsp
call alloc::sync::Arc<T,A>::drop_slow
You can see the lock inc and the lock dec, I'm not an expert on assembly but I'm guessing this corresponds to the atomic increment and decrement
i guess @raven ridge can maybe help confirm whether that's correct or not
it is possible it doesn't happen with Arc, ye
it does work with Rc if I am reading the utterly incomprehensible assembly right.
that doesn't mean that rust is doing any clever analysis
with Rc, you're simply incrementing and decrementing a non-atomic integer
it's trivial for LLVM to remove that later
that's very possible, I mostly used rust as the example since it is the most familiar here, Kokka and Nim do actually do clever analysis.
(kokka for sure, could be wrong about nim, but it would surprise me)
I mean if rust did anything clever here, your and sinbad's comments notwithstanding, I'd actually be shocked
because that's just not the point of Rust
and it's also much harder to be clever when these things are not truly "baked in"
Swift refcounts are part of the language
Arc has a bit of special treatment but it's basically just another type to the compiler
so when it sees .clone() which is running arbitrary custom logic, it's not that easy to just magically make these increment/decrement goa way
makes sense
rust's Arc and C++'s shared_ptr aren't fast, you just barely use them, so the language is still very fast on the whole
when shared_ptr first came out, some people got really excited and started using them as if it were a GC language, and in fact this made their code really, really slow 🙂
I mean, I said Rc, and you turned around and showed it doesn't work with Arc. There's a difference between those, and it matters to the optimizations possible. It's also why I linked a paper about reference counting strategies that decrease the number of atomics needed.
way up here, for reference
I guess you still don't have a godbolt link?
https://koka-lang.github.io/koka/doc/book.html#why-perceus this is another example.
I figured 🙂
Like I said, Rc in simple cases like these can just be optimized by LLVM; it's the same as doing
x+=1;
x-=1;
of course this gets optimized out. that doesn't imply any clever lifetime analysis
What do you think llvm is doing when it optimizes that out?
it sees a non atomic integer incremented, then decremented... so it knows it can optimize it out?
escape analysis
nothing to do with lifetime analysis
right, as long as the intermediate state cannot be observed
okay, now I want you to use your imagination here because the point I was making was about potential optimizations possible for python, and using inspiration from what other languages and compilers are capable of. Sorry if using the word lifetime with regard to rust caused you to get pedantic in a way that doesn't change the overall point
it absolutely changes the point, because this is just a trivial low level optimization, bringing rust's lifetimes and clever static type system into it as you tried to do was totally irrelevant.
it just doesn't seem like you are sorry. Perhaps you should review the #code-of-conduct
seems like people are getting sarcastic with each other, which isn't helping understanding. probably people are using words in slightly different ways. clarify.
To clarify: If there's no other scope something can possibly exist in, and we have any proof of that, we can elide some number of reference count updates until the scope sharing is possible
that seems like what quicknir was describing. you are in agreement, no?
I feel like you say a bunch of stuff, and then i ask for concrete examples of what you describe, and the examples never come, and you are more interested in one-upping people here than in constructive conversation 🤷♂️
you can't really prove that something doesn't exist in an outside scope as far as python goes
you can if it was created in the inner scope
No, I dont think I agree with much of what quicknir has said on this in the overall discussion.
you don't need to prove that it doesn't exist in an outside scope
you just need to prove that nobody can observe the intermediate state
it seemed like your last two messages were very similar, but maybe i missed some nuance.
but even then, I mean, somebody could hit Ctrl-c couldn't they if the python code is running interactively?
forcing an exception up, in between the two lines
I'm also having trouble understanding what the disagreement is at this point
@urban sandal in any case, it might be instructive to read about the free-threading work happening now to remove the GIL. they are dealing with many things like this.
well, integers in python are immutable so you'd need to change the example anyway
I'm aware, I pointed that out
while I appreicate that people havent followed the whole chat, it gets a btit frustrating to have a bunch of people hop in and miss the overall discussion that's happened, so im gonna step back from this.
but in e.g. C++, if you ahve
mutation();
stuff();
antimutation();
then as long as stuff() doesn't involve any non-inlined function calls, and as long as nothing in stuff() observes anything touched by mutation()
and as long as none of it touches atomics (which can be observed by another thread)
the compiler can in principle optimize out mutation() and antimutation()
i'm sure i missed some technical details, but the tone is unmistakable.
this is actually even true for atomics btw, it's just that fusing atomic writes is kind of crazy and compielrs don't really do that
in python, I don't think you can ever prove that stuff() doesn't jump outwards because you could for example just force an exception up
and probably, many other things
you can use inspect to examine the frames above you on the stack
yes, but then you would need to look at all those frames above you and see if they have any references to the stuff touched in mutation()
and the whole optimization just becomes infeasible really quickly
Yeah, I remain very unconvinced traditional techniques for improving refcounting performance can be applied to python
I think there's a lot you can do in a JIT that has the ability to bail out ("deopt") if something unusual happens
interestingly, exceptions contain a reference to the stack of Python frames, which contain references to their locals. So, for instance, py def foo(): a = 10 if random.random < 0.01: raise RuntimeError() a += 10 return a Arguably Python shouldn't optimize this down to a return of a literal return 20 because if it did the runtime error would hold a reference to a frame that's missing the a local it ought to have
Yeah, various forms of speculative execution can probably work
Yeah I tend to agree. It's a good fit for a language that can be very dynamic and crazy but almost never is
Does anyone have coursera subscription that can be shared?
!rule ad
🤓 python can optimise this to
def foo():
a = 10
raise TypeError("< not supported between function and float")
that's unsafe, what if another thread set random.random = 0 in the meantime
Do the methods of the tokenizer work in such a way that non-ascii unicode characters are not viewed as one character?
!ban @quasi stag scam
:x: User is already permanently banned (#94486).
welp
should we care about that?
who is "we"
it absolutely matters for optimization in the interpreter
it is an abstract "we"
i was talking about core developers
if random is a module, and random.random is documented to be a function, isn't it ok to assume that it is a function at runtime?
if someone is monkey patching stdlib - they are intentionally shooting in their own leg
it's part of what Python allows you to do at runtime, so optimizations need to consider that it can happen
it's also not inconceivable that it happens in real code; I've seen quite a few tests that mock.patch random.random
It's also very possible through beginner spaghetti, especially for things like max and min and list.
I'm not sure I can see what sort of optimization you're imagining here, honestly. The compiler doesn't even know whether a name refers to a module or not - it just emits a "load the global with this name" bytecode instruction
it doesn't necessarily know whether an import random happened, and even if someone did do import random it doesn't necessarily know that nothing has later overwritten the name random with something else - ```py
import random
random = random.randrange(5)
and that's before you even get into weird stuff like ```py
import random
globals()[input()] = 42
any changes to the compiler that would change the behavior of one of these programs aren't optimizations, they're changes to the semantics of the language itself
things like this are what quicknir meant yesterday when saying that the language can be very dynamic but almost never is. Though as Jelle points out, monkeypatches in unit tests are one place where Python really can be this dynamic in real world code.
database models can also be pretty dynamic in a way
iirc dataclasses uses eval() or exec() to create the init?
thanks to the hell that is the Python import system, it can't even assume that import random gives it a reference to the random module from the stdlib. Maybe the user has a random.py in the current directory. Or maybe they've got an import hook doing something weird... Or maybe they've assigned to sys.modules["random"].
you can't even assume it's a module
i did not know this was possible ```pycon
def g(a, /, **kwargs):
... print(a, kwargs)
...
g(5, a=2) # duplicate arguments?
5 {'a': 2}
only if it's positional-only
how come i only learned about it now
https://peps.python.org/pep-0570/#semantic-corner-case you didn't read PEP 570
Python Enhancement Proposals (PEPs)
that's one of the motivations for positional-only arguments - they let you define a function where the **kwargs can use any name, a la dict.update()
what's the point of the func_type mode when compiling? can't it be turned into a Callable/Protocol when evaluating?
much better u learn Lua
I'll give u example Lua message="hello world"
print "hello world"
or much short cut
print "hello world"
i already know lua
also, this is off-topic, go to #ot2-never-nester’s-nightmare if you want to talk more about it
huh I'm confusing 3 off topics
anbody know where these names come from in pycore_ast.h
typedef enum _operator { Add=1, Sub=2, Mult=3, MatMult=4, Div=5, Mod=6, Pow=7,
LShift=8, RShift=9, BitOr=10, BitXor=11, BitAnd=12,
FloorDiv=13 } operator_ty;```
File says it's generated by `asdl_c.py` but I don't know what that works off of.
Parser/Python.asdl
Parser/Python.asdl lines 102 to 103
operator = Add | Sub | Mult | MatMult | Div | Mod | Pow | LShift
| RShift | BitOr | BitXor | BitAnd | FloorDiv```
thanks
I have successfully (sort of) made a new operator now :)
unfortunately despite it building finally
the built version does not accept the new operator
But progress is progress :)
that's interesting
what does it do?
Nothing except throw a syntax error
Cannot figure out how to attach the symbol to a bytecode thingy
codegen occurs in Python/compile.c
no like what does it do if it compiles successfully?
me and my friend have the final goal of:
reassignable custom operators with no default behavior.
Currently we're just trying to figure out how one new operator works start to finish, so my idea so far has been to just make an addition clone
We also wanted to be able to make operators out of unicode characters, but thats gonna take a bit more work to understand
Alright the big sticking point I keep coming back to is that in bytecodes.c, I see all these things like BINARY_OP_ADD_INT etc. but didn't know what they meant. By my best following of the macros and whatever, I found this in opcode_ids.h
#define BINARY_OP_ADD_FLOAT 150
#define BINARY_OP_ADD_INT 151```
But like what do these numbers mean? How does it know?
bytecode specializations
they're autogenerated along with the actual despecialized opcodes
but how does it know that number is related to the operator or keyword or whatever
that is to say, how does it know when it sees a plus to go to the BINARY_OP_ADD_...
it doesn't. compile.c generates BINARY_OP_ADD
and at runtime, the specializer may change it to BINARY_OP_ADD_FLOAT if it sees that the code is usually adding floats
for your new operator you probably shouldn't worry about specializations
Does anyone have coursera subscription?
why is my cmd python install going like 40kb/ps
my internet has 40mbps
typedef struct {
/* Number implementations must check *both*
arguments for proper type and implement the necessary conversions
in the slot functions themselves. */
binaryfunc nb_add;
binaryfunc nb_subtract;
binaryfunc nb_multiply;
binaryfunc nb_remainder;
binaryfunc nb_divmod;
ternaryfunc nb_power;
unaryfunc nb_negative;
unaryfunc nb_positive;
unaryfunc nb_absolute;
inquiry nb_bool;
unaryfunc nb_invert;
binaryfunc nb_lshift;
binaryfunc nb_rshift;
binaryfunc nb_and;
binaryfunc nb_xor;
binaryfunc nb_or;
unaryfunc nb_int;
void *nb_reserved; /* the slot formerly known as nb_long */
unaryfunc nb_float;
binaryfunc nb_inplace_add;
binaryfunc nb_inplace_subtract;
binaryfunc nb_inplace_multiply;
binaryfunc nb_inplace_remainder;
ternaryfunc nb_inplace_power;
binaryfunc nb_inplace_lshift;
binaryfunc nb_inplace_rshift;
binaryfunc nb_inplace_and;
binaryfunc nb_inplace_xor;
binaryfunc nb_inplace_or;
binaryfunc nb_floor_divide;
binaryfunc nb_true_divide;
binaryfunc nb_inplace_floor_divide;
binaryfunc nb_inplace_true_divide;
unaryfunc nb_index;
binaryfunc nb_matrix_multiply;
binaryfunc nb_inplace_matrix_multiply;
} PyNumberMethods;```
The journey continues...
I found how `abstract.c` and `ceval.c` and `compile.c` eventually end up checking binary operators and it led me here. It seems like these binaryfuncs are supposed to be the operations to carry out. Where are these defined?
they are defined on individual types. however, it's not necessary for a new operator to call into one of these slots
slots exist only for perfomance reasons, right? (and i guess they make it easier to define methods in CAPI)
They reduce the object size, making it easier to have a large number of instances.
IIRC they aren't really faster than normal objects though
i am talking about method slots in type objects
__slots__ were a little bit faster than regular attrs in versions before 3.12
I think so. They might also predate the magic methods historically, not sure
It's sort of a weird system with some odd edge cases (like how some operations have multiple slots)
two operations can occupy the same slot (like __set__ and __delete__ use the same slot)
and one operation can occupy two slots (+ operator might perform a number addition operation, or a sequence concatenation operation; slots for them are different)
if I understand correctly, "slot" is just a pointer in the type struct
the thing i dont understand is how is it possible to sometimes store c-function in the slot, and sometimes store python-function
is it storing a pointer to some struct that can help distinguish between these cases? kinda a tagged union
it stores a helper function that takes care of calling the Python function
I had to learn how this works to implement PEP 688
had to write both a wrapper to call the C slot from Python code and another wrapper to call the Python method from C code
I feel like i got offered the red pill or the blue pill and chose wrong after digging into how Python works
just put me back in the simulation lol
That helper function is the key part of how I got the first version of fishhook to work, it grabbed the pointer to the helper off of a python class and copied it into the c type class
where can i read about this helper function?
yea, is there any information on slots and what-not somewhere?
Objects/abstract.c
Can anyone tell me how to be a shark in algorithms?
Some good sites where i can test myself or understand in easy. I feel like my teacher is bad at formulate things so i try to Think about homestudy for that class.
Please don’t cross post the same message in multiple channels.
Idk
What would you want to talk about?
@final geode Would you or another member of the Microsoft team be willing to do a benchmark run on a branch of mine with superinstructions? JeffersGlass/cpython -> justin-supernodes-onlypairs https://github.com/JeffersGlass/cpython/tree/justin-supernodes-onlypairs
Local testing suggests it's ~6% faster than main + JIT currently, but I'd be curious what a benchmark on official hardware would look like
Sure!
Should be done in 2.5 hours or so.
Hm, is your branch based off of CPython's main, or my old justin branch? If the latter, the benchmarking may be using a commit prior to the JIT for the base comparison...
Just something to be aware of.
Good to know! Have not rebased it to current main yet - I'll look at the reference commit once the results are out and take that into account.
If the "official" benchmark results are still promising, I'll rebase on top of current main, and see about doing a larger/smarter selection of Superinstruction pairs
I'm not advocating for it, but has there ever been discussion about adding do-while to Python, or a documented reason for why it's never been part of the language?
searching my email archive for python-ideas found several, there was a thread "A suggestion for a do...while loop" in 2017
Looks like your branch failed to build.
python3.11 ./Tools/jit/build.py x86_64-pc-linux-gnu --file ./Tools/jit/superinstructions.csv
Traceback (most recent call last):
File "/home/benchmarking/actions-runner/_work/benchmarking/benchmarking/cpython/./Tools/jit/build.py", line 6, in <module>
import _targets
File "/home/benchmarking/actions-runner/_work/benchmarking/benchmarking/cpython/Tools/jit/_targets.py", line 14, in <module>
import _jit_c
File "/home/benchmarking/actions-runner/_work/benchmarking/benchmarking/cpython/Tools/jit/_jit_c.py", line 156
raise ValueError(f"Wrong number of first_nodes {len(first_nodes)=}\n{'\n'.join(str(f) for f in first_nodes)}")
^
SyntaxError: f-string expression part cannot include a backslash
that's valid in 3.12, right?
but I'm guessing we want a slightly bigger compatibility range
I think so, but our benchmarking infrastructure currently builds using 3.11, which is the current minimum required version.
Ah gotcha, didn’t realize that. I’ll tweak things to be 3.11 compatible and test locally, and ping you again when it’s ready?
Sounds good. Maybe do the rebase too while you're at it, if it's not too much trouble?
Can do! May not be today
i think someone here might might find this low-level edge-case interesting: #esoteric-python message
yall whats the use case of classes
#python-discussion is the right place for that question!
@final geode That justin-supernodes-onlypairs branch should now be Python 3.11 compatible - it was all things in f-strings that were legal in 3.12 but illegal in 3.11 (nested same-type quotes, and backslashes).
It's also rebased on top of main as of last night. Locally, I'm showing about ~4% speed increase over main+jit with 251 superinstruction pairs.
Cool, I'll fire it off.
Same as before, give it 2.5-3 hours.
Hm, I'm still getting a bunch of build errors (this time when compiling template_2.c). Are you able to build locally?
Like, hundreds of errors like this:
error: use of undeclared identifier 'next_instr'
error: use of undeclared identifier 'opcode'; did you mean 'opcode0'?
error: use of undeclared identifier 'opcode_targets
Let me have a look
Seems like you might need to redefine some macros or something.
Check out template.c for some examples.
I thought I had fixed this particular issue... let me see if I did a bad git again...
Longer error notes for a couple:
In file included from cpython/work/template_2.c:97:
cpython/Python/executor_cases.c.h:3825:17: error: use of undeclared identifier 'next_instr'
GOTO_TIER_ONE(target);
^
cpython/Python/ceval_macros.h:424:5: note: expanded from macro 'GOTO_TIER_ONE'
next_instr = target; \
^
In file included from cpython/work/template_2.c:97:
cpython/Python/executor_cases.c.h:3825:17: error: use of undeclared identifier 'next_instr'
cpython/Python/ceval_macros.h:425:5: note: expanded from macro 'GOTO_TIER_ONE'
DISPATCH(); \
^
cpython/Python/ceval_macros.h:99:9: note: expanded from macro 'DISPATCH'
NEXTOPARG(); \
^
cpython/Python/ceval_macros.h:153:30: note: expanded from macro 'NEXTOPARG'
_Py_CODEUNIT word = *next_instr; \
^
In file included from cpython/work/template_2.c:97:
cpython/Python/executor_cases.c.h:3825:17: error: use of undeclared identifier 'opcode'
cpython/Python/ceval_macros.h:425:5: note: expanded from macro 'GOTO_TIER_ONE'
DISPATCH(); \
^
cpython/Python/ceval_macros.h:99:9: note: expanded from macro 'DISPATCH'
NEXTOPARG(); \
^
cpython/Python/ceval_macros.h:154:9: note: expanded from macro 'NEXTOPARG'
opcode = word.op.code; \
^
Ah, I had committed one of the superinstruction templates to git instead of letting it be auto-generated each time, and it was using an outdated version. I'm going to leave that checked in for now, since that branch now builds successfully on a clean pull, and maybe move those templates to a TemporaryDirectory so they don't hang around.
Should now (actually) be good to go. Apologies for the trouble.
Queued it up. (Might take an our or so longer since the last job is still running the base commit.)
...the build failed again when linking before running the PGO task: gcc: error: Python/jit.o: No such file or directory
Well dang, my bad again, I didn't understand fully how the the jit dependencies in the makefile worked, which interacted badly since jit.c is built at compile time in this branch. I have fixed this.
This does currently work for me locally:
git clone https://github.com/jeffersglass/cpython.git
cd cpython
git switch justin-supernodes-onlypairs
./configure --enable-optimizations --with-lto=yes --enable-experimental-jit
make
make test
However since this is the fourth time I've claimed this is ready... let me try from scratch a couple more times and step through the steps in _benchmarking.yml as well, to see if there's anything else I can find that is broken.
Sorry for the mess, and thank you for your time
isn't that yes in --with-lto redundant
--with options in autoconf are automatically set to yes if they are the --with variant (not without) with no arguments passed to it
It may be - I’m just running what’s in the benchmarking script in bench_runner verbatim.
Your messages have been removed for being way off-topic
Can anyone point me to where Python does integer arithmetic in CPython? I've been searching the source code and have been having trouble locating this. IIRC CPython uses infinite-width integers and I want to see the actual source code for this
Objects/longobject.c line 3641
_PyLong_Add(PyLongObject *a, PyLongObject *b)```
worth remembering that ints are called "long" in the C source (if you've used Python 2 you might understand why)
elsewere in this file is the type definition for the long objects, where it has definitions for "slots" like nb_add that map to the arithmetic operations
from there you should be able to trace down to the functions where the actual arithmetic is done
is the _continuation module something Python has or is it a PyPy extension?
!e import _continuation
@dusk comet :x: Your 3.12 eval job has completed with return code 1.
001 | Traceback (most recent call last):
002 | File "/home/main.py", line 1, in <module>
003 | import _continuation
004 | ModuleNotFoundError: No module named '_continuation'
no, there is no such module
!e import _socket
@hybrid relic :warning: Your 3.12 eval job has completed with return code 0.
[No output]
Not the right channel, see #❓|how-to-get-help
will move there
Does someone know the algorithm that python uses to actually salt hashes
for strings
i.e. how it combines the deterministic hash of the string itself, with the randomly generated salt?
seems like its siphash, found a link to the paper, good enough
In [12]: exp = (TypeError, ValueError)
In [13]: try:
...: raise ValueError('hi')
...: except exp as e:
...: print(e)
...:
hi
I didn't expect this to work.
Interesting, will it also respect instancecheck
what is instancecheck?
what code could test this?
Create a metaclass with this Dunder, then pass its instance to the except clause
it doesn't
Good to know
... def __instancecheck__(self, arg): return True
...
>>> class E(Exception, metaclass=Meta): pass
...
>>> isinstance(ValueError(), E)
True
>>> try: raise ValueError()
... except E: pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
try: raise ValueError()
^^^^^^^^^^^^^^^^^^
ValueError
class Meta(type):
def __instancecheck__(self, arg): return True
def __subclasscheck__(self, arg): return True
class E(metaclass=Meta): pass
raise E()
Traceback (most recent call last):
File "D:\_.py", line 302, in <module>
raise E()
TypeError: exceptions must derive from BaseException
how do you get squiggles under the raise statement?
is it a recent 3.13 feature?
iirc error locations in repl are also 3.13 feature
yeah I was in 3.13
hey everyone, I know HKT PEP is currently stale, but I'm considering reviving it; any ideas are welcome in the discussions of https://github.com/nekitdev/peps
are python strings stored in UTF-8 internally
this is a little complicated but
If you're using PyPy, Python always stores strings as UTF-8
if you're using the official Python, cpython
If I remember correctly it switches between UTF-16 and UTF-32 depending on the context
CPython stores strings as either Latin-1, UCS-2, or UCS-4, depending on what's the highest codepoint.
The reason it doesn't do UTF-8 is because it's variable-length, so you can't do O(1) random access.
There is though a C-API function you can call to request a UTF-8 buffer. If it's all ASCII data it'll return the Latin-1 buffer (since it'll be identical), otherwise it'll allocate and cache an additional buffer for you. So you can treat them like they contain UTF-8 data.
not relevant here?
but really just enter the correct password lol
it's not a blue screen error
will python have piping```
a |> b |> c |> d == d(c(b(a)))
feel free to submit a pep
there might already be discussions about it. Do some research before making a suggestion.
Various versions of this has been suggested, but afaik always shot down. Personally I do quite a bit of work on Elm and I vastly prefer writing all my code avoiding that stuff. Makes my Elm look more like a lisp, but it's just much simpler to refactor and for someone who doesn't know functional languages well (including me!) to work with it.
btw this is doable without new syntax
you can override __rmatmul__ on functions and other callables to achieve that
but can that do this? ```pycon
5 |> print(2, $)
2 5
you'd also want something right associative like __rpow__
>> and << would be nice for piping in different directions
Yeah, this feature doesn't work well without currying or some shorthand syntax for partial application
idk if by partial application, you would include lambdas or not
Right now you'd have to do >> (lambda x: print(5, x))
I guess it's possible to do that, but a bit clumsy
Though even in Haskell/Elm you'll need a lambda or flip fn if the argument is in the "wrong" position
yeah, python lambdas sucking is definitely a big issue with this approach
C++ has similar issues with ranges. Not as extreme in the sense that C++ lambdas are less limited, but they are very verbose
You can make the pipe more syntactic by making it e.g. insert its result into a call as the first/last argument, akin to clojure arrow macros or raku feed operators.
placeholder notation is nice for pipes
yeah, you just still have a mess when it's in the wrong position
coincidentally i was just involved in a discussion about a placeholder proposal for C++
[&](){}()
it's just really brutal to not have nice lambdas from day 1, what can you say. very hard to retrofit. closes a lot of doors
[&] (auto x) { return 2*x; } more realistic example; yours is immediately invoked 🙂
Honestly, pipes aren't that great if you have to spam lambdas to get them to behave.
iife 🙂
Really, you want pipes from day 1 so that your APIs are designed with the arguments in the right places.
idk exactly how broadly you define "pipes" here, but lots of languages do the same kinds of things spamming lambdas and it looks great?
eh I think you have this backwards
i mean even something like https://github.com/chrisgrimm/better_partial works if you can wrap your callables
you have nice lambdas, because "piping" isn't something you do with a handful of pre-selected functions
it's a general appraoch to data manipulation
I guess kotlin's way works actually, that's true.
kotlin's way is basically "the way" at this point
C#, Rust, Swift
they all do something very similar
because it's a pretty simple approach that's flexible and works great. extensions + concise lambdas, boom.
I am more used to the concept in lisps and functional languages
actually
you could totally do this today already
pipe(x
, foo(_, 42)
, bar(1, 2, 3, _)
)
where pipe implicitly wraps its varargs with the better_partial decorator
you definitely can. it just somehow doesn't feel as nice when you're writing all your piping nested inside some function call
wait no
The arguments are fully evaluated before the function call
yeah, you'd need to pass them as lambdas
For the simple case, you could pass tuples
You'd need to pass the function and the args separately yeah
The better_partial lib supplies a decorator to decorate these functions with so you don't need to pass them in some lazy way
that's generally one of the nice things about lambdas though, and why they're such a good building block.
there are a decen tnumber of use cases where you want to pass operations around and not having them evaluated immediatley
yeah the goal here is to make this in a way that you can just import _ and pipe and have it work without any extra work
But yeah, I feel like anyone dabbling in esoteric Python has at some point written a shell-pipe based lib now
IIRC in R a function can choose whether to take an argument evaluated (as in Python) or as some kind of AST-node-like thing
ah R
(I implemented this in Python once, although obviously not as convenient as in R land)
apparently i have written something i've never heard of before
makinator
hi guys, could anyone tell me how take sliding window of length 3 k=3 for py nums = [1, 3, -1, -3, 5, 3, 6, 7] I did something like this but it is not right.
for i in range(len(nums)):
arr = nums[i:i+k]```
could anyone tell me the logic behind creating a sliding window?
#1035199133436354600 in the future but what do you want it to look like?
you can just discard the lists that arent of len 3
i think they were already assessed in #python-discussion
hi there.
i have asked a question there(https://discord.com/channels/267624335836053506/1214905366018785280) hours before.
and @subtle vector pointed out that this wierd behiver is only in python 3.12, not in 3.11.
so i m wondering, is that a BUG of python 3.12?
i did not find it in bugs.python.org .
is that a python`s BUG? or Works As Intended?
!e import dis; dis()
@feral island :x: Your 3.12 eval job has completed with return code 1.
001 | Traceback (most recent call last):
002 | File "/home/main.py", line 1, in <module>
003 | import dis; dis()
004 | ^^^^^
005 | TypeError: 'module' object is not callable. Did you mean: 'dis.dis(...)'?
It's to decide whether to show the "Did you mean" suggestion
😮 .. that actually makes sense.
AH. thank you a lot. I'm sorry to bother you.
on_error:
if callable(getattr(mod, mod.__name__, None)):
show_nice_error()
the nice error detection has side effect.....😅
range literals 😮
https://peps.python.org/pep-0204/
Python Enhancement Proposals (PEPs)
Rejected
i do enjoy, however, that
[ TBD: Guido, amend/confirm this, please. Preferably both; this is a PEP, it should contain all the reasons for rejection and/or reconsideration, for future reference. ]
is still there
this is actually intuituitve ```py
[1:10]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
[:5]
[0, 1, 2, 3, 4]
[5:1:-1]
[5, 4, 3, 2]
i like it
yeah
it's reasonably intuitive, but so is list(range(5, 1, -1))
i think the point is being able to do it implictly, list over range can be confusing for newbies
but now ranges aren't lists so 🤷♂️
reusing existing syntax for a new thing is also confusing for newbies. Going from x[1:10] (a slice of a sequence) to [1:10] (a range literal) is not gonna be obvious, since they create two different types of objects
and newbies do learn type conversions pretty quickly - at least int(x) and str(x). It's not a big leap from there to list(x)
fair
I'm using Ubuntu-22.04 on wsl which comes with python 3.10. I downloaded 3.12 and the pip along with it.
Now I'm getting this error: ModuleNotFoundError: No module named 'apt_pkg'
determine the behavior of these 3 lines of code ```py
[5, 6, 1:6, 7, 9]
[x:y for x in (1, 2) y in (3, 4)]
[(8:14:-1)]
1.) invalid, range literals only work with integers, not tuples
2.) invalid, there are no range comprehensione
3.) debatable. Could be allowed, or forbidden.
: already binds tighter in getitem so the first should work, and for the second I don't see how it'd be different from any other expression in the list comp?
1.) not range literals but for consistency : should bind as tight as in np_arr[x:y, a:b]
2.) okay, so stick to [range(x, y) for x in (1, 2) for y in (3, 4)]
3.) let's say your point 1 was valid; [5, 6, (1:6), 7, 9]
this is how julia handles those
julia> [5, 6, 1:6, 7, 9]
5-element Vector{Any}:
5
6
1:6
7
9
julia> [x:y for x in (1, 2) for y in (3, 4)]
4-element Vector{UnitRange{Int64}}:
1:3
1:4
2:3
2:4
julia> [(8:14:-1)]
1-element Vector{StepRange{Int64, Int64}}:
8:14:7
this julia looks like python
as expected.. i guess
ig maybe you don’t need [ and ] to create a range literal?```
0:5 == range(5)
1:2 == range(1, 2)
[1:7] == [range(1, 7)]
[*1:7] == list(range(1, 7))
[]-less syntax for range literals sounds like it'd clash with the syntax for type annotations - theoretically you'd want to be able to make ranges out of variables and not just literals (i.e. like doing x = start:end), but if you were to put that range literal by itself as a line of code, its indistinguishable from a type annotation
1..7 but that’s new syntax so fair enough
would it clash? The annotations are on the name side of assignments or definitions so you ahve a = separating it
I would take new syntax over a:b being a slice sometimes and a range other times.
there's a clash for just x: y as a statement
sure
what would step range syntax be by then tho
0..10..2?
Ah yeah that would be ambigous, but considering it's a noop when interpreted as a range I think it'd be fine if it was the annotation
0..10 % 2?
TIL : binds tighter. I'd still keep that case invalid and allow only "simple" range literals.
Does PEG grammars have regex support? what does Python use for such things? like how would a comment rule look like?
comment: '#' what_here?
This is not allowed based on the published paper.
([\n]!)*
you'd probably handle that in the tokenizer
I mean, shouldn't the grammar handle comments?
.gram file doesnt seem to mention it.
they're stripped out in the lexing stage
otherwise it'd be very annoying because comments can appear everywhere, so you'd have to add them to every single rule
well yes, I understand that.
btw stand-alone x:y in my python repo basically creates a slice object
it's a lot more consistent and expected
not really, it's only valid in unambiguous contexts
it's like a named expression
it is a breaking change for anything that uses annotations, like dataclasses
But there's no real use in interpreting it as a range in that context
I guess there could be some class magic that can work with the standalone expression but that's an extreme edge case and could work with the normal constructor
it's still semantically different (x:y requires x and y to be bound variables)
(and of course with metaclasses nothing is safe)
tell me an annotation that's in the form of (x:y) or (a, x:y)
if a literal was restricted to those contexts it wouldn't be breaking
either range or slice literal
sure
I don't have any issues with default arguments and how they're specified wrt mutable objects. Though if they didn't currently exist as a feature, and the specification were being debated, do you think the consensus would be that the default argument should essentially be a no-argument lambda?
def my_func(a, b=[]): # `[]` is an expression that's re-evaluated each time `my_func` is called without a value for `b`.
...
Not sure. The other side of this is also a common gotcha (late binding).
And you wouldn't even have the same counterspell (giving the function/lambda a default argument and putting the value into the closure)
what's the other gotcha here?
FWIW this is how default arguments work in pretty much all other languages I can think of.
!e
funcs = []
for i in range(3):
def func():
print(i)
funcs.append(func)
for func in funcs:
func()
@quick snow :white_check_mark: Your 3.12 eval job has completed with return code 0.
001 | 2
002 | 2
003 | 2
what does that have to do with default arguments
It would make nested functions always behave like this.
You couldn't solve this by giving func a i=i argument anymore.
previously doing def func(i=i) changed that behavour to do 0 1 2
but then you say: "you couldn't use (abuse) default arguments to do this thing"
sure. that's two separate points.
If arguments were late-bound, then the default of the nested function would be re-evaluated on every call
I think I'm pretty okay withat, tbh, I think func(i=i) is pretty hacky and bad for readability
but I do agree that having default arguments computed at call time makes more sense
So you couldn't use this quirk to get nested functions to behave in a someetimes more intuitive fashion
Right, I understand. but I don't think that the result is really much more intuitive.
Sure, it's a major breaking change though
oh, for sure, no argument there.
Personally I think late-bound arguments are generally the better default, but it's too late in Python's evolution as a language to change the behavior now
but "breaking change of existing behavior" was never in this discussion
the original comment was:
Though if they didn't currently exist as a feature, and the specification were being debated, do you think the consensus would be
And then that was followed up by a link to a proposal that would give it special syntax
Adding late-bound arguments as an option might be feasible but all the syntax feels ugly to me
Agree on the first point
THe second, don't have much opinion on. Having half a dozen
if x is None:
x = dict()
Consider this:
def foo():
x = 1
def bar(y=x):
print(y)
return bar
You'd have to scan that "lambda" for any mentions of names that occur in the defining frame's locals.
What should happen if that expression doesn't resolve (e.g. typo) at call time? NameError?
I don't know. Maybe I've just used Python so long that only the current behavior makes sense to me.
at the start of functions is also pretty ugly
x = x or {} :)
def f(x=types.MappingProxyType({}) 😄
abusing truthiness, you truly have used python for too long 😛
It's use, not abuse. Quack. :P
😛
Yeah, I wouldn't use a non is check there
as for your example, idk, that example just isn't going to behave intuitively IMHO, in general, because it's not intuitive code.
it's cool to demonstrate quirks of python but in a real codebase you best believe anyone who writes that is just getting told to rewrite it
so whether early binding defaults make that example more or less intuitive, is for me, a total non-factor
The initial example I mentioned has bitten most Python devs I've met (and talked about Python with) in real world code
what's important is that early binding makes the 99.99% common case unintuitive
There are ways to solve it without the default argument abuse though
but that example has nothing to do, directly with default arguments...
it was a demonstration of how default argument hacks can help solve that problem
for example ```py
@default_arg(name=value)
def inner_function(a, name): pass
but they're not the cause of the problem
Isn't the exact same thing that causes the headaches with default arguments, what also causes the problem with your example?
early binding?
I just tried this with kotlin fwiw, to keep it with GC languages:
fun main() {
val funcs = (1..3).map { {print(it)} }
for (f in funcs) {
f()
}
}
Prints 123
No, the problem in the example is the same that lazy default args would introduce: late binding.
🤷♂️
so far all your example shows is that python's behavior around this stuff is very unintuitive in two common examples
and you're basically saying you wouldn't want to see one fixed, because that would remove a hacky solution for the other
Nobody's talking about fixing anything anyways; this isn't going to change. It's about "what if Python was designed today".
I find the current behavior very intuitive, I don't have anything else to say I haven't already.
I've never met someone who said they found the result of
def foo(x=[]):
x.append(5)
print(x)
foo()
foo()
intuitive before, so that's interesting for sure
fwiw, kotlin does the same thing as python here if you use the same variable in all the lambdas - kotlin creates a new variable in for loops (and obviously map, python would do the same thing with map too), so it avoids the weirdness by having more scopes. e.g.
fun funcs(): List<() -> Int> {
var res: MutableList<() -> Int> = MutableList(0){{0}}
var i = 0;
res.add{i}
i++
res.add{i}
i++
res.add{i}
for (j in 1 .. 3) {
res.add{j}
}
return res
}```does 2 2 2 1 2 3
For sure, this is definitely kind of weird too
the thing is that i++ "should" create a new variable as well
LIke, your brain would like to believe:
var i = 0;
val closure = i;
i += 1;
print(closure)
and closure here is still 0
at least, my brain would like to believe that.
it's like the closure is binding to the name itself, rather than to the value the name is bound to, which is how assignment works in these languages
It's more common since it lets you mutate the variables from inside the closure, that is
def foo():
hits = 0
def handler():
nonlocal hits
hits += 1
run_thing(handler)
```will work (in languages without `nonlocal`, it is the only useful default).
(fwiw java does forbid this, you cannot mutate variables if they are also used in a closure, both from inside and outside the closure).
If it worked like traditional assignment, this would be inexpressible.
I would argue this should be the programmers choice, like in some algol and C++, but that then gives you yet another binary choice to teach.
It's a bit inprecise
Things could still be mutated from inside the closure
They just couldn't be reassigned
Ye, that's what I meant
I have had to write int[] hits = new int[1] in java exactly once for exactly this reason.
but arguably this is no differnt than how functions cannot "mutate" their integer arguments
Ye, and that is also occasionally annoying
the solution is just to find a different way to express it, and given all th epotential headaches with shared mutability in GC languages
is this ever really a bad thing, considering the long term?
import tkinter as tk
class App():
def __init__(self):
self.root = tk.Tk()
self.root.geometry('350x350')
self.root.title('Example')
self.root.mainloop
return
if __name__ == '__main__':
App()```
when running this snippet on the REPL, after putting in the code and then hitting enter, the GUI opens.
however this won't run when you do it via python main.py because it's missing the parens around mainloop, i feel this is a bug
@spark verge asking here since the thread closed
It will flash open the window and close it again, it does run
it doesn't even flash the windows and close it for me
it simply runs without opening the GUI
as expected
But if you put a time.sleep in somewhere then you'll see it
Probably the issue is the _ keeping _.tk alive
If you hit newline/carriage return maybe the window will close
the UI only opens when you hit enter (on repl)
Right
so shouldn't this be a bug regardless? not sure
Does it disappear after _ gets reassigned?
Oh weird maybe that's a bug that tk doesn't cleanup on __del__
Hi, all - I wanted to have a discussion on Python Docstrings styles, comparing two specific styles. Is this the right channel or should this go in a Help Channel? Didn't want to post full blurb until I was in the right space.
Hmm there's no formatting-and-linting channel but maybe there should be
or a documentation channel?
I'll post here for now, @spark verge / @feral island , and I can always pivot if need be!
A few weeks ago, I discovered the following style of DocStrings and really liked it:
def fahrenheit_to_celsius(fahrenheit):
"""Converts Fahrenheit to Celsius.
Args:
fahrenheit: The temperature in Fahrenheit. (float)
Returns:
celsius: The temperature in Celsius. (float)
"""
However, yesterday, I discovered another style that I can sympathize with as a good style (albiet a bit more busy/cluttered):
def repeatStr(userStr, repeatTimes=5):
"""
Accepts user input string & returns string output repeated on separate lines.
Args:
userStr: string
The string to be repeated.
repeatTimes: int (Optional, Default=5)
The number of times to repeat the string.
Returns:
outputStr: string
The string repeated on separate lines.
"""
The 1st style to me feels more Pythonic, although I could see it quickly broaching line length limits from various formatters/linters.
I wanted to get ya'll's thoughts on each or a preference as I work to build my own understanding of best practices/personal preferences.
There are different styles used by different projects, I think "Google-style" and "numpy-style" are common terms but I haven't familiarized myself with them much. In your second style, I don't like that the default value is repeated from the signature. (And the type too, if you add type annotations, as you probably should.) That is bad because there's always a risk that the docstring and code go out of sync with future changes.
There's always PEP 727
As I pressed <Enter>, I had the same thought as you about the repeating of the default value (not very DRY).
Maybe not "always" 🙂
This one is new to me - I knew about PEP 8 and PEP 257, but not 727. Reading now
It's a draft and unlikely to be accepted, for what it's worth
I love that 727 surveys other languages to view how it's done there. Makes me think Style #2 I posted was inspired by a former Java dev
here's how the author of PEP 727 uses it (via typing-extensions)
https://github.com/tiangolo/fastapi/blob/9aad9e38686b06d207d55b51584e1a9910c51761/fastapi/applications.py#L64-L822
this is kinda scary for me...
the __init__ signature is more than 750 lines long
also, all the languages they surveyed used a microsyntax inside the docstring, so it's not clear what that survey is for 😛
Take this: ```py
routes: Annotated[
Optional[List[BaseRoute]],
Doc(
"""
Note: you probably shouldn't use this parameter, it is inherited
from Starlette and supported for compatibility.
---
A list of routes to serve incoming HTTP and WebSocket requests.
"""
),
deprecated(
"""
You normally wouldn't use this parameter with FastAPI, it is inherited
from Starlette and supported for compatibility.
In FastAPI, you normally would use the *path operation methods*,
like `app.get()`, `app.post()`, etc.
"""
),
] = None,
You could make it a little less verbose by just improving the writing inside the docstringspy
routes: Annotated[
Optional[List[BaseRoute]],
Doc("Routes to serve incoming HTTP and WebSocket requests."),
deprecated(
"""
It is inherited from Starlette and supported for compatibility.
Instead use the path operation methods, like app.get(), app.post(), etc.
"""
),
] = None,
but that's still more verbose thanpy
routes: Optional[List[BaseRoute] = None,
Routes to serve incoming HTTP and WebSocket requests.
deprecated: It is inherited from Starlette and supported for compatibility.
Instead use the path operation methods, like app.get(), app.post(), etc.
Some languages support annotations for parameters:
@doc("Routes to serve incoming HTTP and WebSocket requests.")
@deprecated(
"""
It is inherited from Starlette and supported for compatibility.
Instead use the *path operation methods*, like `app.get()`, `app.post()`, etc.
"""
)
routes: Optional[List[BaseRoute] = None,
``` though this is a pretty big addition to the language
It always felt like this would need an ide to nicely work with as it adds quite a bit of noise to the signature
typing alone is a bit too much already in some cases
Yeah, it acts as a painful roadblock when reading the code
WIth a single docstring, you can at least click a > button on the left of most editors (and on GitHub) and hide it all
That's a big part of PEP 727, IDE support & implementation (since it would have a lot of impact there)
Yes but having to use a properly configured IDE to read some code isn't ideal
IDE doesn't have to be the only way to read the code - it's my understanding this would flow through things like python3 -m pydoc any_module_name_here , unless I'm overly optimistic about PEP 727 implementation
Tying back to this, I think I'm leaning towards Style #1 based on the above convo until a more formal definition comes out.
def repeatStr(userStr, repeatTimes=5):
"""
Accepts user input string & returns string repeated on separate lines.
Args:
userStr: The string to be repeated. (string)
repeatTimes: The number of times to repeat the string. (int) <Optional>
Returns:
outputStr: The string repeated on separate lines. (string)
"""
Sample with the Optional added in angle brackets & default removed from Args, only shown in the parameter list (where readers can know it's defaulted since it has a value set)
Not just that, but it seriously hurts code review, as some sample git patches showed.
Now I'm thinking it's redundant to state Optional when a default value is set
I feel like this style of parameters often just restates what has already been said
def repeatStr(userStr: str, repeatTimes: int = 5) -> str:
"""
Repeat string on separate lines
""""
annotations for individual parameters are more useful when they are more or less independent. Like if the function had a boolean flag that changed how it behaves in an edge case
But Optional doesn't mean "may be omitted", it means "...or None"
Are you saying the specific descriptions I gave for the Args/Returns are apparent from the variable names so there's no need to state them?
Some from the names, some from the first sentence of the docstring
In the case of the above, a default value is set, so "Optional" to me meant "It's optional for a user to update this beyond the default value, but not required for successful execution". In your statement of "or None", my intent with a default value is the program executes whether or not that variable is supplied elsewhere as a parameter, so I think we're saying the same thing?
yeah, "optional argument" does mean that, it's the typing.Optional that's the misnomer
Thanks for clarifying; I hadn't even looked at typing for use at all.
The samples I put above are from me trying to teach my friend programming & get them to write docstrings at all 😅 From that sense, I'd rather someone put too much into a Docstring than not enough or not write one at all
I'd rather someone put too much into a Docstring than not enough or not write one at all
I'm not quite sure about that. I think it's worth teaching to keep the right balance
Comments aren't free, they require maintenance. An incorrect, redundant or confusing comment makes the code harder to read, not easier
"too much" meaning my sample from 1:44 pm MT. If that's "too much", that's way better than none at all; I can see the value of your suggestion from 2:01 PM MT, but it comes across a bit more difficult to interpret as I imagine a beginner reading it (similar to if I asked him to write a list comprehension already... that would be ill of me so soon)
Comments aren't free, they require maintenance
I've seen more issues from a lack of comments than ones that are present AND incorrect/confusing; have you seen a different mix in your experience?
Yeah, especially if the docstrings are mandatory (i.e. if every function and class requires a docstring or the CI will fail)
I've never been [fortunate enough to be] somewhere where functions/classes require docstrings, ha
Here's an example from our bot:
https://github.com/python-discord/bot/blob/e3e421b5012202778557acd7c44801d5adb9a5b4/bot/exts/info/codeblock/_parsing.py#L57-L62
it restates the name of the class, but doesn't try to explain what tick is
bot/exts/info/codeblock/_parsing.py lines 57 to 62
class CodeBlock(NamedTuple):
"""Represents a Markdown code block."""
content: str
language: str
tick: str```
It's going to be a mystery what tick is, if you don't already know that the Python bot will complain at messages using the "wrong tick"
'''py
print(42)
'''
That is indeed a good example of that. Had me turning my head and squinting my eyes until I realized the problem in your sample.
is garbage collection called between cpython frame objects within the eval loop? or when is it called?
whenever you least expect it
more seriously, I think it can currently happen after every bytecode instruction? previous versions allowed it anywhere when an allocation was triggered
now how bad of an idea is this
import foo
bar = foo(…)
it's possible, but I want it to become a not bad practice, lol 😁
#python-discussion message
so, what are the chances of that? as far as I know, stuff like module level __getattr__ is a thing that's recognized by linters, so why not do that for other module level "magic" methods such as __call__ and so on
!pep 726
could certainly save some boilerplate in smaller modules
nooo, my hopes and dreams... crushed 
oh wait there’s hope
apparently if they come up with a good reason for it theyll consider revisiting
you can click through to the rejection message under "Resolution"
the reasons are in the PEP or the discourse discussion
“”" Hi, thanks for writing up the nice PEP spawning the lively discussion around callable modules. The steering council has discussed this PEP and decided to Reject it. (as I mentioned we were leaning towards in PEP 713: Callable Modules - #66 by gpshead) We didn’t feel that there was a compelling reason to have it even though it clearly could...
i saw
thanks!
on an unrelated note: is there a way to add a gdb breakpoint within a .py file?
Breakpoints In Python (Debugging with GDB)