[HN Gopher] OO in Python is mostly pointless
___________________________________________________________________
OO in Python is mostly pointless
Author : leontrolski
Score : 95 points
Date : 2021-01-27 19:58 UTC (3 hours ago)
(HTM) web link (leontrolski.github.io)
(TXT) w3m dump (leontrolski.github.io)
| choppaface wrote:
| I feel the major reason OO doesn't fit well in Python is because
| Python was designed to harness "duck typing," which enables
| polymorphism with less code. Furthermore, attrs / dataclass does
| away with a lot of the Object boilerplate that most developers
| would prefer not to write when moving quickly.
|
| To top it all off, context of use matters. For scripts without
| tests or limited extensibility, the value of OO is mostly in
| clarity to the reader, and here attrs / dataclass can be very
| concise and effective.
|
| That said, when the object jungle gets very large, Python gets a
| lot more tricky. Look at matplotlib or try to write a billing
| system with a variety of account and transaction types.
|
| Nevertheless it's nice for a post like this to call out the
| existence of the bijection between OO and 'non-OO' Python
| programming styles. The issue is worthy of contemplation for any
| early Python programmer once one gets the feet wet.
| mumblemumble wrote:
| I'm not sure I follow. Python's duck typing explicitly relies
| on its object-oriented features. I'm not sure that it's even
| possible to implement duck typing in a language that isn't
| object-oriented.
| banthar wrote:
| Object oriented programming is not about defining classes. It's
| about using objects. You don't have to define new classes to do
| OOP. Just use existing classes and objects polymorphically!
| url_layout.format() resp.raise_for_status()
| resp.json() session.add()
|
| All those calls are dynamically dispatched - the essence of
| object oriented programming. This is what allows you to not worry
| about: * which string implementation `url_layout` uses * which
| HTTP protocol, encryption, authentication, chunking `resp` uses *
| what database is connected to `session`
|
| You cannot avoid using objects - that's how all modern operating
| systems work.
|
| Using classes without the need to call them polimorphically just
| as a nice namespace for methods is a separate issue.
| blt wrote:
| One good use for Python's OOP is operator overloading. Since
| Python doesn't have type signatures, you can't overload the
| "plus" operator with a free function like you can in C++. Writing
| numerical code without operator overloading is painful.
| ben509 wrote:
| I'd argue that's not object-oriented programming.
|
| You are using Python's class mechanism, but that's where the
| OOness ends. The types themselves are immutable value types,
| carrying no state and reacting to no messages, and they don't
| exploit a class hierarchy beyond having a common "Number"
| class. The "methods" don't even behave like methods, e.g.
| __add__(self, other) has special wiring so it obscures which
| side of the addition is "self".
|
| That's very different than a classic OO scheme like a UI
| toolkit.
| mumblemumble wrote:
| I don't know if one has really made any case at all about OOP,
| neither for nor against, if one hasn't considered cases that
| involve any sort of conditional branching.
|
| Because the key problem that OOP is supposed to solve is not
| lumping bits of data together. The problem that OOP is supposed
| to solve is using polymorphism to limit the proliferation of
| repetitive if-statements that need to be maintained every time
| the value they're switching on acquires a new interesting case to
| consider.
| bccdee wrote:
| In practice, I've found that kind of use case for polymorphic
| dispatch to not be especially common -- meanwhile, most OO
| languages strongly encourage making everything objects. There's
| absolutely a time and place for a good abstract interface, but
| it's always struck me that classes are overused in general.
|
| That's why I really appreciate the go/rust model, where you can
| tack interfaces and methods onto structs if you want to, but
| there's no pressure to do so.
| Mikhail_Edoshin wrote:
| This is basically how I use Python :) Truly, recently I tried to
| remember how to create a class method so that I can have
| different constructors and I forgot! Got back go just structures
| and functions.
|
| Most OO in most OO languages is just polymorphism and inheritance
| is merely a roundabout way to construct or traverse the method
| graph for polymorphic objects. It's often much easier to do
| polymorphism directly and only if you really need it: quite often
| you can fare well without polymorphism at all and simply write
| branching instructions.
|
| The useful OO must be about changing state in a controlled
| manner, but this remains largely unaddressed.
| nicbou wrote:
| Django's class-based views are a perfect example of well-written
| OO python.
|
| A view takes a request, gets some data, and renders a response.
|
| Each part of the process (and its subparts) is abstracted in
| methods that can be overridden. This lets you implement certain
| things (authentication, formatting, compression, caching,
| logging, pagination) once in an abstract class or mixin, and add
| it to all your views.
|
| Of course, Django already has those classes and mixins.
|
| This means you can write very simple views. Just define the
| queryset and the template, and you're done. Everything else just
| works, because it's already implemented in well-tested parent
| classes.
| jmkr wrote:
| Why not have the client stored in a dictionary, it's just a url
| location, instead of having a dataclass? You don't even need a
| "construct_url" function.
| OnlyMortal wrote:
| Nonsense. Everything been a dictionary is incredibly important.
| david422 wrote:
| OOP also works well when you want different _behavior_. In your
| bag of functions you have the same behavior for all objects. If
| you want different behavior, you're going to have to start adding
| branches in code that really has no business making those branch
| decisions.
| nemetroid wrote:
| When writing Python, I usually try to follow this item from the
| C++ Core Guidelines (replace "struct" with "dataclass"):
|
| > C.2: Use class if the class has an invariant; use struct if the
| data members can vary independently
|
| > An invariant is a logical condition for the members of an
| object that a constructor must establish for the public member
| functions to assume. After the invariant is established
| (typically by a constructor) every member function can be called
| for the object. An invariant can be stated informally (e.g., in a
| comment) or more formally using Expects.
|
| > If all data members can vary independently of each other, no
| invariant is possible.
|
| https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines...
| throwawaygal7 wrote:
| the author is right of course, but in practice he's wrong.
|
| I've been in a 500k loc codebase mostly in python. As things got
| complicated, instead of the couple of arguments seen in his
| example you need up with half a dozen or more, or variable
| argument lists, people passing callbacks with callbacks and all
| sorts of shit like that instead of just writing an object and
| context manager or two.
|
| In a large codebase these rapidly become very difficult to reason
| about and are better structured as objects with a discrete set of
| operations.
| leontrolski wrote:
| (Author here) I used to think that, but I've since found myself
| working on a similar sized codebase where we use very little OO
| and none of these problems have manifested themselves. Avoiding
| large function signatures and passing higher order functions
| around takes a little discipline, but wrapping those things
| with a bit of OO doesn't make the root problems go away, it
| just encourages their proliferation. (To reiterate the article,
| I'm by and large talking application development as opposed to
| library development).
| dvfjsdhgfv wrote:
| When Python was being conceived OOP was all the rage, with
| "everything is an object in Python" motto. Even if you want, you
| can't have bare structs, you need to create a class. At this
| point refusing to add a method to it and using external functions
| to modify its state is just a matter of taste - in many cases it
| makes sense, and in several it doesn't.
| zmmmmm wrote:
| So the example manages to evade any need for encapsulation. It's
| viable because the internal hidden state is so cheap to compute
| (the full URL) that it can afford to reconstruct it within every
| call.
|
| But imagine if constructing the url was a more expensive
| operation. Perhaps it has to read from disk or even make a
| database call. Now you really need to cache that as internal
| state. But doing that means external manipulation of root_url or
| url_layout will break things. Those operations need to be
| protected now. So do you make "set_url_layout" and "set_root_url"
| functions? And hope people know they have to call those and not
| manipulate the attributes directly? Probably you'd feel safer if
| you can put that state into a protected data structure that isn't
| so easily visible. It makes it clear those are not for external
| manipulation and you should only be interacting with the data
| through its "public interface".
|
| Of course this brings about all the evils of impure functions,
| mutated state etc. But if you are going hardcore functional in
| the first place then it becomes rather obvious OO is not going to
| fit well there, so its a bit of a redundant argument then.
| gfalcao wrote:
| I disagree that it's mostly pointless because if you're
| practicing Test-driven development OO provides a great way to
| abstract agents and their actions.
|
| I've seen a lot of python code that looks like the example from
| the blog but that's simply just a narrow example and does not
| reflect all the potential of OO, not only in Python but in any
| programming language.
|
| The thing I see in common in such poorly designed OO code is that
| it is usually not covered with unit tests that are actually
| useful and informative. Sometimes it's just not even tested at
| all.
| gfalcao wrote:
| At the end it comes down to how the developer designs their
| software, as Fred Brooks argued in "No Silver Bullet"
| (https://en.wikipedia.org/wiki/No_Silver_Bullet)
|
| Brooks goes on to argue that there is a difference between
| "good" designers and "great" designers. He postulates that as
| programming is a creative process, some designers are
| inherently better than others. He suggests that there is as
| much as a tenfold difference between an ordinary designer and a
| great one. He then advocates treating star designers equally
| well as star managers, providing them not just with equal
| remuneration, but also all the perks of higher status: large
| office, staff, travel funds, etc.
| gfalcao wrote:
| Section from "No Silver Bullet"
|
| _Object-oriented programming._
|
| Many students of the art hold out more hope for object-
| oriented programming than for any of the other technical fads
| of the day.
|
| I am among them.
|
| Mark Sherman of Dartmouth notes that we must be careful to
| distin- guish two separate ideas that go under that name:
| abstract data types and hierarchical types, also called
| classes.
|
| The concept of the abstract data type is that an object's
| type should be defined by a name, a set of proper values, and
| a set of proper operations, rather than its storage
| structure, which should be hidden.
|
| Examples are Ada packages (with private types) or Modula's
| modules.
|
| Hierarchical types, such as Simula-67's classes, allow the
| definition of general interfaces that can be further refined
| by providing subordinate types.
|
| The two concepts are orthogonal -- there may be hierarchies
| without hiding and hiding without hierarchies.
|
| Both concepts represent real advances in the art of building
| software.
|
| Each removes one more accidental difficulty from the process,
| allowing the designer to express the essence of his design
| without having to express large amounts of syntactic material
| that add no new information content.
|
| For both abstract types and hierarchical types, the result is
| to remove a higher-order sort of accidental difficulty and
| allow a higher-order expression of design.
|
| Nevertheless, such advances can do no more than to remove all
| the accidental difficulties from the expression of the
| design.
|
| The complexity of the design itself is essential; and such
| attacks make no change whatever in that.
|
| An order-of-magnitude gain can be made by object-oriented
| programming only if the unnecessary underbrush of type
| specification remaining today in our programming language is
| itself responsible for nine- tenths of the work involved in
| designing a program product.
|
| I doubt it.
| jennyyang wrote:
| Pointing to someone's bad code example and saying "This is why
| all OO is pointless", is truly a lazy effort.
|
| Good OOP is good. Bad OOP is bad. That's like every other piece
| of coding. Some excellent examples of great OO code that I've
| worked with have to do with having an abstract class to define a
| data api, and then being able to switch providers seamlessly
| because the internal interface is the same, and all you need to
| do is write a vendor-specific inherited class.
| yowlingcat wrote:
| How long has the author used Python? OO, while responsible for
| much of the footguns, is also responsible for an enormous amount
| of Python's expressive power. Being able iterate or key into
| arbitrary objects as if they were lists or dictionaries is
| enormously powerful, but all of that structure is OO based. It's
| a huge part of what "pythonic" means.
|
| > If you've taken the pure-FP/hexagonal-architecture pill
|
| Writing software is not something you take a conceptual "pill"
| for. You learn new tools that you add to your toolchest and use
| at your own discretion. Tools which purport to be able to
| displace a whole arena of otherwise stable, mature, and high
| productivity tools better be at least comparable if not
| significantly better to encourage upgrade. FP as a tool
| definitely comes in handy for quite a few use cases. FP as an
| ideology? Not so much.
|
| Regarding the latter, I'm getting a little sick of the FP
| chauvinism that that pretends that pre-existing software and how
| it was written is obviously inferior to the new, purely FP way
| without any real persuasive evidence. Code that's written in an
| overly FP oriented way is no less immune to the same codebase
| diseases that code written in an overly OO oriented way is. It's
| the map-territory problem all over again.
| jphoward wrote:
| I found when I started programming I never used OOP. Then I used
| it too much. And then recently I use it incredibly sparingly. I
| think this is most people's experience.
|
| However, there are certain situations where I cannot imagine
| working without OOP.
|
| For example, GUI development. Surely nobody would want to do
| without having a Textbox, Button, inherit from a general Widget,
| and have all the methods like .enable(), .click(), and properties
| like enabled, events like on_click, etc.?
|
| Similarly, a computer game, having an EnemyDemon inherit from
| Enemy, so that it has .kill(), .damage(), and properties for
| health, speed etc.?
|
| I'd really like to know how the most anti-OOPers think situations
| like this should be handled? (I'm not arguing, genuinely
| interested)
| thepratt wrote:
| If we step into the haskell land of monads having explicit
| functionality kept in individual monads with their own
| instances would be one way to segment this type of stuff.
| Something vaguely written as below would let you run actions
| where anyone can move or is an enemy, and default
| implementations can be provided as well. data
| Demon = { ... } data Action = Dead
| | KnockedBack | Polymorphed class
| Character a where health :: Int class
| (Character a) => Movement a where speed :: Int
| class (Character a) => Enemy a where kill :: a ->
| Action damage :: a -> Action instance
| Character Demon where health = 30
| instance Movement Demon where speed = 5
| instance Enemy Demon where kill _ = _
| damage _ = _
|
| https://soupi.github.io/rfc/pfgames/ is a talk going through an
| experience building a game in a pure fp way with Haskell and
| how they modelled certain aspects of game dev. Most of the code
| examples are when you press down in the slides.
| beaconstudios wrote:
| the patterns can still be completely the same without OOP -
| pairing data and functions that operate on said data.
| enemy.damage() and Enemy::damage(enemy) are functionally and
| semantically equivalent. But in the latter case (where you
| separate data and code) you don't need to worry about object
| assembly/IoC, how to pass a reference to A all the way through
| the object graph to object B, composition over inheritance
| becomes the default (at least in my experience using TypeScript
| interfaces, YMMV with other languages). The benefits of OOP,
| primarily state encapsulation, stop looking like benefits when
| it turns out your state boundaries weren't quite right.
|
| Of course I'm biased as I went through the same "procedural =>
| OOP => case-by-case" learning curve as the GP. But I ended up
| spending a lot of time trying to satisfy vague rules when using
| OOP - with procedural/functional programming with schema'd
| data, I get to spend a lot more time on what I actually want to
| do. Not worrying about SRP, SOLID, object assembly, how to fix
| my object graph now that A needs to know about B, and so on.
| jcelerier wrote:
| > how to pass a reference to A all the way through the object
| graph to object B
|
| you just end up replacing the object graph by the call graph,
| which makes all the business logic much messier as now every
| function call takes a "context" argument
| beaconstudios wrote:
| That's not been true in my experience - what you do end up
| with is a global data structure, which is the shared state
| for all or most non-ephemeral top level concerns. Aside
| from recursive functions I find call stacks tend to be
| quite short.
| [deleted]
| pje wrote:
| > I cannot imagine working without OOP ... in GUI development
|
| What is React but essentially a (wildly popular) functional GUI
| framework?
| joeberon wrote:
| But react isn't the widget library
| leontrolski wrote:
| In game development even there seems to be a shift away from
| OO, to data + functions under the guise of "Data
| Orientated/Driven Development" - eg:
| https://www.youtube.com/watch?v=0_Byw9UMn9g
|
| Edit: ignore me - this person seems to know more what they're
| talking about - https://news.ycombinator.com/item?id=25933781
| setr wrote:
| At least regarding games, ECS is the popular flavor-of-the-
| month alternative to OOP as a design strategy. The main problem
| being that games often have a _lot_ of special cases, which
| break the inheritance hierarchy quite quickly.
|
| E.g. Defining a weapon > {sword, wand} hierarchy, with
| respective properties for melee and casting, and then defining
| a unique weapon _spellsword_ which is capable of both melee
| _and_ casting. You could inherit from weapon, and copy & paste
| sword/wand code, or inherit from sword/wand, and copy & paste
| the other, but the hierarchy is broken.
|
| ECS would rather have you define [melee] and [casting]
| components, and then define a sword to have [melee], wand to
| have [casting] and spellsword to have [melee, casting]. So
| instead of representing the relationships as a tree of
| inheritance, you represent it as a graph of components
| (properties). And then you generically process any object with
| the melee tag, and any object with the casting tag, as needed.
|
| And of course then you could trivially go and reach out across
| the hierarchies and toss [melee] onto your house object and
| wield your house like a sword -- I don't know why you'd want to
| do that, but the architecture is flexible enough to do so
| (perhaps to your detriment).
|
| Dwarf Fortress probably has the best example of this:
| https://github.com/BenLubar/raws/blob/archive/objects/creatu...
|
| That's probably more an example of "metadata-driven" but it's
| ultimately the same thing -- an entity in the game is defined
| by its components, and the job of the game engine is to simply
| drive those components through the simulation. That particular
| example has its metadata (e.g. aesthetics:
| [CREATURE_TILE:249][COLOR:2:0:0]), its capabilities (e.g.
| [AMPHIBIOUS][UNDERSWIM]) and its data (e.g.
| [PETVALUE:10][BODY_SIZE:0:0:200]).
|
| And it even has inheritance :-)
| [CREATURE:TOAD_MAN] [COPY_TAGS_FROM:TOAD]
| [APPLY_CREATURE_VARIATION:ANIMAL_PERSON]
| reidjs wrote:
| Thanks for sharing that Dwarf Fortress file, I never thought
| about the structure for all those attributes.
| vadansky wrote:
| I've been hearing about ECS for a decade so it's definitely
| more then flavor of the month. However the issue is that
| unfortunately Unreal/Unity are both OO first.
| setr wrote:
| It's definitely been around but I think Unity's (never-
| finishing) ECS + Rust gamedev community's focus on it has
| really spiked its popularity/interest lately. Otherwise
| pretty much every recommendation/engine is OOP-based, with
| a few straggling extensions/libraries for ECS here and
| there.
|
| No idea about usage in industry though, but it comes up
| randomly e.g blizzard:
| https://www.youtube.com/watch?v=W3aieHjyNvw
| megameter wrote:
| The way to really grasp ECS architecture is not to look too
| hard at the implementations(which are all making specific
| trade-offs) but to recognize where it resembles and deviates
| from relational database design. A real-time game can't
| afford the overhead of storing data in a 3NF schema, but it
| can design a custom structure that preserves some data
| integrity and decoupling properties while getting an
| optimized result for the common forms of query.
|
| The behavioral aspects are subsumed in ECS to sum types,
| simple branching and locks on resources, where the OOP-
| embracing mode was to focus on language-level polymorphism
| and true "black-boxing". Since the assumed default mode of a
| game engine is global access to data and the separation of
| concerns is built around maintaining certain concurrency
| guarantees(the order in which entities are updated should
| have minimal impact on outcomes), ECS makes more sense at
| scale.
|
| The implementation trade-off comes in when you start
| examining how dynamic you want the resulting system to be:
| You could generate an optimal static memory layout for a
| scene(with object pools used to allow dynamic quantities to
| some limit) or you could have a dynamic type system, in
| essence. The latter is more straightforward to feed into the
| edit-test iteration loop, but the former comes with all the
| benefits of static assumptions. Most ECS represents a point
| in the middle where things are componentized to a hardcoded
| schema.
| jgwil2 wrote:
| In case others are wondering:
|
| https://en.wikipedia.org/wiki/Entity_component_system
| anaerobicover wrote:
| Note for readers who want to search for more: ECS is "Entity
| Component System"
|
| And Eric Lippert has a fantastic series of blog posts where
| he also discusses this problem:
| https://ericlippert.com/2015/04/27/wizards-and-warriors-
| part...
| setr wrote:
| exactly the post I was trying to remember when talking
| about spellswords :)
|
| Those posts are also cool in that defining games as a set
| of rules that operate on things within it is a really
| _neat_ mental model -- the program should basically look
| like a DnD rulebook, with statblocks and all.
| nickjj wrote:
| > Similarly, a computer game, having an EnemyDemon inherit from
| Enemy, so that it has .kill(), .damage(), and properties for
| health, speed etc.?
|
| I'm not a game developer in the slightest but as a gamer and
| developer I've often thought about similar things a little bit.
|
| In another example, let's say you were playing a game like
| Diablo II / Path of Exile where you have items that could drop
| with random properties. Both of those games support the idea of
| "legacy" items. The basic idea is the developers might have
| allowed some armor to drop with a range of +150-300% defense in
| version 1.0 of the game but then in 1.1 decided to nerf the
| item by reducing its range to +150-200% defense.
|
| Instead of going back and modifying all 1.0 versions of the
| item to fit the new restrictions, the game keeps the old 1.0
| item around as its own entity. It has the same visible name to
| the player but the legacy version has the higher stats. Newer
| versions of the item that drop will adhere to the new 1.1 stat
| range.
|
| That made me think that they are probably not using a highly
| normalized + OOP approach to generate items. I have a hunch
| every item is very denormalized and maybe even exists as its
| own individual entity with a set of stats associated to it
| based on whenever it happened to be generated. Sort of like an
| invoice item in a traditional web app. You wouldn't store a
| foreign key reference to the price of the item in the invoice
| because that might change. Instead you would store the price at
| the time of the transaction.
|
| I guess this isn't quite OOP vs not OOP but it sort of maybe is
| to some degree.
|
| I'd be curious if any game devs in the ARPG genre post here.
| How do you deal with such high amounts of stat variance, legacy
| attribute persistence, etc.?
| ed25519FUUU wrote:
| Largely I agree with the author's sentiment, but I'm not sure I
| totally agree with some of the alternatives. For example I think
| using an @lru_cache decorator over a `get_...()` function to
| return variables is much better at all than using a global var.
| In fact, it looks hackier if you ask me.
|
| https://leontrolski.github.io/sane-config.html
| tabulatouch wrote:
| OOP is meant for re-use across different projects. Frameworks, or
| self-contained classes.
| jgwil2 wrote:
| This may be a bit pedantic, but OOP != inheritance. Inheritance
| is the worst part of OOP. OOP is also modules and namespaces and
| encapsulation, all of which are great.
| spaetzleesser wrote:
| Agreed. Inheritance CAN be very useful but I would consider it
| an advanced feature for special cases. It's definitely not
| something that should be taught to beginners.
| socialdemocrat wrote:
| Things which all exist in non-OOP languages and before OOP
| became a fad. No OO isn't inheritance, but implementation
| inheritance is one of the few things which is unique to OOP
| based programming. Most other aspects of OOP will be found
| within other paradigms.
|
| More importantly OOP is not merely a bag of syntax and features
| but a way of thinking about software development and
| structuring your programs. OOP says you organize your code
| around objects and the actions done on those objects.
|
| That is something I find that often isn't a great way of
| structuring your code. But I don't think OOP is useless. I do
| use OO thinking in my code, just not as much as I used to. I
| prefer functional thinking. Often I organize code arounds verbs
| rather than nouns. So one file may be a similar kind of action
| performed on many different kinds of objects.
|
| E.g. when writing a rocker simulator, I would have one source
| code file which contained mass calculations for a variety of
| objects.
|
| Another file would contain rocket thrust calculations.
|
| I would say though that in GUI programming I find that OO
| thinking tends to make a lot of sense.
| bccdee wrote:
| I don't think modules or namespaces really count as OOP. And
| encapsulation can exist in the absence of OOP if the privacy
| barrier exists at (eg) the module level instead of the class
| level.
|
| I'd argue that inheritance is really the core of OOP (and by
| extension methods, which are only different from functions
| insofar as they interact with inheritance).
| petr25102018 wrote:
| How are modules and namespaces OOP?
| jgwil2 wrote:
| To me OOP is about separation of concerns, abstract data
| types, information hiding. Modules/namespaces support
| decomposition the same way that objects do, and historically
| they became popular because of and in tandem with object-
| oriented languages. So it's true as others have pointed out
| that they can and do exist in other paradigms; I would argue
| that is evidence of the influence of OOP and the fact that
| its best ideas have been incorporated all over the place.
| bccdee wrote:
| Those are just good programming principles in general
| though. Abstract data types, information-hiding, and
| separation-of-concerns were being done in C well before OOP
| was a thing. Granted, the information-hiding wasn't as
| sophisticated as it got once OOP was introduced, but I
| don't think it makes sense to attribute information-hiding
| to OOP as a result, especially since newer programming
| language have used visibility attributes in the absence of
| OOP classes.
| cogman10 wrote:
| By and large, I think inheritance is a major mistake.
| Interfaces are the only "good" part of inheritance.
|
| Sure, there are edge cases where inheritance is truly the best
| way to do things, but those are few and far between.
|
| I cringe every time I dive into some of my day job's more
| deeply inheritance based code. It is SO hard to make changes
| there without breaking a bunch of stuff unintentionally.
| gorgoiler wrote:
| In general, object orientation is a reasonably elegant way of
| binding together a compound data type and functions that operate
| on that data type. Let us accept this at least, and be happy to
| use it if we want to! It is _useful_.
|
| What are emphatically _not_ pretty or useful are Python's leading
| underscores to loosely enforce encapsulation. Ugh. I'd sooner use
| camelCase.
|
| Nor do I find charming the belligerent lack of any magical
| syntactic sugar for `self`. Does Python force you to pass it as
| an argument to make some kind of clever point? Are there
| psychotic devs out there who call it something other than `self`?
| Yuck!
|
| And why are some classes (int, str, float) allowed to be lower
| case but when I try to join that club I draw the ire from the
| linters? The arrogance!
|
| ...but I still adore Python. _People call these things
| imperfections but it's just who we are._
|
| PS I liked the Python5 joke a lot.
| Ankintol wrote:
| > In general, object orientation is a reasonably elegant way of
| binding together a compound data type and functions that
| operate on that data type. Let us accept this at least, and be
| happy to use it if we want to!
|
| Is it elegant? OO couples data types to the functions that
| operate on them. After years of working on production OO I've
| still never come across a scenario where I wouldn't have been
| equally or better served by a module system that lets me co-
| locate the type with the most common operations on that type
| with all the auto-complete I want: //type
| MyModule { type t = ... func foo = ...
| func bar = ... }
|
| If I want to make use of the type without futzing around with
| the module, I just grab it and write my own function
| ssivark wrote:
| Totally. That structure also allows for multiple dispatch,
| which makes generic programming much much more pleasant than
| OO (which is basically single dispatch). Eg. See Julia.
|
| To elaborate, tying methods with the individual/first
| argument makes it very difficult to model interactions of
| multiple objects.
| fleabitdev wrote:
| How do you compensate for the lack of type-dependent name
| resolution? MyModule.foo(my_t) seems verbose, compared to
| my_t.foo()
| bcrosby95 wrote:
| FWIW this is how Elixir works. You just do MyModule.foo.
| girvo wrote:
| Nim handles this quite nicely for you: either syntax works!
| JNRowe wrote:
| For those unfamiliar, the UFCS1 wikipedia page has an
| explanation and a few examples.
|
| 1 https://en.m.wikipedia.org/wiki/Uniform_Function_Call_S
| yntax
| Smaug123 wrote:
| If anything, this is an _aid_ to type-checking. Since
| MyModule.foo takes only things of type t, the type-checker
| 's job is extremely easy and it will help you a lot more in
| cases when your program is incomplete.
|
| So often when using C#-style fluent APIs I find that I'm
| completely on my own and have to turn a half-written line
| into something syntactically correct before Intellisense
| gives me anything useful. Using an F#-style MyModule.foo,
| the compiler can tell me everything.
| dan-robertson wrote:
| Most of the time you have:
|
| - short names inside modules. I.e. you might have a
| function called Foo.merge(...) instead of
| x.merge_with_foo(...)
|
| - a way to bring modules into scope so you don't need to
| specify the name
|
| - not using that many modules. Most lines of code won't
| have more than one or two function calls so it shouldn't
| matter that much (other techniques can be used in
| complicated situations)
|
| The key advantage of type-dependant name resolution is in
| using the same names for different types. You might want to
| write code like foo.map(...) and it is ok if you don't know
| the exact type of foo. With modules you may need to know
| whether to call SimpleFoo.map or CompoundFoo.map.
| Kototama wrote:
| Except the binding always implies the object is mutated on
| changes, which make any form of reasoning or concurrent
| programming difficult.
| riffraff wrote:
| Why? There are plenty of OO libraries where method calls do
| not change the object.
| klyrs wrote:
| > Are there psychotic devs out there who call it something
| other than `self`? Yuck!
|
| I have, under duress. It was a result of using syntactic sugar
| wrapping a PHP website to make a Python API; it was convenient
| to pass form variables into a generic handler method of a
| class. The problem? The website, fully outside of my control,
| had a page which had an input named "self" which resulted in
| two values for that argument. Rather than refactor the class
| and dozens of scripts that depended on it, I renamed 'self' to
| 's_lf' in that one function and called it a day.
|
| Also, python has class methods. The convention is to use 'cls'
| in that context, to avoid confusing the parameter (a class)
| with an instance of that class.
| [deleted]
| dfinninger wrote:
| Just to discuss one point:
|
| > And why are some classes (int, str, float) allowed to be
| lower case
|
| Also, boolean. These are primitive data types. For instance, in
| Java there's a difference between int and Integer. I'd assume
| that Python special-cases these because they are primitive. But
| I haven't been through the Python internals, so it's only a
| guess.
| rbanffy wrote:
| They are far less primitive than Java's - they are classes
| too, but can't be extended.
| mohaine wrote:
| > Are there psychotic devs out there who call it something
| other than `self`? Yuck!
|
| I was converting some old Java code to Python recently and I
| almost decided to switch to `this` just to make the task less
| repetitive. Luckily, sanity returned after I saw the first def
| abc(this,
| mrslave wrote:
| PEP 20 -- The Zen of Python [0] ... "Explicit is better than
| implicit."
|
| In the case of the unnecessarily repetitious `self` it means to
| violate DRY, make the mundane manual - and therefore error
| prone - and tedious.
|
| [0] https://www.python.org/dev/peps/pep-0020/
| rbanffy wrote:
| > What are emphatically not pretty or useful are Python's
| leading underscores to loosely enforce encapsulation.
|
| Python will not prevent anyone from doing something dumb. It'll
| just force them to acknowledge that by forcing them to use a
| convention. As a library writer, I'm free to change or remove
| anything that starts with an underscore because, if someone
| else is depending on it, frankly, they had it coming. I can
| assume everyone who uses my library is a responsible adult and
| I can treat them as that.
|
| > And why are some classes (int, str, float) allowed to be
| lower case
|
| Because they are part of the language like `else` or `def`.
| Only Guido can do that. ;-)
| takeda wrote:
| > As a library writer, I'm free to change or remove anything
| that starts with an underscore because, if someone else is
| depending on it, frankly, they had it coming. I can assume
| everyone who uses my library is a responsible adult and I can
| treat them as that.
|
| Totally agree, as an user, I feel anxious whenever I have to
| use underscored names. It's great that Python still allows me
| to do it and there were few times when it was useful, but
| when it stops working I know it's 100% on me.
| tus88 wrote:
| > Nor do I find charming the belligerent lack of any magical
| syntactic sugar for `self`. Does Python force you to pass it as
| an argument to make some kind of clever point? Are there
| psychotic devs out there who call it something other than
| `self`? Yuck!
|
| If you weren't an ignoramous you would understand it _can_ be
| something else, like klass.
| gorgoiler wrote:
| You spelled it wrong!
|
| Ruby's approach is the most delightful, I find. elf is an
| instance of _Elf_ and in elf.health, self is the elf.
|
| _Elf_ is an instance of Class and in Elf.wiseass, self is
| the _Elf_ class itself.
| jghn wrote:
| I'd personally contest the notion that binding state and
| functionality directly together is "reasonably elegant" in the
| first place.
|
| But ignoring that, it depends on the flavor of object
| orientation. Yes, the most mainstream style bundles state
| directly with functionality but not all do. But for instance
| the CLOS family of OOP maintains separate state and
| functionality and one binds desired functionality to those
| classes which should have it. This is not _too_ dissimilar from
| typeclasses IMO.
| reedwolf wrote:
| Thems fightin words.
| m4r35n357 wrote:
| ahem, operator overloading
| ben509 wrote:
| I think OO caught on because it encourages people to organize
| their thoughts, and good structure alone brings a tremendous
| improvement in code.
|
| I think the reaction to it is more the "everything is mutable"
| approach most OO languages took, which led to things that looked
| organized but that were a hot mess of side-effects.
|
| With dataclasses (or attrs) you can cut back on that by freezing
| everything, and still get the clarity of methods that lay out the
| essential functionality of a type.
|
| In their own example, "get_items" and "save_items" make a bit
| more sense if they're stashed away in the Client namespace, and
| you can see that they're essential to what a Client does.
|
| And while Oil's conjecture is probably right, I'm not sure it
| survives if you add the caveat, "in that same language."
|
| For instance, Python doesn't have a native way of expressing sum
| types outside of inheritance. That's not to say inheritance,
| especially an always open style, is a good way of working with a
| sum type, it's just the only native way.
| dsego wrote:
| I think OOP caught on because of IDEs with auto-complete
| functionality, you type object dot and it offers all the method
| names for you.
| cgrealy wrote:
| OOP was around long before IDEs were that popular.
| benkuhn wrote:
| The author's OO example is hard to understand, but they're wrong
| about why. It's not bad because it's OO, but that it's very badly
| done OO: the class couples two different concerns (network API
| client and database). That's why it makes more sense as a bag of
| functions.
|
| The general version of the point doesn't work very well, and many
| of the other OO use-cases the author discusses actually work much
| better than alternatives.
|
| For example, on abstract base classes: if you replace this with a
| bag of functions I think you end up reinventing virtual dispatch
| --that is, each function's top level is a bunch of `if
| isinstance(...)` branches. This is much harder to read, and
| harder to add new implementations to, than abstract methods. It's
| also no easier to understand.
|
| (There is a subset of this advice that I think does improve your
| code's understandability, which is "only ever override abstract
| methods," but that is very different from "don't use OO.")
|
| For impure classes, the author suggests e.g. using `responses`
| (an HTTP-level mocking library) instead of encapsulating these
| behind an interface. This is a fine pattern for simple stuff, but
| it is _not_ more understandable than a fake interface. The hand-
| written fake HTTP responses you end up having to write are a lot
| less readable than a mock implementation of a purpose-built
| Python interface. (Source: I once mocked a lot of XML-RPC APIs
| with `responses` before I knew better; it was not
| understandable.)
| andrewstuart wrote:
| I found Python OO to be a super elegant and practical way to
| implement a common API for multiple databases.
| nerpderp82 wrote:
| One doesn't need OO for that tho, the module system applies
| just as well to solving that problem.
| 0xTJ wrote:
| This is - as the kids put it - a very hot take.
| mywittyname wrote:
| I write a lot of Python code. Objects are absent in most of what
| I write. But the 10% or so of the time I use objects, it is
| because objects are the best way I can think of to structure the
| program.
|
| So I don't think objects in Python are useless at all. I think
| Python is designed in a way that allows them to be used
| effectively if desired, or avoided all together when there's no
| apparent benefit.
| pmontra wrote:
| I maintain a couple of Django projects for a customer of mine.
| I wrote almost 50% of the code base. Those two projects use
| very few classes, probably only the models. Everything else is
| a bag of functions organized in modules.
|
| It's very similar to an Elixir /Phoenix project I'm working on
| for another customer. Modules and functions and a couple of
| dozens of GenServers that act as objects to maintain status
| where we need it.
|
| And yet, I easily follow the OO model of Ruby of Rails. I feel
| it right in that context, probably because the rails of RoR are
| more well defined than the ones of Django. Of course when I
| script in Ruby I bet rarely write a class. A few functions,
| actually methods of Object, are all I need there.
| tasty_freeze wrote:
| The example given in the article is not compelling -- there are
| only two arguments in play, so passing two arguments directly
| instead of passing an object holding those two things isn't
| painful.
|
| To me the big win of classes isn't inheritance, it is that
| functions and related data live in a common scope.
|
| If you write your code as a bag of data structures and a bag of
| functions that you pass those ad-hoc data structures to, it is
| less clear which things play together.
| commandlinefan wrote:
| Not only that, but the second example _is_ object-oriented, he
| 's just doing it "by hand" rather than taking advantage of the
| built-in language syntax (which, in spite of his insistence to
| the contrary, does make it clearer what's actually going on).
| socialdemocrat wrote:
| I would say that is simply down you you not being experienced
| with writing code this way.
|
| Once you begin writing functional code more regularly, you will
| get much the opposite way of thinking. Today I find it
| frustrating that I need an object first. In my mind, I already
| know what action I want to do. I know what function I want to
| call. I start with that and look at what arguments it takes in.
|
| OOP programming is frustrating now, because instead of going
| straight for the function I have to locate the appropriate
| object first. That is like a big detour.
|
| > If you write your code as a bag of data structures and a bag
| of functions that you pass those ad-hoc data structures to, it
| is less clear which things play together.
|
| Noting suggest you need to write code like that. People who
| write functional code tend to have a lot of well defined data
| types. I use well defined data types in Julia. Haskell and
| OCaml developers use well defined data types. We are not using
| using dictionaries or something.
| kwdc wrote:
| I've done plenty of maintenance programming. Most OO involves
| abuse of the paradigm anyway.
|
| Most people don't use inheritance to large AND good effect. Or
| they use it and create a mess. Deep hierarchies of inheritance
| need to be carefully designed or they become a mess. All the
| hiding means less clarity about what is happening when things go
| wrong.
|
| That said, its a valid approach and can yield useful results.
| Especially when you can just add a thing to a list and simply
| call a method on it without having to bother to know what it is.
| jope12 wrote:
| >Deep hierarchies of inheritance need to be carefully designed
| or they become a mess.
|
| Deep hierarchies of _anything_ need to be carefully designed.
| brixon wrote:
| I try to use the rule of after three levels of inheritance or
| abstraction then it needs to be a black box and then you can
| reset your count.
| canada2us wrote:
| One will appreciate OO once he/she saw a 2000-line code in a
| single file.
| bccdee wrote:
| You don't need to use classes to break a big file into several
| smaller files.
| acdha wrote:
| This post says more about the author's experience than Python,
| especially with the list of exceptions. The common pattern behind
| those exceptions is that they're not as simple as the examples,
| which gets to a better lesson that OO is not pointless but that
| you should use it cautiously when you know that your problem
| domain has a sufficiently complex combination of state and code.
| Anything is going to seem simpler when you have 2 variables and
| the whole thing is 20 lines and while it's definitely useful to
| pause before accreting complexity it's also important to remember
| that projects and teams come in many different forms and there's
| no guarantee that something which works for you on a project now
| will be the best option for someone else with different needs.
| leontrolski wrote:
| Hi, I think you're correct in some domains - I probably should
| have spelt out that I'm by-and-large talking about application
| development, where if you have a "sufficiently complex
| combination of state and code" you probably have a problem
| rather than a candidate for wrapping in a class.
| yowlingcat wrote:
| > where if you have a "sufficiently complex combination of
| state and code" you probably have a problem rather than a
| candidate for wrapping in a class
|
| Ironically, I think most people in that situation would agree
| with you that they "probably have a problem" -- the
| difference is that they see the problem as the important
| thing to focus investing resources into solving whereas you
| see its existence as an inherently poisoned entity that must
| be conceptually purified. While both parties would probably
| agree on the existence of /a/ problem, the definition of what
| needs to be addressed and how is likely to differ, and I
| can't say that the latter attitude is the norm at any high
| productivity engineering organization I've ever worked at,
| built, or encountered.
| megameter wrote:
| A decent rule of thumb I sometimes apply is:
|
| 1. What I have could be formalized into a state machine. 2.
| That state machine needs to be reused and re-entered. 3. I want
| to apply inputs to the state machine with method calls.
|
| Of course you can overapply the thought and end up with an
| enterprise architecture - that's why YAGNI is important. But
| when a section builds up a bit of conceptual redundancy there's
| usually a state machine that can be pulled out of it in OO
| form.
|
| And I would certainly do it that way in Python, as well as
| other languages.
| KaiserPro wrote:
| Having recently started a script that avoided OO because I
| wrongly assumed that its was going to be 4 functions and done, I
| disagree.
|
| I don't care about inheritance, that's just a way for someone to
| trip you up and make them feel smug about being "elegant"
|
| I don't care for people trying to hide stuff with
| self._hiddenThing. Its python, nothing is hidden. If I want to
| reach in and grab your class by the ankles, prepending a "_"
| isn't going to stop me.
|
| I agree wholeheartedly with dataclasses. I just wish that type
| annotations were actually enforced at run time. At the moment
| they are only really useful if you have pyre turned up to 11 (bye
| bye productivity) or have an IDE that understands type hints.
|
| but, the hill I will die on is this: storing class state in
| self.blah. It is correct and proper to do that in a class, it
| limits the amount of args I have to shove into a function/method,
| it also allows me to pull out and check things before I do them.
| Yes they can be abused. Yes it means that you need to call
| functions in order. Yes it means you have to be more defensive
| about missing state.
|
| But you need to be careful about that anyway. with minimal OO you
| can avoid a lot of typing and nasty verbose function args in
| python.
___________________________________________________________________
(page generated 2021-01-27 23:00 UTC)