Re: [Tutor] class functions/staticmethod?

2019-08-13 Thread Cameron Simpson

On 14Aug2019 11:15, Steven D'Aprano  wrote:

On Wed, Aug 14, 2019 at 09:58:35AM +1000, Cameron Simpson wrote:

On 11Aug2019 22:58, James Hartley  wrote:
>I am lacking in understanding of the @staticmethod property.
>Explanation(s)/links might be helpful.  I have not found the descriptions
>found in the Internet wild to be particularly instructive.

You have received some answers; to me they seem detailed enough to be
confusing.


Its only confusing if you don't work your way through it carefully and
systematically. There's a lot to understand, but if you don't understand
it, Python's behaviour in this case seems counter-intuitive and hard to
follow.


Yeah, but it helps to understand the objective: function context.

A deep dive into the mechanisms used to achieve that is a load to 
ingest. High levels of detail tend to swamp one's view of the larger 
picture, particularly when learning.


[...]

I think of things this way: what context does a method require?  Not
everything needs the calling instance.

Here endeth the lesson.


Given that you go on to write almost another 150 lines of explanation, I
think a better description would be "Here *begins* the lesson" *wink*


Well, maybe, but I really wanted to highlight the objective: 
@classmethod and @staticmethod dictate the context provided to the 
method.


All the examples that follow aim, however vaguely, to show those 
contexts in action.



Your lesson, I think, assumes that it is obvious that staticmethods
don't have access to the calling instance, or its class.


No, it aims to make that point clear. EVerything else is example or 
mechanism.



But if you look
at James' code, I think you will agree that he's assuming that
staticmethods *do* have access to the calling class, and is perplexed by
the fact that the look-up of class variables (class attributes) fails.


Because nobody had said that @staticmethod and @classmethod _define_ the 
provided context.



Your lesson gives us no clue why James' first method, "dimensions()",
which he describes as a "class method", isn't a class method and doesn't
actually work correctly, even though it appears to at first glance.


I didn't try to tackle his code. I think it is better to get the 
intended use of @classmethod and @staticmethod clear. Digging into 
whatever weird consequences there might be to his slightly wrong code 
just brings confusion.


Cheers,
Cameron Simpson 
___
Tutor maillist  -  Tutor@python.org
To unsubscribe or change subscription options:
https://mail.python.org/mailman/listinfo/tutor


Re: [Tutor] class functions/staticmethod?

2019-08-13 Thread Steven D'Aprano
On Wed, Aug 14, 2019 at 09:58:35AM +1000, Cameron Simpson wrote:
> On 11Aug2019 22:58, James Hartley  wrote:
> >I am lacking in understanding of the @staticmethod property.
> >Explanation(s)/links might be helpful.  I have not found the descriptions
> >found in the Internet wild to be particularly instructive.
> 
> You have received some answers; to me they seem detailed enough to be 
> confusing.

Its only confusing if you don't work your way through it carefully and 
systematically. There's a lot to understand, but if you don't understand 
it, Python's behaviour in this case seems counter-intuitive and hard to 
follow.

Python makes the behaviour of regular instance methods so simple and 
intuitive, it can be quite a blow when you try to do something that 
isn't.


> I think of things this way: what context does a method require?  Not 
> everything needs the calling instance.
> 
> Here endeth the lesson.

Given that you go on to write almost another 150 lines of explanation, I 
think a better description would be "Here *begins* the lesson" *wink*


Your lesson, I think, assumes that it is obvious that staticmethods 
don't have access to the calling instance, or its class. But if you look 
at James' code, I think you will agree that he's assuming that 
staticmethods *do* have access to the calling class, and is perplexed by 
the fact that the look-up of class variables (class attributes) fails.

If James comes from a Java background, he's probably assuming that 
static methods do have access to the class variables, using undotted 
names:

class K(object):
attr = 1
@staticmethod
def foo():
return attr

In Java, K.foo() would return 1.

Your lesson gives us no clue why James' first method, "dimensions()", 
which he describes as a "class method", isn't a class method and doesn't 
actually work correctly, even though it appears to at first glance.


-- 
Steven
___
Tutor maillist  -  Tutor@python.org
To unsubscribe or change subscription options:
https://mail.python.org/mailman/listinfo/tutor


Re: [Tutor] class functions/staticmethod?

2019-08-13 Thread Cameron Simpson

On 11Aug2019 22:58, James Hartley  wrote:

I am lacking in understanding of the @staticmethod property.
Explanation(s)/links might be helpful.  I have not found the descriptions
found in the Internet wild to be particularly instructive.


You have received some answers; to me they seem detailed enough to be 
confusing.


I think of things this way: what context does a method require?  Not 
everything needs the calling instance.


Here endeth the lesson.



All this stuff below is examples based on that criterion:

