J'ai eu un entretien de programmation récemment, un écran de téléphone dans lequel nous avons utilisé un éditeur de texte collaboratif .
On m'a demandé de mettre en œuvre un certaine API , et a choisi de le faire dans Python . En résumant l'énoncé du problème, disons que j'avais besoin d'une classe dont les instances stockaient certaines data
et certains other_data
.
J'ai pris une profonde inspiration et j'ai commencé à taper. Après quelques lignes, j'avais quelque chose comme ça:
class Service(object): data = [] def __init__(self, other_data): self.other_data = other_data ...
Mon intervieweur m'a arrêté:
data = []
. Je ne pense pas que ce soit du Python valide? »Pour référence, et pour vous donner une idée de ce que je voulais, voici comment j'ai modifié le code:
comment construire un algorithme d'apprentissage automatique
class Service(object): def __init__(self, other_data): self.data = [] self.other_data = other_data ...
En fin de compte, nous avions tous les deux tort. La vraie réponse réside dans la compréhension de la distinction entre les attributs de classe Python et les attributs d'instance Python.
Remarque: si vous avez un expert sur les attributs de classe, vous pouvez passer directement à cas d'utilisation .
Mon intervieweur s'est trompé en ce que le code ci-dessus est syntaxiquement valide.
Moi aussi, je me suis trompé en ce qu'il ne définit pas de 'valeur par défaut' pour l'attribut d'instance. Au lieu de cela, il définit data
comme un classe attribut avec valeur []
.
D'après mon expérience, les attributs de classe Python sont un sujet qui beaucoup les gens savent quelque chose sur, mais peu comprennent complètement.
Un attribut de classe Python est un attribut de la classe (circulaire, je sais), plutôt qu'un attribut d'un exemple d'une classe.
Utilisons un exemple de classe Python pour illustrer la différence. Ici, class_var
est un attribut de classe, et i_var
est un attribut d'instance:
class MyClass(object): class_var = 1 def __init__(self, i_var): self.i_var = i_var
Notez que toutes les instances de la classe ont accès à class_var
, et qu'il est également accessible en tant que propriété du classe elle-même :
foo = MyClass(2) bar = MyClass(3) foo.class_var, foo.i_var ## 1, 2 bar.class_var, bar.i_var ## 1, 3 MyClass.class_var ## <— This is key ## 1
Pour les programmeurs Java ou C ++, l'attribut de classe est similaire - mais pas identique - au membre statique. Nous verrons en quoi ils diffèrent plus tard.
Pour comprendre ce qui se passe ici, parlons brièvement de Espaces de noms Python .
À espace de noms est un mappage des noms aux objets, avec la propriété qu'il n'y a aucune relation entre les noms dans différents espaces de noms. Ils sont généralement implémentés en tant que dictionnaires Python, bien que cela soit abstrait.
Selon le contexte, vous devrez peut-être accéder à un espace de noms à l'aide de la syntaxe dot (par exemple, object.name_from_objects_namespace
) ou en tant que variable locale (par exemple, object_from_namespace
). A titre d'exemple concret:
class MyClass(object): ## No need for dot syntax class_var = 1 def __init__(self, i_var): self.i_var = i_var ## Need dot syntax as we've left scope of class namespace MyClass.class_var ## 1
Classes Python et les instances de classes ont chacune leurs propres espaces de noms distincts représentés par attributs prédéfinis MyClass.__dict__
et instance_of_MyClass.__dict__
, respectivement.
à quoi sert node.js
Lorsque vous essayez d'accéder à un attribut à partir d'une instance d'une classe, il regarde d'abord son exemple espace de noms. S'il trouve l'attribut, il renvoie la valeur associée. Sinon, il puis regarde dans le classe namespace et renvoie l'attribut (s'il est présent, renvoyer une erreur dans le cas contraire). Par exemple:
foo = MyClass(2) ## Finds i_var in foo's instance namespace foo.i_var ## 2 ## Doesn't find class_var in instance namespace… ## So look's in class namespace (MyClass.__dict__) foo.class_var ## 1
L'espace de noms d'instance prend la suprématie sur l'espace de noms de classe: s'il y a un attribut avec le même nom dans les deux, l'espace de noms d'instance sera vérifié en premier et sa valeur retournée. Voici une version simplifiée du code ( la source ) pour la recherche d'attributs:
def instlookup(inst, name): ## simplified algorithm... if inst.__dict__.has_key(name): return inst.__dict__[name] else: return inst.__class__.__dict__[name]
Et, sous forme visuelle:
Dans cet esprit, nous pouvons comprendre comment les attributs de classe Python gèrent l'affectation:
Si un attribut de classe est défini en accédant à la classe, il remplacera la valeur de tout instances. Par exemple:
foo = MyClass(2) foo.class_var ## 1 MyClass.class_var = 2 foo.class_var ## 2
Au niveau de l'espace de noms… nous définissons MyClass.__dict__['class_var'] = 2
. (Remarque: ce n'est pas le code exact (qui serait setattr(MyClass, 'class_var', 2)
) comme __dict__
renvoie un dictproxy , un wrapper immuable qui empêche l'affectation directe, mais qui aide pour la démonstration). Ensuite, lorsque nous accédons à foo.class_var
, class_var
a une nouvelle valeur dans l'espace de noms de classe et donc 2 est renvoyé.
Si une variable de classe Paython est définie en accédant à une instance, elle remplacera la valeur seulement pour cet exemple . Cela remplace essentiellement la variable de classe et la transforme en une variable d'instance disponible, intuitivement, seulement pour cet exemple . Par exemple:
foo = MyClass(2) foo.class_var ## 1 foo.class_var = 2 foo.class_var ## 2 MyClass.class_var ## 1
Au niveau de l'espace de noms… nous ajoutons le class_var
attribut à foo.__dict__
, donc lorsque nous recherchons foo.class_var
, nous retournons 2. Pendant ce temps, d'autres instances de MyClass
volonté ne pas avoir class_var
dans leurs espaces de noms d'instance, ils continuent donc à trouver class_var
dans MyClass.__dict__
et donc retourner 1.
Question de quiz: que faire si votre attribut de classe a un type mutable ? Vous pouvez manipuler (mutiler?) L'attribut de classe en y accédant via une instance particulière et, à son tour, finir manipuler l'objet référencé auquel toutes les instances accèdent (comme souligné par Timothy Wiseman ).
Ceci est mieux démontré par l'exemple. Revenons à Service
J'ai défini plus tôt et voir comment mon utilisation d'une variable de classe aurait pu conduire à des problèmes plus tard.
class Service(object): data = [] def __init__(self, other_data): self.other_data = other_data ...
Mon objectif était d'avoir la liste vide ([]
) comme valeur par défaut pour data
, et pour chaque instance de Service
avoir ses propres données qui serait modifié au fil du temps, instance par instance. Mais dans ce cas, on obtient le comportement suivant (rappelons que Service
prend un argument other_data
, ce qui est arbitraire dans cet exemple):
s1 = Service(['a', 'b']) s2 = Service(['c', 'd']) s1.data.append(1) s1.data ## [1] s2.data ## [1] s2.data.append(2) s1.data ## [1, 2] s2.data ## [1, 2]
Ce n'est pas bon - modifier la variable de classe via une instance la modifie pour toutes les autres!
Au niveau de l'espace de noms… toutes les instances de Service
accèdent et modifient la même liste dans Service.__dict__
sans faire leur propre data
attributs dans leurs espaces de noms d'instance.
Nous pourrions contourner ce problème en utilisant l'affectation; autrement dit, au lieu d’exploiter la mutabilité de la liste, nous pourrions attribuer notre Service
objets pour avoir leurs propres listes, comme suit:
s1 = Service(['a', 'b']) s2 = Service(['c', 'd']) s1.data = [1] s2.data = [2] s1.data ## [1] s2.data ## [2]
Dans ce cas, nous ajoutons s1.__dict__['data'] = [1]
, donc l'original Service.__dict__['data']
reste inchangé.
Malheureusement, cela nécessite que Service
les utilisateurs ont une connaissance intime de ses variables et sont certainement sujets aux erreurs. Dans un sens, nous nous attaquerons aux symptômes plutôt qu’à la cause. Nous préférerions quelque chose de correct par construction.
Ma solution personnelle: si vous utilisez simplement une variable de classe pour attribuer une valeur par défaut à une variable d'instance Python potentielle, n'utilisez pas de valeurs modifiables . Dans ce cas, chaque instance de Service
allait remplacer Service.data
avec son propre attribut d'instance finalement, donc utiliser une liste vide par défaut a conduit à un petit bogue qui a été facilement ignoré. Au lieu de ce qui précède, nous pourrions avoir soit:
Évité d'utiliser la liste vide (une valeur modifiable) comme «valeur par défaut»:
class Service(object): data = None def __init__(self, other_data): self.other_data = other_data ...
Bien sûr, nous devrons gérer le None
cas approprié, mais c’est un petit prix à payer.
Les attributs de classe sont délicats, mais examinons quelques cas où ils seraient utiles:
Stockage des constantes . Les attributs de classe étant accessibles en tant qu'attributs de la classe elle-même, il est souvent utile de les utiliser pour stocker des constantes spécifiques à la classe à l'échelle de la classe. Par exemple:
class Circle(object): pi = 3.14159 def __init__(self, radius): self.radius = radius def area(self): return Circle.pi * self.radius * self.radius Circle.pi ## 3.14159 c = Circle(10) c.pi ## 3.14159 c.area() ## 314.159
Définition des valeurs par défaut . À titre d'exemple trivial, nous pourrions créer une liste bornée (c'est-à-dire une liste qui ne peut contenir qu'un certain nombre d'éléments ou moins) et choisir d'avoir une limite par défaut de 10 éléments:
class MyClass(object): limit = 10 def __init__(self): self.data = [] def item(self, i): return self.data[i] def add(self, e): if len(self.data) >= self.limit: raise Exception('Too many elements') self.data.append(e) MyClass.limit ## 10
Nous pourrions alors créer des instances avec leurs propres limites spécifiques, en affectant à l'instance limit
attribut.
foo = MyClass() foo.limit = 50 ## foo can now hold 50 elements—other instances can hold 10
Cela n'a de sens que si vous voulez votre instance typique de MyClass
pour ne contenir que 10 éléments ou moins. Si vous attribuez des limites différentes à toutes vos instances, alors limit
doit être une variable d'instance. (Rappelez-vous cependant: faites attention lorsque vous utilisez des valeurs mutables comme valeurs par défaut.)
Suivi de toutes les données sur toutes les instances d'une classe donnée . C'est en quelque sorte spécifique, mais je pourrais voir un scénario dans lequel vous voudrez peut-être accéder à un élément de données lié à chaque instance existante d'une classe donnée.
Pour rendre le scénario plus concret, disons que nous avons un Person
classe, et chaque personne a un name
. Nous voulons garder une trace de tous les noms qui ont été utilisés. Une approche pourrait consister à itérer sur la liste des objets du ramasse-miettes , mais il est plus simple d’utiliser des variables de classe.
Notez que, dans ce cas, names
ne sera accessible qu'en tant que variable de classe, donc la valeur par défaut mutable est acceptable.
class Person(object): all_names = [] def __init__(self, name): self.name = name Person.all_names.append(name) joe = Person('Joe') bob = Person('Bob') print Person.all_names ## ['Joe', 'Bob']
Nous pourrions même utiliser ce modèle de conception pour suivre toutes les instances existantes d'une classe donnée, plutôt que seulement quelques données associées.
class Person(object): all_people = [] def __init__(self, name): self.name = name Person.all_people.append(self) joe = Person('Joe') bob = Person('Bob') print Person.all_people ## [, ]
Performance (sorte de… voir ci-dessous).
Remarque: Si vous vous inquiétez des performances à ce niveau, vous ne voudrez peut-être pas utiliser Python en premier lieu, car les différences seront de l'ordre du dixième de milliseconde - mais c'est toujours amusant de fouiller un peu, et cela aide par souci d'illustration.
Rappelez-vous que l’espace de noms d’une classe est créé et rempli au moment de la définition de la classe. Cela signifie que nous n'effectuons qu'une seule mission - déjà : Pour une variable de classe donnée, tandis que les variables d'instance doivent être affectées à chaque fois qu'une nouvelle instance est créée. Prenons un exemple.
def called_class(): print 'Class assignment' return 2 class Bar(object): y = called_class() def __init__(self, x): self.x = x ## 'Class assignment' def called_instance(): print 'Instance assignment' return 2 class Foo(object): def __init__(self, x): self.y = called_instance() self.x = x Bar(1) Bar(2) Foo(1) ## 'Instance assignment' Foo(2) ## 'Instance assignment'
Nous attribuons à Bar.y
une seule fois, mais instance_of_Foo.y
à chaque appel à __init__
.
Comme preuve supplémentaire, utilisons le Désassembleur Python :
lors de l'élaboration d'un énoncé de problème, un chercheur parle de chacun des éléments suivants, sauf :
import dis class Bar(object): y = 2 def __init__(self, x): self.x = x class Foo(object): def __init__(self, x): self.y = 2 self.x = x dis.dis(Bar) ## Disassembly of __init__: ## 7 0 LOAD_FAST 1 (x) ## 3 LOAD_FAST 0 (self) ## 6 STORE_ATTR 0 (x) ## 9 LOAD_CONST 0 (None) ## 12 RETURN_VALUE dis.dis(Foo) ## Disassembly of __init__: ## 11 0 LOAD_CONST 1 (2) ## 3 LOAD_FAST 0 (self) ## 6 STORE_ATTR 0 (y) ## 12 9 LOAD_FAST 1 (x) ## 12 LOAD_FAST 0 (self) ## 15 STORE_ATTR 1 (x) ## 18 LOAD_CONST 0 (None) ## 21 RETURN_VALUE
Quand on regarde le code octet, il est à nouveau évident que Foo.__init__
doit faire deux affectations, tandis que Bar.__init__
fait juste un.
En pratique, à quoi ressemble vraiment ce gain? Je serai le premier à admettre que les tests de chronométrage dépendent fortement de facteurs souvent incontrôlables et que les différences entre eux sont souvent difficiles à expliquer avec précision.
Cependant, je pense que ces petits extraits (exécutés avec le Python timeit module) aident à illustrer les différences entre les variables de classe et d'instance, donc je les ai quand même incluses.
comment créer un site Web bootstrap
Remarque: je suis sur un MacBook Pro avec OS X 10.8.5 et Python 2.7.2.
10000000 calls to `Bar(2)`: 4.940s 10000000 calls to `Foo(2)`: 6.043s
Les initialisations de Bar
sont plus rapides de plus d'une seconde, donc la différence semble être statistiquement significative.
Alors pourquoi est-ce le cas? Un spéculatif explication: nous faisons deux affectations dans Foo.__init__
, mais une seule dans Bar.__init__
.
10000000 calls to `Bar(2).y = 15`: 6.232s 10000000 calls to `Foo(2).y = 15`: 6.855s 10000000 `Bar` assignments: 6.232s - 4.940s = 1.292s 10000000 `Foo` assignments: 6.855s - 6.043s = 0.812s
Remarque: il est impossible de réexécuter votre code de configuration à chaque essai avec timeit , nous devons donc réinitialiser notre variable lors de notre essai. La deuxième ligne de temps représente les temps ci-dessus avec les temps d'initialisation calculés précédemment déduits.
D'après ce qui précède, il ressemble à Foo
prend seulement environ 60% tant que Bar
pour gérer les affectations.
pourquoi est-ce le cas? Un spéculatif explication: lorsque nous affectons à Bar(2).y
, nous regardons d'abord dans l'espace de noms de l'instance (Bar(2).__dict__[y]
), échouons à trouver y
, puis cherchons dans l'espace de noms de classe (Bar.__dict__[y]
) , puis faire la bonne affectation. Lorsque nous affectons à Foo(2).y
, nous effectuons deux fois moins de recherches que nous affectons immédiatement à l'espace de noms de l'instance (Foo(2).__dict__[y]
).
En résumé, si ces gains de performances n’ont pas d’importance en réalité, ces tests sont intéressants au niveau conceptuel. Au contraire, j'espère que ces différences aideront à illustrer les distinctions mécaniques entre les variables de classe et d'instance.
Les attributs de classe semblent être sous-utilisés en Python; beaucoup de programmeurs ont des impressions différentes sur leur fonctionnement et sur les raisons pour lesquelles ils pourraient être utiles.
Mon avis: les variables de classe Python ont leur place dans l'école du bon code. Utilisés avec précaution, ils peuvent simplifier les choses et améliorer la lisibilité. Mais lorsqu'ils sont négligemment jetés dans une classe donnée, ils ne manqueront pas de vous faire trébucher.
Une chose que je voulais inclure mais je n'avais pas de point d'entrée naturel…
Python n'a pas privé variables pour ainsi dire, mais une autre relation intéressante entre la dénomination de classe et d'instance vient avec la transformation des noms.
Dans le guide de style Python, il est indiqué que les variables pseudo-privées doivent être précédées d’un double trait de soulignement: «__». Ce n'est pas seulement un signe pour les autres que votre variable est censée être traitée en privé, mais aussi un moyen d'en empêcher l'accès, en quelque sorte. Voici ce que je veux dire:
class Bar(object): def __init__(self): self.__zap = 1 a = Bar() a.__zap ## Traceback (most recent call last): ## File '', line 1, in ## AttributeError: 'Bar' object has no attribute '__baz' ## Hmm. So what's in the namespace? a.__dict__ {'_Bar__zap': 1} a._Bar__zap ## 1
Regardez ça: l'attribut d'instance __zap
est automatiquement préfixé avec le nom de la classe pour donner _Bar__zap
.
Bien qu'il soit toujours paramétrable et accessible en utilisant a._Bar__zap
, ce changement de nom est un moyen de créer une variable «privée» car il vous empêche et d'autres d'y accéder par accident ou par ignorance.
Edit: comme l'a gentiment souligné Pedro Werneck, ce comportement est en grande partie destiné à aider avec le sous-classement. dans le Guide de style PEP 8 , ils le considèrent comme ayant deux objectifs: (1) empêcher les sous-classes d'accéder à certains attributs, et (2) empêcher les conflits d'espaces de noms dans ces sous-classes. Bien qu'utile, la modification des variables ne doit pas être considérée comme une invitation à écrire du code avec une distinction supposée public-privé, comme c'est le cas en Java.
En relation: Devenez plus avancé: évitez les 10 erreurs les plus courantes commises par les programmeurs PythonComme son nom l'indique, un espace de noms Python est un mappage de noms à objets, avec la propriété qu'il n'y a aucune relation entre les noms dans différents espaces de noms. Les espaces de noms sont généralement implémentés en tant que dictionnaires Python, bien que cela soit abstrait.
En Python, une méthode de classe est une méthode appelée avec la classe comme contexte. Ceci est souvent connu sous le nom de méthodes statiques dans d'autres langages de programmation. Une méthode d'instance, en revanche, est appelée avec une instance comme contexte.
Dans ce cas, l'espace de noms d'instance prend la suprématie sur l'espace de noms de classe. S'il y a un attribut avec le même nom dans les deux, l'espace de noms de l'instance sera vérifié en premier et sa valeur retournée.