
Thank you very much for this exhaustive explanation and example. I really like it and agree with you that the implementation provided by your example is much more well designed. The problem I have with it is that I feel like it assumes that I have a way to introduce such changes when writing the API, whereas I was talking more about using existing solutions. The provided example was based on common situations encountered when writing the code in Django. It may be that I'm using this tool not to its full potential, but it seems like many of your points were about bad API design, not the actual maybe-dot operator. Such operator would make it much easier to work with those frameworks that do not allow for more readable/well thought-out solutions. I'm thinking about it as a syntactic sugar that can speed things up (and even make it more readable) in a situation where there is no time and/or budget to come up with a better architecture (like e.g. the one you have provided). The reality is that usually from the business perspective nobody wants a well designed system, only the functioning one. It may be that I misunderstood your response and didn't provide a valid answer - please let me know in that case. Best regards On Tue, Oct 19, 2021 at 1:29 AM Steve Dower <steve.dower@python.org> wrote:
Okay, I'll let myself get sucked into responding ONE TIME, but only because you gave me such a nice API to work with :)
On 10/18/2021 9:11 PM, Piotr Waszkiewicz wrote:
class User(DBModel): phone: str | None
class Publisher(DBModel): owner: ForeignKey[User] | None
class Book(DBModel) publisher: ForeignKey[Publisher] | None
Imagine wanting to get the phone number of the person that published a certain book from the database. In this situation, with maybe-dot operator I can write:
phone_number = book.publisher?.owner?.phone
Consider today, you wrote this as "book.publisher.owner.phone". You would potentially get AttributeError, from any one of the elements - no way to tell which, and no way to react.
Generally, AttributeError indicates that you've provided a value to an API which doesn't fit its pattern. In other words, it's an error about the *type* rather than the value.
But in this case, the (semantic, not implementation) *type* is known and correct - it's a publisher! It just happens that the API designed it such that when the *value* is unknown, the *type* no longer matches.
This is PRECISELY the kind of (IMHO, bad) API design that None-aware operators will encourage.
Consider an alternative:
class ForeignKey: ... def __bool__(self): return not self.is_dbnull
def value(self): if self.is_dbnull: return self.Type.empty() # that is, DBModel.empty() return self._value
class DBModel: @classmethod def empty(cls): return cls(__secret_is_empty_flag=True)
def __bool__(self): return not self._is_empty
def __getattr__(self, key): if not self: t = self._get_model_type(key) return t.empty() if isinstance(t, DBModel) else None ...
class User(DBModel): phone: str | None
class Publisher(DBModel): owner: ForeignKey[User]
class Book(DBModel) publisher: ForeignKey[Publisher]
Okay, so as the API implementer, I've had to do a tonne more work. That's fine - *that's my job*. The user hasn't had to stick "| None" everywhere (and when we eventually get around to allowing named arguments in indexing then they could use "ForeignKey[User, non_nullable=True]", but I guess for now that would be some subclass of ForeignKey).
But now here's the example again:
book.publisher.owner.phone
If there is no publisher, it'll return None. If there is no owner, it'll return None. If the owner has no phone number, it'll return None.
BUT, if you misspell "owner", it will raise AttributeError, because you referenced something that is not part of the *type*. And that error will be raised EVERY time, not just in the cases where 'publisher' is non-null. It takes away the random value-based errors we've come to love from poorly coded web sites and makes them reliably based on the value's type (and doesn't even require a type checker ;) ).
Additionally, if you want to explicitly check whether a FK is null, you can do everything with regular checks:
if book.publisher.owner: # we know the owner! else: # we don't know
# Get all owner names - including where the name is None - but only if # Mrs. None actually published a book (and not just because we don't # know a book's publisher or a publisher's owner) owners = {book.id: book.publisher.owner.name for book in all_books if book.publisher.owner}
# Update a null FK with a lazy lookup book.publisher = book.publisher or publishers.get(...)
You can't do anything useful with a native None here besides test for it, and there are better ways to do that test. So None is not a useful value compared to a rich DBModel subclass that *knows* it is empty.
---
So to summarise my core concern - allowing an API designer to "just use None" is a cop out, and it lets people write lazy/bad APIs rather than coming up with good ones.
Cheers, Steve _______________________________________________ Python-Dev mailing list -- python-dev@python.org To unsubscribe send an email to python-dev-leave@python.org https://mail.python.org/mailman3/lists/python-dev.python.org/ Message archived at https://mail.python.org/archives/list/python-dev@python.org/message/BRTRKGY6... Code of Conduct: http://python.org/psf/codeofconduct/