Here's a trite example class:

 class Rectangle:
   def __init__(self, width, height):
 self.width=width
 self.height = height

Most methods do things with self, and are thus "instance methods", the 
default. They automatically receive the instance used to call them as 
the first "self" argument.


 def area(self):
   return self.width * self.height

They need "self" as their context to do their work.

Some methods might not need an instance as context: perhaps they return 
information that is just based on the class, or they are factory methods 
intended to return a new instance of the class. Then you might use a 
@classmethod decorator to have the calling instance's class as the 
context.


 @classmethod
 def from_str(cls, s):
   width, height = parse_an_XxY_string(s)
   return cls(width, height)

And some methods do not need the class or the instance to do something 
useful:


 @staticmethod
 def compute_area(width, height):
   return width * height

and so we don't give them the instance or the class as context.

Now, _why_?

Instance methods are obvious enough - they exist to return values 
without the caller needing to know about the object internals.


Class methods are less obvious.

Consider that Python is a duck typed language: we try to arrange that 
provided an object has the right methods we can use various different 
types of objects with the same functions. For example:


 def total_area(flat_things):
   return sum(flat_thing.area() for flat_thing in flat_things)

That will work for Rectangles and also other things with .area() 
methods. Area, though, is an instance method.


Class methods tend to come into their own with subclassing: I 
particularly use them for factory methods.


Supposing we have Rectangles and Ellipses, both subclasses of a 
FlatThing:


 class FlatThing:
   def __init__(self, width, height):
 self.width=width
 self.height = height
 @classmethod
 def from_str(cls, s):
   width, height = parse_an_XxY_string(s)
   return cls(width, height)

 class Rectangle(FlatThing):
   def area(self):
 return self.width * self.height

 class Ellipse(FlatThing):
   def area(self):
 return self.width * self.height * math.PI / 4

See that from_str? It is common to all the classes because they can all 
be characterised by their width and height. But I require the class for 
context in order to make a new object of the _correct_ class. Examples:


 rect = Rectangle.from_str("5x9")
 ellipse = Ellipse.from_str("5x9")
 ellispe2 = ellipse.from_str("6x7")

Here we make a Rectangle, and "cls" is Rectangle when you call it this 
way. Then we make an Ellipse, and "cls" is Ellipse when called this way.  
And then we make another Ellipse from the first ellipse, so "cls" is 
again "Ellipse" (because "ellipse" is an Ellipse).


You can see that regardless of how we call the factory function, the 
only context passed is the relevant class.


And in the last example (an Ellipse from an existing Ellipse), the class 
comes from the instance used to make the call. So we can write some 
function which DOES NOT KNOW whether it gets Ellipses or Rectangles:


 def bigger_things(flat_things):
   return [ flat_thing.from_str(
  "%sx%s" % (flat_thing.width*2, flat_thing.height*2))
for flat_thing in flat_things
  ]

Here we could pass in a mix if Rectangles or Ellipses (or anything else 
with a suitable from_str method) and get out a new list with a matching 
mix of bigger things.


Finally, the static method.

As Peter remarked, because a static method does not have the instance or 
class for context, it _could_ be written as an ordinary top level 
function.


Usually we use a static method in order to group top level functions 
_related_ to a specific class together. It also helps with imports 
elsewhere.


So consider the earlier:

 @staticmethod
 def compute_area(width, height):
   return width * height

in the Rectangle class. We _could_ just write this as a top level 
function outside the class:


 def compute_rectangular_area(width, height):
   return width * height

Now think about using that elsewhere:

 from flat_things_module import Rectangle, Ellipse, compute_rectangular_area

 area1 = compute_rectangular_area(5, 9)
 area2 = Rectangle.compute_area(5, 9)
 area3 = Ellipse.compute_area(5, 9)

I would rather use the forms of "area2" and "area3" because it is clear 
that I'm getting an area function from a nicely named class. 

Re: [Tutor] class functions/staticmethod?

2019-08-12 Thread Steven D'Aprano
Part 3.


On Sun, Aug 11, 2019 at 10:58:37PM -0500, James Hartley wrote:


> from collections import namedtuple
> 
> class Foo():
> Dimensions = namedtuple('Dimensions', ['height', 'width'])
> _dimensions = Dimensions(3, 4)
> 
> def dimensions():
> print('id = {}'.format(id(Foo._dimensions)))
> return Foo._dimensions
> 
> @staticmethod
> def dimensions1():
> print('id = {}'.format(id(_dimensions)))
> return _dimensions


In part 2, I explained that we can re-write the dimensions() method to 
work correctly using the @classmethod decorator:

@classmethod
def dimensions(cls):
print('id = {}'.format(id(cls._dimensions)))
return cls._dimensions


Another benefit of doing this is that it will now work correctly in 
subclasses.

class Bar(Foo):  # inherit from Foo
_dimensions = (3, 4, 5, 6)  # Override the parent's "dimensions".


