constructor classmethods

Ethan Furman ethan at stoneleaf.us
Mon Nov 7 13:13:39 EST 2016


On 11/07/2016 12:46 AM, teppo.pera at gmail.com wrote:
> torstai 3. marraskuuta 2016 14.45.49 UTC Ethan Furman kirjoitti:
>> On 11/03/2016 01:50 AM, teppo wrote:
>>
>>> The guide is written in c++ in mind, yet the concepts stands for any
>>>   programming language really. Read it through and think about it. If
>>>   you come back to this topic and say: "yeah, but it's c++", then you
>>>   haven't understood it.
>>
>> The ideas (loose coupling, easy testing) are certainly applicable in
>>  Python -- the specific methods talked about in that paper, however,
>>  are not.
>
> Please elaborate. Which ones explicitly?

The ones in place solely to make testing easier.

Others, such as passing an Engine into Car instead of making Car create its own, are valid.  To compare to what I said elsewhere, the exact Engine used is *not* an implementation detail -- any particular Car could have a range of Engines that work with it, and which is used is not determined by the car itself.

>> To go back to the original example:
>>
>> def __init__(self, ...):
>>       self.queue = Queue()
>>
>> we have several different (easy!) ways to do dependency injection:
>>
>> * inject a mock Queue into the module
>> * make queue a default parameter
>>
>> If it's just testing, go with the first option:
>>
>> import the_module_to_test
>> the_module_to_test.Queue = MockQueue
>>
>> and away you go.
>
> This is doable, but how would you inject queue (if we use Queue as an
>  example) with different variations, such as full, empty, half-full,
>  half-empty. :) For different tests.

In this case I would go with my last example:

ex = Example()
test_queue = generate_half_empty_queue()
ex.queue = test_queue
# do the testing

>> If the class in question has legitimate, non-testing, reasons to specify different Queues, then make it a default argument instead:
>>
>> def __init__(self, ..., queue=None):
>>       if queue is None:
>>           queue = Queue()
>>       self.queue = queue
>
> I already stated that this is is fine, as long as the number of arguments
>  stays in manageable levels.

How is having 15 arguments in a .create() method better than having 15 arguments in __init__() ?

>  Although I do think testing is good enough
>  reason to have it injected anytime. For consistency, it makes sense to
>  have same way to create all objects. I wouldn't suggested of using that
>  mechanism in public API's, just in internal components.

And that consistent way is to just call the class -- not to call class.create().

>> or, if it's just for testing but you don't want to hassle injecting a
>>  MockQueue into the module itself:
>>
>> def __init__(self, ..., _queue=None):
>>       if _queue is None:
>>           _queue = Queue()
>>       self.queue = _queue
>>
>> or, if the queue is only initialized (and not used) during __init__
>>  (so you can replace it after construction with no worries):
>>
>> class Example:
>>       def __init__(self, ...):
>>           self.queue = Queue()
>>
>> ex = Example()
>> ex.queue = MockQueue()
>> # proceed with test
>
> This I wouldn't recommend. It generates useless work when things start to change,
>  especially in large code bases. Don't touch internal stuff of class in tests (or
>  anywhere else).

So, if you use the create() method, and it sets up internal data structures, how do you test them?  In other words, if create() makes that queue then how do you test with a half-empty queue?

>> The thing each of those possibilities have in common is that the normal use-case
>>  of just creating the thing and moving on is the very simple:
>>
>> my_obj = Example(...)
>>
>> To sum up:  your concerns are valid, but using c++ (and many other language)
>>  idioms in Python does not make good Python code.
>
> This is not necessarily simply a c++ idiom, but a factory method design pattern [...]

Not all design patterns make sense in every language.

--
~Ethan~



More information about the Python-list mailing list