Using your definition, Bar.dimensions() will return Foo._dimensions 
instead of Bar._dimensions. But using the classmethod version works as 
expected.

So why doesn't the staticmethod version work correctly? Its all to do 
with the way variable names are resolved by the interpreter.

If you are used to Java, for example, you might expect that "class 
variables" (what Python calls "class attributes") are part of the scope 
for methods:


spam = 999  # Global variable spam.

class MyClass(object):
 spam = 1  # Class attribute ("variable") spam.

 def method(self):
 return spam

instance = MyClass()


If you are used to Java's rules, you would expect that instance.method() 
will return 1, but in Python it returns the global spam, 999.

To simplify a little, the scoping rules for Python are described by the 
LEGB rule:

- Local variables have highest priority;
- followed by variables in the Enclosing function scope (if any);
- followed by Global variables;
- and lastly Builtins (like `len()`, `zip()`, etc).

Notice that the surrounding class isn't included.[1] To access either 
instance attributes or class attributes, you have to explicitly say so:

 def method(self):
 return self.spam

This is deliberate, and a FAQ:

https://docs.python.org/3/faq/design.html#why-must-self-be-used-explicitly-in-method-definitions-and-calls


Using Java's scoping rules, the staticmethod would have worked:

@staticmethod
def dimensions1():
print('id = {}'.format(id(_dimensions)))
return _dimensions

because it would see the _dimensions variable in the class scope. But 
Python doesn't work that way. You would have to grab hold of the class 
from the global scope, then grab dimensions:

@staticmethod
def dimensions1():
_dimensions = Foo._dimensions
print('id = {}'.format(id(_dimensions)))
return _dimensions


If you are coming from a Java background, you may have been fooled by an 
unfortunate clash in terminology. A "static method" in Java is closer to 
a *classmethod* in Python, not a staticmethod.

The main difference being that in Java, class variables (attributes) 
are automatically in scope; in Python you have to access them through 
the "cls" parameter.



-- 
Steven
___
Tutor maillist  -  Tutor@python.org
To unsubscribe or change subscription options:
https://mail.python.org/mailman/listinfo/tutor


Re: [Tutor] class functions/staticmethod?

2019-08-12 Thread Peter Otten
James Hartley wrote:

> I am lacking in understanding of the @staticmethod property.
> Explanation(s)/links might be helpful.  I have not found the descriptions
> found in the Internet wild to be particularly instructive.  Given the code
> below:
> =8<--
> from collections import namedtuple
> 
> class Foo():
> Dimensions = namedtuple('Dimensions', ['height', 'width'])
> _dimensions = Dimensions(3, 4)
> 
> def dimensions():
> print('id = {}'.format(id(Foo._dimensions)))
> return Foo._dimensions

That works with the class as Foo.dimensions is just a function in Python 3, 
but not with an instance because Python will try to pass the instance as the 
first argument

>>> Foo.dimensions()
id = 140192821560880
Dimensions(height=3, width=4)
>>> Foo().dimensions()
Traceback (most recent call last):
  File "", line 1, in 
TypeError: dimensions() takes 0 positional arguments but 1 was given

You can turn it into a static method

@staticmethod
def dimensions():
print('id = {}'.format(id(Foo._dimensions)))
return Foo._dimensions

>>> Foo.dimensions()
id = 139629779179056
Dimensions(height=3, width=4)
>>> Foo().dimensions()
id = 139629779179056
Dimensions(height=3, width=4)

or, when you are planning for subclases, into a classmethod:

$ cat staticmethod_demo.py
class Foo():
_dimensions = "foo-dimensions"

@classmethod
def class_dimensions(cls):
return cls._dimensions

@staticmethod
def static_dimensions():
return Foo._dimensions


class Bar(Foo):
_dimensions = "bar-dimensions"
$ python3 -i staticmethod_demo.py 
>>> Foo.class_dimensions(), Foo.static_dimensions()
('foo-dimensions', 'foo-dimensions')
>>> Bar.class_dimensions(), Bar.static_dimensions()
('bar-dimensions', 'foo-dimensions')

> 
> @staticmethod
> def dimensions1():
> print('id = {}'.format(id(_dimensions)))
> return _dimensions
> =8<--
> The class method Foo.dimensions() is capable of accessing class members,
> but Foo.dimensions1() cannot. What does the @staticmethod decorator really
> add?

You do not really need static methods; they work like module-level 
functions. They are more of a means to organize your code; by writing

class Foo:
   @staticmethod
   def bar(...): 
   do stuff

instead of

def foo_bar(...):
do stuff

class Foo:
pass

you make the mental association between the class and the function a bit 
stronger.

___
Tutor maillist  -  Tutor@python.org
To unsubscribe or change subscription options:
https://mail.python.org/mailman/listinfo/tutor